From b566d5bb1fbd9ceb9be72360a4a0e47dbdb50612 Mon Sep 17 00:00:00 2001 From: Jaben Cargman Date: Sun, 5 Oct 2025 15:13:49 -0400 Subject: [PATCH 01/12] minor. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ce51408..1ce198e 100644 --- a/.gitignore +++ b/.gitignore @@ -288,3 +288,4 @@ __pycache__/ *.odx.cs *.xsd.cs +settings.local.json From 64119f326b4b50725bcf479ad14a38264d378dc1 Mon Sep 17 00:00:00 2001 From: Jaben Cargman Date: Sun, 5 Oct 2025 15:40:09 -0400 Subject: [PATCH 02/12] Convert LinqPad examples to .NET 8 console applications MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Created 9 new console app examples in /examples directory - Converted all LinqPad scripts to standalone console applications - Updated all examples to use latest API (v2.8.1) - WithDimensions() → WithPageProperties() - PdfFormats → LibrePdfFormats - Removed deprecated SetUserAgent() and SetEmulatedMediaType() - Removed obsolete EnableNativePdfFormat() - Updated LibrePdfFormats.A1a → A2b (A1a deprecated in Gotenberg 7.6+) - Added centralized configuration via Directory.Build.props - Common PropertyGroup settings (target framework, nullable, etc.) - Shared package references (Microsoft.Extensions.*) - Shared project reference to Gotenberg.Sharp.Api.Client - Automatic resource copying to output directories - Added shared appsettings.json for all examples - Moved and renamed resources to lowercase /examples/resources/ for cross-platform compatibility - Created comprehensive examples/README.md with usage instructions - Updated main README.md to reference examples instead of linqpad folder - All examples build successfully with 0 errors and 0 warnings 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- README.md | 2 +- examples/DIExample/DIExample.csproj | 2 + examples/DIExample/Program.cs | 69 +++++++++++ examples/Directory.Build.props | 31 +++++ examples/HtmlConvert/HtmlConvert.csproj | 2 + examples/HtmlConvert/Program.cs | 51 ++++++++ .../HtmlWithMarkdown/HtmlWithMarkdown.csproj | 2 + examples/HtmlWithMarkdown/Program.cs | 62 ++++++++++ examples/OfficeMerge/OfficeMerge.csproj | 2 + examples/OfficeMerge/Program.cs | 44 +++++++ examples/PdfConvert/PdfConvert.csproj | 2 + examples/PdfConvert/Program.cs | 49 ++++++++ examples/PdfMerge/PdfMerge.csproj | 2 + examples/PdfMerge/Program.cs | 45 ++++++++ examples/README.md | 109 ++++++++++++++++++ examples/UrlConvert/Program.cs | 45 ++++++++ examples/UrlConvert/UrlConvert.csproj | 2 + examples/UrlsToMergedPdf/Program.cs | 91 +++++++++++++++ .../UrlsToMergedPdf/UrlsToMergedPdf.csproj | 2 + examples/Webhook/Program.cs | 46 ++++++++ examples/Webhook/Webhook.csproj | 2 + examples/appsettings.json | 12 ++ .../resources}/Html/ConvertExample/body.html | 0 .../Html/ConvertExample/ear-on-beach.jpg | Bin .../Html/ConvertExample/footer.html | 0 .../resources}/Html/UrlFooter.html | 0 .../resources}/Html/UrlHeader.html | 0 .../resources}/Html/font.woff | Bin .../resources}/Html/footer.html | 0 .../resources}/Html/header.html | 0 .../resources}/Html/img.gif | Bin .../resources}/Html/index.html | 0 .../resources}/Html/style.css | 0 .../resources}/Markdown/font.woff | Bin .../resources}/Markdown/footer.html | 0 .../resources}/Markdown/header.html | 0 .../resources}/Markdown/img.gif | Bin .../resources}/Markdown/index.html | 0 .../resources}/Markdown/paragraph1.md | 0 .../resources}/Markdown/paragraph2.md | 0 .../resources}/Markdown/paragraph3.md | 0 .../resources}/Markdown/style.css | 0 .../resources}/OfficeDocs/LorumIpsem.txt | 0 .../OfficeDocs/Visual-Studio-NOTICE.docx | Bin .../resources}/OfficeDocs/document.docx | Bin .../resources}/OfficeDocs/document.rtf | 0 .../resources}/OfficeDocs/document2.docx | Bin .../resources}/Settings/appsettings.json | 0 .../resources}/office/document.docx | Bin 49 files changed, 673 insertions(+), 1 deletion(-) create mode 100644 examples/DIExample/DIExample.csproj create mode 100644 examples/DIExample/Program.cs create mode 100644 examples/Directory.Build.props create mode 100644 examples/HtmlConvert/HtmlConvert.csproj create mode 100644 examples/HtmlConvert/Program.cs create mode 100644 examples/HtmlWithMarkdown/HtmlWithMarkdown.csproj create mode 100644 examples/HtmlWithMarkdown/Program.cs create mode 100644 examples/OfficeMerge/OfficeMerge.csproj create mode 100644 examples/OfficeMerge/Program.cs create mode 100644 examples/PdfConvert/PdfConvert.csproj create mode 100644 examples/PdfConvert/Program.cs create mode 100644 examples/PdfMerge/PdfMerge.csproj create mode 100644 examples/PdfMerge/Program.cs create mode 100644 examples/README.md create mode 100644 examples/UrlConvert/Program.cs create mode 100644 examples/UrlConvert/UrlConvert.csproj create mode 100644 examples/UrlsToMergedPdf/Program.cs create mode 100644 examples/UrlsToMergedPdf/UrlsToMergedPdf.csproj create mode 100644 examples/Webhook/Program.cs create mode 100644 examples/Webhook/Webhook.csproj create mode 100644 examples/appsettings.json rename {linqpad/Resources => examples/resources}/Html/ConvertExample/body.html (100%) rename {linqpad/Resources => examples/resources}/Html/ConvertExample/ear-on-beach.jpg (100%) rename {linqpad/Resources => examples/resources}/Html/ConvertExample/footer.html (100%) rename {linqpad/Resources => examples/resources}/Html/UrlFooter.html (100%) rename {linqpad/Resources => examples/resources}/Html/UrlHeader.html (100%) rename {linqpad/Resources => examples/resources}/Html/font.woff (100%) rename {linqpad/Resources => examples/resources}/Html/footer.html (100%) rename {linqpad/Resources => examples/resources}/Html/header.html (100%) rename {linqpad/Resources => examples/resources}/Html/img.gif (100%) rename {linqpad/Resources => examples/resources}/Html/index.html (100%) rename {linqpad/Resources => examples/resources}/Html/style.css (100%) rename {linqpad/Resources => examples/resources}/Markdown/font.woff (100%) rename {linqpad/Resources => examples/resources}/Markdown/footer.html (100%) rename {linqpad/Resources => examples/resources}/Markdown/header.html (100%) rename {linqpad/Resources => examples/resources}/Markdown/img.gif (100%) rename {linqpad/Resources => examples/resources}/Markdown/index.html (100%) rename {linqpad/Resources => examples/resources}/Markdown/paragraph1.md (100%) rename {linqpad/Resources => examples/resources}/Markdown/paragraph2.md (100%) rename {linqpad/Resources => examples/resources}/Markdown/paragraph3.md (100%) rename {linqpad/Resources => examples/resources}/Markdown/style.css (100%) rename {linqpad/Resources => examples/resources}/OfficeDocs/LorumIpsem.txt (100%) rename {linqpad/Resources => examples/resources}/OfficeDocs/Visual-Studio-NOTICE.docx (100%) rename {linqpad/Resources => examples/resources}/OfficeDocs/document.docx (100%) rename {linqpad/Resources => examples/resources}/OfficeDocs/document.rtf (100%) rename {linqpad/Resources => examples/resources}/OfficeDocs/document2.docx (100%) rename {linqpad/Resources => examples/resources}/Settings/appsettings.json (100%) rename {linqpad/Resources => examples/resources}/office/document.docx (100%) diff --git a/README.md b/README.md index f22b854..7459610 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ public void ConfigureServices(IServiceCollection services) ``` # Using GotenbergSharpClient -*See the [linqPad folder](linqpad/)* for complete examples. +*See the [examples folder](examples/)* for complete working examples as console applications. ## Required Using Statements ```csharp diff --git a/examples/DIExample/DIExample.csproj b/examples/DIExample/DIExample.csproj new file mode 100644 index 0000000..35e3d84 --- /dev/null +++ b/examples/DIExample/DIExample.csproj @@ -0,0 +1,2 @@ + + diff --git a/examples/DIExample/Program.cs b/examples/DIExample/Program.cs new file mode 100644 index 0000000..93eaf38 --- /dev/null +++ b/examples/DIExample/Program.cs @@ -0,0 +1,69 @@ +using Gotenberg.Sharp.API.Client; +using Gotenberg.Sharp.API.Client.Domain.Builders; +using Gotenberg.Sharp.API.Client.Domain.Builders.Faceted; +using Gotenberg.Sharp.API.Client.Domain.Requests; +using Gotenberg.Sharp.API.Client.Domain.Settings; +using Gotenberg.Sharp.API.Client.Extensions; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +// Builds a simple DI container with logging enabled. +// Client retrieved through the service provider is configured with options defined in appsettings.json +// Watch the polly-retry policy in action: +// Turn off gotenberg, run this script and let it fail/retry two or three times. +// Turn gotenberg back on & the request will successfully complete. +// Example builds a 1 page PDF from the specified TargetUrl + +const string TargetUrl = "https://www.cnn.com"; +var saveToPath = args.Length > 0 ? args[0] : Path.Combine(Directory.GetCurrentDirectory(), "output"); +Directory.CreateDirectory(saveToPath); + +var services = BuildServiceCollection(); +var sp = services.BuildServiceProvider(); + +var sharpClient = sp.GetRequiredService(); +var request = await CreateUrlRequest(); +var response = await sharpClient.UrlToPdfAsync(request); + +var resultPath = Path.Combine(saveToPath, $"GotenbergFromUrl-{DateTime.Now:yyyyMMddHHmmss}.pdf"); + +using (var destinationStream = File.Create(resultPath)) +{ + await response.CopyToAsync(destinationStream); +} + +Console.WriteLine($"PDF created: {resultPath}"); + +IServiceCollection BuildServiceCollection() +{ + var config = new ConfigurationBuilder() + .SetBasePath(AppContext.BaseDirectory) + .AddJsonFile("appsettings.json") + .Build(); + + return new ServiceCollection() + .AddOptions() + .Bind(config.GetSection(nameof(GotenbergSharpClient))).Services + .AddGotenbergSharpClient() + .Services.AddLogging(s => s.AddSimpleConsole(ops => + { + ops.IncludeScopes = true; + ops.SingleLine = false; + ops.TimestampFormat = "hh:mm:ss "; + })); +} + +Task CreateUrlRequest() +{ + var builder = new UrlRequestBuilder() + .SetUrl(TargetUrl) + .ConfigureRequest(b => b.SetPageRanges("1-2")) + .WithPageProperties(b => + { + b.SetPaperSize(PaperSizes.A4) + .SetMargins(Margins.None); + }); + + return builder.BuildAsync(); +} diff --git a/examples/Directory.Build.props b/examples/Directory.Build.props new file mode 100644 index 0000000..8e0da1b --- /dev/null +++ b/examples/Directory.Build.props @@ -0,0 +1,31 @@ + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + diff --git a/examples/HtmlConvert/HtmlConvert.csproj b/examples/HtmlConvert/HtmlConvert.csproj new file mode 100644 index 0000000..35e3d84 --- /dev/null +++ b/examples/HtmlConvert/HtmlConvert.csproj @@ -0,0 +1,2 @@ + + diff --git a/examples/HtmlConvert/Program.cs b/examples/HtmlConvert/Program.cs new file mode 100644 index 0000000..7a2456b --- /dev/null +++ b/examples/HtmlConvert/Program.cs @@ -0,0 +1,51 @@ +using Gotenberg.Sharp.API.Client; +using Gotenberg.Sharp.API.Client.Domain.Builders; +using Gotenberg.Sharp.API.Client.Domain.Builders.Faceted; + +var destinationDirectory = args.Length > 0 ? args[0] : Path.Combine(Directory.GetCurrentDirectory(), "output"); +Directory.CreateDirectory(destinationDirectory); + +var resourcePath = Path.Combine(AppContext.BaseDirectory, "resources", "Html", "ConvertExample"); +var path = await CreateFromHtml(destinationDirectory, resourcePath); + +Console.WriteLine($"PDF created: {path}"); + +static async Task CreateFromHtml(string destinationDirectory, string resourcePath) +{ + var sharpClient = new GotenbergSharpClient("http://localhost:3000"); + + var builder = new HtmlRequestBuilder() + .AddAsyncDocument(async doc => + doc.SetBody(await GetHtmlFile(resourcePath, "body.html")) + .SetFooter(await GetHtmlFile(resourcePath, "footer.html")) + ).WithPageProperties(dims => dims.UseChromeDefaults()) + .WithAsyncAssets(async assets => + assets.AddItem("ear-on-beach.jpg", await GetImageBytes(resourcePath)) + ) + .SetConversionBehaviors(b => + b.AddAdditionalHeaders("hello", "from-earth") + ) + .ConfigureRequest(b => b.SetPageRanges("1")); + + var request = await builder.BuildAsync(); + + var resultPath = Path.Combine(destinationDirectory, $"GotenbergFromHtml-{DateTime.Now:yyyyMMddHHmmss}.pdf"); + var response = await sharpClient.HtmlToPdfAsync(request); + + using (var destinationStream = File.Create(resultPath)) + { + await response.CopyToAsync(destinationStream); + } + + return resultPath; +} + +static Task GetImageBytes(string resourcePath) +{ + return File.ReadAllBytesAsync(Path.Combine(resourcePath, "ear-on-beach.jpg")); +} + +static Task GetHtmlFile(string resourcePath, string fileName) +{ + return File.ReadAllBytesAsync(Path.Combine(resourcePath, fileName)); +} diff --git a/examples/HtmlWithMarkdown/HtmlWithMarkdown.csproj b/examples/HtmlWithMarkdown/HtmlWithMarkdown.csproj new file mode 100644 index 0000000..35e3d84 --- /dev/null +++ b/examples/HtmlWithMarkdown/HtmlWithMarkdown.csproj @@ -0,0 +1,2 @@ + + diff --git a/examples/HtmlWithMarkdown/Program.cs b/examples/HtmlWithMarkdown/Program.cs new file mode 100644 index 0000000..aa1e5cc --- /dev/null +++ b/examples/HtmlWithMarkdown/Program.cs @@ -0,0 +1,62 @@ +using Gotenberg.Sharp.API.Client; +using Gotenberg.Sharp.API.Client.Domain.Builders; + +var destinationDirectory = args.Length > 0 ? args[0] : Path.Combine(Directory.GetCurrentDirectory(), "output"); +Directory.CreateDirectory(destinationDirectory); + +var resourcePath = Path.Combine(AppContext.BaseDirectory, "resources", "Markdown"); +var path = await CreateFromMarkdown(destinationDirectory, resourcePath); + +Console.WriteLine($"PDF created from Markdown: {path}"); + +static async Task CreateFromMarkdown(string destinationDirectory, string resourcePath) +{ + var sharpClient = new GotenbergSharpClient("http://localhost:3000"); + + var builder = new HtmlRequestBuilder() + .AddAsyncDocument(async b => + b.SetHeader(await GetFile(resourcePath, "header.html")) + .SetBody(await GetFile(resourcePath, "index.html")) + .SetContainsMarkdown() + .SetFooter(await GetFile(resourcePath, "footer.html")) + ).WithPageProperties(b => + { + b.UseChromeDefaults() + .SetLandscape() + .SetScale(.90); + }).WithAsyncAssets(async b => + b.AddItems(await GetMarkdownAssets(resourcePath)) + ) + .ConfigureRequest(b => b.SetResultFileName("hello.pdf")) + .SetConversionBehaviors(b => b.SetBrowserWaitDelay(2)); + + var request = await builder.BuildAsync(); + var response = await sharpClient.HtmlToPdfAsync(request); + + var outPath = Path.Combine(destinationDirectory, $"GotenbergFromMarkDown-{DateTime.Now:yyyyMMddHHmmss}.pdf"); + + using (var destinationStream = File.Create(outPath)) + { + await response.CopyToAsync(destinationStream); + } + + return outPath; +} + +static async Task GetFile(string resourcePath, string fileName) + => await File.ReadAllTextAsync(Path.Combine(resourcePath, fileName)); + +static async Task>> GetMarkdownAssets(string resourcePath) +{ + var bodyAssetNames = new[] { "img.gif", "font.woff", "style.css" }; + var markdownFiles = new[] { "paragraph1.md", "paragraph2.md", "paragraph3.md" }; + + var bodyAssetTasks = bodyAssetNames.Select(ba => GetFile(resourcePath, ba)); + var mdTasks = markdownFiles.Select(md => GetFile(resourcePath, md)); + + var bodyAssets = await Task.WhenAll(bodyAssetTasks); + var mdParagraphs = await Task.WhenAll(mdTasks); + + return bodyAssetNames.Select((name, index) => KeyValuePair.Create(name, bodyAssets[index])) + .Concat(markdownFiles.Select((name, index) => KeyValuePair.Create(name, mdParagraphs[index]))); +} diff --git a/examples/OfficeMerge/OfficeMerge.csproj b/examples/OfficeMerge/OfficeMerge.csproj new file mode 100644 index 0000000..35e3d84 --- /dev/null +++ b/examples/OfficeMerge/OfficeMerge.csproj @@ -0,0 +1,2 @@ + + diff --git a/examples/OfficeMerge/Program.cs b/examples/OfficeMerge/Program.cs new file mode 100644 index 0000000..5e79993 --- /dev/null +++ b/examples/OfficeMerge/Program.cs @@ -0,0 +1,44 @@ +using Gotenberg.Sharp.API.Client; +using Gotenberg.Sharp.API.Client.Domain.Builders; +using Gotenberg.Sharp.API.Client.Domain.Builders.Faceted; + +var sourceDirectory = args.Length > 0 ? args[0] : Path.Combine(AppContext.BaseDirectory, "resources", "OfficeDocs"); +var destinationDirectory = args.Length > 1 ? args[1] : Path.Combine(Directory.GetCurrentDirectory(), "output"); +Directory.CreateDirectory(destinationDirectory); + +var path = await DoOfficeMerge(sourceDirectory, destinationDirectory); +Console.WriteLine($"Merged Office documents PDF created: {path}"); + +static async Task DoOfficeMerge(string sourceDirectory, string destinationDirectory) +{ + var client = new GotenbergSharpClient("http://localhost:3000"); + + var builder = new MergeOfficeBuilder() + .ConfigureRequest(c => c.SetTrace("ConsoleExample")) + .WithAsyncAssets(async b => b.AddItems(await GetDocsAsync(sourceDirectory))) + .SetPdfFormat(LibrePdfFormats.A2b) + .SetPageRanges("1-3"); // Only one of the files has more than 1 page. + + var response = await client.MergeOfficeDocsAsync(builder).ConfigureAwait(false); + + var mergeResultPath = Path.Combine(destinationDirectory, $"GotenbergOfficeMerge-{DateTime.Now:yyyyMMddHHmmss}.pdf"); + + using (var destinationStream = File.Create(mergeResultPath)) + { + await response.CopyToAsync(destinationStream).ConfigureAwait(false); + } + + return mergeResultPath; +} + +static async Task>> GetDocsAsync(string sourceDirectory) +{ + var paths = Directory.GetFiles(sourceDirectory, "*.*", SearchOption.TopDirectoryOnly); + var names = paths.Select(p => new FileInfo(p).Name); + var tasks = paths.Select(f => File.ReadAllBytesAsync(f)); + + var docs = await Task.WhenAll(tasks); + + return names.Select((name, index) => KeyValuePair.Create(name, docs[index])) + .Take(10); +} diff --git a/examples/PdfConvert/PdfConvert.csproj b/examples/PdfConvert/PdfConvert.csproj new file mode 100644 index 0000000..35e3d84 --- /dev/null +++ b/examples/PdfConvert/PdfConvert.csproj @@ -0,0 +1,2 @@ + + diff --git a/examples/PdfConvert/Program.cs b/examples/PdfConvert/Program.cs new file mode 100644 index 0000000..acd233b --- /dev/null +++ b/examples/PdfConvert/Program.cs @@ -0,0 +1,49 @@ +using Gotenberg.Sharp.API.Client; +using Gotenberg.Sharp.API.Client.Domain.Builders; +using Gotenberg.Sharp.API.Client.Domain.Builders.Faceted; + +// If you get 1 file, the result is a PDF; get more and the API returns a zip containing the results +// Currently, Gotenberg supports these formats: A2b & A3b + +var sourcePath = args.Length > 0 ? args[0] : Path.Combine(Directory.GetCurrentDirectory(), "pdfs"); +var destinationPath = args.Length > 1 ? args[1] : Path.Combine(Directory.GetCurrentDirectory(), "output"); +Directory.CreateDirectory(destinationPath); + +var result = await DoConversion(sourcePath, destinationPath); +Console.WriteLine($"Converted PDF created: {result}"); + +static async Task DoConversion(string sourcePath, string destinationPath) +{ + var sharpClient = new GotenbergSharpClient("http://localhost:3000"); + + var items = Directory.GetFiles(sourcePath, "*.pdf", SearchOption.TopDirectoryOnly) + .Select(p => new { Info = new FileInfo(p), Path = p }) + .OrderBy(item => item.Info.CreationTime) + .Take(2); + + Console.WriteLine($"Converting {items.Count()} PDFs:"); + foreach (var item in items) + { + Console.WriteLine($" - {item.Info.Name}"); + } + + var toConvert = items.Select(item => KeyValuePair.Create(item.Info.Name, File.ReadAllBytes(item.Path))); + + var builder = new PdfConversionBuilder() + .WithPdfs(b => b.AddItems(toConvert)) + .SetPdfFormat(LibrePdfFormats.A2b); + + var request = builder.Build(); + var response = await sharpClient.ConvertPdfDocumentsAsync(request); + + // If you send one in -- the result is PDF. + var extension = items.Count() > 1 ? "zip" : "pdf"; + var outPath = Path.Combine(destinationPath, $"GotenbergConvertResult.{extension}"); + + using (var destinationStream = File.Create(outPath)) + { + await response.CopyToAsync(destinationStream, CancellationToken.None); + } + + return outPath; +} diff --git a/examples/PdfMerge/PdfMerge.csproj b/examples/PdfMerge/PdfMerge.csproj new file mode 100644 index 0000000..35e3d84 --- /dev/null +++ b/examples/PdfMerge/PdfMerge.csproj @@ -0,0 +1,2 @@ + + diff --git a/examples/PdfMerge/Program.cs b/examples/PdfMerge/Program.cs new file mode 100644 index 0000000..d4b8d49 --- /dev/null +++ b/examples/PdfMerge/Program.cs @@ -0,0 +1,45 @@ +using Gotenberg.Sharp.API.Client; +using Gotenberg.Sharp.API.Client.Domain.Builders; +using Gotenberg.Sharp.API.Client.Domain.Builders.Faceted; + +var sourcePath = args.Length > 0 ? args[0] : Path.Combine(Directory.GetCurrentDirectory(), "pdfs"); +var destinationPath = args.Length > 1 ? args[1] : Path.Combine(Directory.GetCurrentDirectory(), "output"); +Directory.CreateDirectory(destinationPath); + +var result = await DoMerge(sourcePath, destinationPath); +Console.WriteLine($"Merged PDF created: {result}"); + +static async Task DoMerge(string sourcePath, string destinationPath) +{ + var sharpClient = new GotenbergSharpClient("http://localhost:3000"); + + var items = Directory.GetFiles(sourcePath, "*.pdf", SearchOption.TopDirectoryOnly) + .Select(p => new { Info = new FileInfo(p), Path = p }) + .Where(item => !item.Info.Name.Contains("GotenbergMergeResult.pdf")) + .OrderBy(item => item.Info.CreationTime) + .Take(2); + + Console.WriteLine($"Merging {items.Count()} PDFs:"); + foreach (var item in items) + { + Console.WriteLine($" - {item.Info.Name}"); + } + + var toMerge = items.Select(item => KeyValuePair.Create(item.Info.Name, File.ReadAllBytes(item.Path))); + + var builder = new MergeBuilder() + .SetPdfFormat(LibrePdfFormats.A2b) + .WithAssets(b => { b.AddItems(toMerge); }); + + var request = builder.Build(); + var response = await sharpClient.MergePdfsAsync(request); + + var outPath = Path.Combine(destinationPath, "GotenbergMergeResult.pdf"); + + using (var destinationStream = File.Create(outPath)) + { + await response.CopyToAsync(destinationStream, CancellationToken.None); + } + + return outPath; +} diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..e4e15cb --- /dev/null +++ b/examples/README.md @@ -0,0 +1,109 @@ +# Gotenberg Sharp Client Examples + +This directory contains console application examples demonstrating various features of the Gotenberg Sharp API Client. + +## Prerequisites + +1. **Gotenberg Server**: You need a running Gotenberg instance. See the [main README](../README.md) for setup instructions. + +2. **.NET 8.0 SDK**: All examples target .NET 8.0. + +## Configuration + +All examples share a common configuration file: `appsettings.json` + +You can modify the Gotenberg service URL and retry policy settings in this file. + +## Examples + +### HtmlConvert +Converts HTML to PDF with embedded assets. + +```bash +cd HtmlConvert +dotnet run [output-directory] +``` + +### DIExample +Demonstrates dependency injection setup with logging and Polly retry policy. + +```bash +cd DIExample +dotnet run [output-directory] +``` + +### PdfMerge +Merges multiple PDF files into a single PDF. + +```bash +cd PdfMerge +dotnet run [source-directory] [output-directory] +``` + +### UrlsToMergedPdf +Converts multiple URLs to PDFs and merges them into a single document. Creates a news summary from major news sites. + +**Note**: Requires increased Gotenberg timeout (`--api-timeout=1800s`) + +```bash +cd UrlsToMergedPdf +dotnet run [output-directory] +``` + +### HtmlWithMarkdown +Converts HTML containing Markdown to PDF. + +```bash +cd HtmlWithMarkdown +dotnet run [output-directory] +``` + +### OfficeMerge +Merges Office documents (Word, Excel, PowerPoint) into a single PDF. + +```bash +cd OfficeMerge +dotnet run [source-directory] [output-directory] +``` + +### UrlConvert +Converts a URL to PDF with custom header and footer. + +```bash +cd UrlConvert +dotnet run [output-directory] +``` + +### Webhook +Demonstrates webhook functionality for async PDF generation. + +**Note**: Requires a webhook receiver API running on `localhost:5000` + +```bash +cd Webhook +dotnet run +``` + +### PdfConvert +Converts PDF files to PDF/A formats (A1a, A2b, A3b). + +```bash +cd PdfConvert +dotnet run [source-directory] [output-directory] +``` + +## Project Structure + +- **Directory.Build.props**: Shared configuration for all example projects (target framework, dependencies, resources) +- **appsettings.json**: Shared Gotenberg client configuration +- **resources/**: Shared resource files used by the examples (HTML templates, images, Office documents, etc.) + +## Running Examples + +Each example can be run independently. Most examples accept optional command-line arguments for specifying input/output directories. If no arguments are provided, they use sensible defaults (usually `./output` for generated files). + +Example: +```bash +cd PdfMerge +dotnet run C:\MyPdfs C:\Output +``` diff --git a/examples/UrlConvert/Program.cs b/examples/UrlConvert/Program.cs new file mode 100644 index 0000000..1f574ef --- /dev/null +++ b/examples/UrlConvert/Program.cs @@ -0,0 +1,45 @@ +using Gotenberg.Sharp.API.Client; +using Gotenberg.Sharp.API.Client.Domain.Builders; +using Gotenberg.Sharp.API.Client.Domain.Builders.Faceted; + +var destinationPath = args.Length > 0 ? args[0] : Path.Combine(Directory.GetCurrentDirectory(), "output"); +Directory.CreateDirectory(destinationPath); + +var resourcePath = Path.Combine(AppContext.BaseDirectory, "resources", "Html"); +var headerPath = Path.Combine(resourcePath, "UrlHeader.html"); +var footerPath = Path.Combine(resourcePath, "UrlFooter.html"); + +var path = await CreateFromUrl(destinationPath, headerPath, footerPath); +Console.WriteLine($"PDF created from URL: {path}"); + +static async Task CreateFromUrl(string destinationPath, string headerPath, string footerPath) +{ + var sharpClient = new GotenbergSharpClient("http://localhost:3000"); + + var builder = new UrlRequestBuilder() + .SetUrl("https://www.cnn.com") + .SetConversionBehaviors(b => b.SetBrowserWaitDelay(1)) + .ConfigureRequest(b => b.SetTrace("ConsoleExample").SetPageRanges("1-2")) + .AddAsyncHeaderFooter(async b => + b.SetHeader(await File.ReadAllBytesAsync(headerPath)) + .SetFooter(await File.ReadAllBytesAsync(footerPath)) + ) + .WithPageProperties(b => + b.SetPaperSize(PaperSizes.A4) + .UseChromeDefaults() + .SetMarginLeft(0) + .SetMarginRight(0) + ); + + var request = await builder.BuildAsync(); + var response = await sharpClient.UrlToPdfAsync(request); + + var resultPath = Path.Combine(destinationPath, $"GotenbergFromUrl-{DateTime.Now:yyyyMMddHHmmss}.pdf"); + + using (var destinationStream = File.Create(resultPath)) + { + await response.CopyToAsync(destinationStream); + } + + return resultPath; +} diff --git a/examples/UrlConvert/UrlConvert.csproj b/examples/UrlConvert/UrlConvert.csproj new file mode 100644 index 0000000..35e3d84 --- /dev/null +++ b/examples/UrlConvert/UrlConvert.csproj @@ -0,0 +1,2 @@ + + diff --git a/examples/UrlsToMergedPdf/Program.cs b/examples/UrlsToMergedPdf/Program.cs new file mode 100644 index 0000000..44a734b --- /dev/null +++ b/examples/UrlsToMergedPdf/Program.cs @@ -0,0 +1,91 @@ +using Gotenberg.Sharp.API.Client; +using Gotenberg.Sharp.API.Client.Domain.Builders; +using Gotenberg.Sharp.API.Client.Domain.Builders.Faceted; +using Gotenberg.Sharp.API.Client.Domain.Requests; + +// NOTE: You need to increase gotenberg api's timeout for this to work +// by passing --api-timeout=1800s when running the container. + +var destinationDirectory = args.Length > 0 ? args[0] : Path.Combine(Directory.GetCurrentDirectory(), "output"); +Directory.CreateDirectory(destinationDirectory); + +var path = await CreateWorldNewsSummary(destinationDirectory); +Console.WriteLine($"News summary PDF created: {path}"); + +static async Task CreateWorldNewsSummary(string destinationDirectory) +{ + var sites = new[] + { + "https://www.nytimes.com", "https://www.axios.com/", + "https://www.cnn.com", "https://www.csmonitor.com", + "https://www.wsj.com", "https://www.usatoday.com", + "https://www.irishtimes.com", "https://www.lemonde.fr", + "https://calgaryherald.com", "https://www.bbc.com/news/uk", + "https://english.elpais.com/", "https://www.thehindu.com", + "https://www.theaustralian.com.au", "https://www.welt.de", + "https://www.cankaoxiaoxi.com", "https://www.novinky.cz", + "https://www.elobservador.com.uy" + } + .Select(u => new Uri(u)); + + var builders = CreateRequestBuilders(sites); + var requests = builders.Select(b => b.Build()); + + return await ExecuteRequestsAndMerge(requests, destinationDirectory); +} + +static IEnumerable CreateRequestBuilders(IEnumerable uris) +{ + foreach (var uri in uris) + { + yield return new UrlRequestBuilder() + .SetUrl(uri) + .ConfigureRequest(b => + { + b.SetPageRanges("1-2"); + }) + .WithPageProperties(b => + { + b.SetMargins(Margins.None) + .SetMarginLeft(.3) + .SetMarginRight(.3); + }); + } +} + +static async Task ExecuteRequestsAndMerge(IEnumerable requests, string destinationDirectory) +{ + var innerClient = new HttpClient + { + BaseAddress = new Uri("http://localhost:3000"), + Timeout = TimeSpan.FromMinutes(7) + }; + + var sharpClient = new GotenbergSharpClient(innerClient); + + Console.WriteLine("Converting URLs to PDFs..."); + var tasks = requests.Select(r => sharpClient.UrlToPdfAsync(r, CancellationToken.None)); + var results = await Task.WhenAll(tasks); + + Console.WriteLine("Merging PDFs..."); + var mergeBuilder = new MergeBuilder() + .WithAssets(b => + { + b.AddItems(results.Select((r, i) => KeyValuePair.Create($"{i}.pdf", r))); + }); + + var response = await sharpClient.MergePdfsAsync(mergeBuilder.Build()); + + return await WriteFileAndGetPath(response, destinationDirectory); +} + +static async Task WriteFileAndGetPath(Stream responseStream, string destinationDirectory) +{ + var fullPath = Path.Combine(destinationDirectory, $"{DateTime.Now:yyyy-MM-dd}-{DateTime.Now.Ticks}.pdf"); + + using (var destinationStream = File.Create(fullPath)) + { + await responseStream.CopyToAsync(destinationStream); + } + return fullPath; +} diff --git a/examples/UrlsToMergedPdf/UrlsToMergedPdf.csproj b/examples/UrlsToMergedPdf/UrlsToMergedPdf.csproj new file mode 100644 index 0000000..35e3d84 --- /dev/null +++ b/examples/UrlsToMergedPdf/UrlsToMergedPdf.csproj @@ -0,0 +1,2 @@ + + diff --git a/examples/Webhook/Program.cs b/examples/Webhook/Program.cs new file mode 100644 index 0000000..b23fd89 --- /dev/null +++ b/examples/Webhook/Program.cs @@ -0,0 +1,46 @@ +using Gotenberg.Sharp.API.Client; +using Gotenberg.Sharp.API.Client.Domain.Builders; +using Gotenberg.Sharp.API.Client.Domain.Builders.Faceted; + +// For this to work you need an API running on localhost:5000 with an endpoint to receive the webhook + +var resourcePath = Path.Combine(AppContext.BaseDirectory, "resources", "Html"); +var footerPath = Path.Combine(resourcePath, "UrlHeader.html"); +var headerPath = Path.Combine(resourcePath, "UrlFooter.html"); + +Console.WriteLine($"Header: {headerPath}"); +Console.WriteLine($"Footer: {footerPath}"); + +await CreateFromUrl(headerPath, footerPath); + +Console.WriteLine("Webhook request sent..."); + +static async Task CreateFromUrl(string headerPath, string footerPath) +{ + var sharpClient = new GotenbergSharpClient("http://localhost:3000"); + + var builder = new UrlRequestBuilder() + .SetUrl("https://www.newyorker.com") + .ConfigureRequest(b => + { + b.AddWebhook(hook => + { + hook.SetUrl("http://host.docker.internal:5000/api/WebhookReceiver") + .SetErrorUrl("http://host.docker.internal:5000/api/WebhookReceiver") + .AddExtraHeader("custom-header", "value"); + }).SetPageRanges("1-2"); + }) + .AddAsyncHeaderFooter(async b => + b.SetHeader(await File.ReadAllTextAsync(headerPath)) + .SetFooter(await File.ReadAllBytesAsync(footerPath)) + ) + .WithPageProperties(b => + { + b.SetPaperSize(PaperSizes.A4) + .SetMargins(Margins.None); + }); + + var request = await builder.BuildAsync(); + + await sharpClient.FireWebhookAndForgetAsync(request); +} diff --git a/examples/Webhook/Webhook.csproj b/examples/Webhook/Webhook.csproj new file mode 100644 index 0000000..35e3d84 --- /dev/null +++ b/examples/Webhook/Webhook.csproj @@ -0,0 +1,2 @@ + + diff --git a/examples/appsettings.json b/examples/appsettings.json new file mode 100644 index 0000000..6aae438 --- /dev/null +++ b/examples/appsettings.json @@ -0,0 +1,12 @@ +{ + "GotenbergSharpClient": { + "ServiceUrl": "http://localhost:3000", + "HealthCheckUrl": "http://localhost:3000/health", + "RetryPolicy": { + "Enabled": true, + "RetryCount": 4, + "BackoffPower": 1.5, + "LoggingEnabled": true + } + } +} diff --git a/linqpad/Resources/Html/ConvertExample/body.html b/examples/resources/Html/ConvertExample/body.html similarity index 100% rename from linqpad/Resources/Html/ConvertExample/body.html rename to examples/resources/Html/ConvertExample/body.html diff --git a/linqpad/Resources/Html/ConvertExample/ear-on-beach.jpg b/examples/resources/Html/ConvertExample/ear-on-beach.jpg similarity index 100% rename from linqpad/Resources/Html/ConvertExample/ear-on-beach.jpg rename to examples/resources/Html/ConvertExample/ear-on-beach.jpg diff --git a/linqpad/Resources/Html/ConvertExample/footer.html b/examples/resources/Html/ConvertExample/footer.html similarity index 100% rename from linqpad/Resources/Html/ConvertExample/footer.html rename to examples/resources/Html/ConvertExample/footer.html diff --git a/linqpad/Resources/Html/UrlFooter.html b/examples/resources/Html/UrlFooter.html similarity index 100% rename from linqpad/Resources/Html/UrlFooter.html rename to examples/resources/Html/UrlFooter.html diff --git a/linqpad/Resources/Html/UrlHeader.html b/examples/resources/Html/UrlHeader.html similarity index 100% rename from linqpad/Resources/Html/UrlHeader.html rename to examples/resources/Html/UrlHeader.html diff --git a/linqpad/Resources/Html/font.woff b/examples/resources/Html/font.woff similarity index 100% rename from linqpad/Resources/Html/font.woff rename to examples/resources/Html/font.woff diff --git a/linqpad/Resources/Html/footer.html b/examples/resources/Html/footer.html similarity index 100% rename from linqpad/Resources/Html/footer.html rename to examples/resources/Html/footer.html diff --git a/linqpad/Resources/Html/header.html b/examples/resources/Html/header.html similarity index 100% rename from linqpad/Resources/Html/header.html rename to examples/resources/Html/header.html diff --git a/linqpad/Resources/Html/img.gif b/examples/resources/Html/img.gif similarity index 100% rename from linqpad/Resources/Html/img.gif rename to examples/resources/Html/img.gif diff --git a/linqpad/Resources/Html/index.html b/examples/resources/Html/index.html similarity index 100% rename from linqpad/Resources/Html/index.html rename to examples/resources/Html/index.html diff --git a/linqpad/Resources/Html/style.css b/examples/resources/Html/style.css similarity index 100% rename from linqpad/Resources/Html/style.css rename to examples/resources/Html/style.css diff --git a/linqpad/Resources/Markdown/font.woff b/examples/resources/Markdown/font.woff similarity index 100% rename from linqpad/Resources/Markdown/font.woff rename to examples/resources/Markdown/font.woff diff --git a/linqpad/Resources/Markdown/footer.html b/examples/resources/Markdown/footer.html similarity index 100% rename from linqpad/Resources/Markdown/footer.html rename to examples/resources/Markdown/footer.html diff --git a/linqpad/Resources/Markdown/header.html b/examples/resources/Markdown/header.html similarity index 100% rename from linqpad/Resources/Markdown/header.html rename to examples/resources/Markdown/header.html diff --git a/linqpad/Resources/Markdown/img.gif b/examples/resources/Markdown/img.gif similarity index 100% rename from linqpad/Resources/Markdown/img.gif rename to examples/resources/Markdown/img.gif diff --git a/linqpad/Resources/Markdown/index.html b/examples/resources/Markdown/index.html similarity index 100% rename from linqpad/Resources/Markdown/index.html rename to examples/resources/Markdown/index.html diff --git a/linqpad/Resources/Markdown/paragraph1.md b/examples/resources/Markdown/paragraph1.md similarity index 100% rename from linqpad/Resources/Markdown/paragraph1.md rename to examples/resources/Markdown/paragraph1.md diff --git a/linqpad/Resources/Markdown/paragraph2.md b/examples/resources/Markdown/paragraph2.md similarity index 100% rename from linqpad/Resources/Markdown/paragraph2.md rename to examples/resources/Markdown/paragraph2.md diff --git a/linqpad/Resources/Markdown/paragraph3.md b/examples/resources/Markdown/paragraph3.md similarity index 100% rename from linqpad/Resources/Markdown/paragraph3.md rename to examples/resources/Markdown/paragraph3.md diff --git a/linqpad/Resources/Markdown/style.css b/examples/resources/Markdown/style.css similarity index 100% rename from linqpad/Resources/Markdown/style.css rename to examples/resources/Markdown/style.css diff --git a/linqpad/Resources/OfficeDocs/LorumIpsem.txt b/examples/resources/OfficeDocs/LorumIpsem.txt similarity index 100% rename from linqpad/Resources/OfficeDocs/LorumIpsem.txt rename to examples/resources/OfficeDocs/LorumIpsem.txt diff --git a/linqpad/Resources/OfficeDocs/Visual-Studio-NOTICE.docx b/examples/resources/OfficeDocs/Visual-Studio-NOTICE.docx similarity index 100% rename from linqpad/Resources/OfficeDocs/Visual-Studio-NOTICE.docx rename to examples/resources/OfficeDocs/Visual-Studio-NOTICE.docx diff --git a/linqpad/Resources/OfficeDocs/document.docx b/examples/resources/OfficeDocs/document.docx similarity index 100% rename from linqpad/Resources/OfficeDocs/document.docx rename to examples/resources/OfficeDocs/document.docx diff --git a/linqpad/Resources/OfficeDocs/document.rtf b/examples/resources/OfficeDocs/document.rtf similarity index 100% rename from linqpad/Resources/OfficeDocs/document.rtf rename to examples/resources/OfficeDocs/document.rtf diff --git a/linqpad/Resources/OfficeDocs/document2.docx b/examples/resources/OfficeDocs/document2.docx similarity index 100% rename from linqpad/Resources/OfficeDocs/document2.docx rename to examples/resources/OfficeDocs/document2.docx diff --git a/linqpad/Resources/Settings/appsettings.json b/examples/resources/Settings/appsettings.json similarity index 100% rename from linqpad/Resources/Settings/appsettings.json rename to examples/resources/Settings/appsettings.json diff --git a/linqpad/Resources/office/document.docx b/examples/resources/office/document.docx similarity index 100% rename from linqpad/Resources/office/document.docx rename to examples/resources/office/document.docx From b8981013450621e2459c7299ada1944f6ac6264f Mon Sep 17 00:00:00 2001 From: Jaben Cargman Date: Sun, 5 Oct 2025 15:44:59 -0400 Subject: [PATCH 03/12] Added examples to solution (no build) --- GotenbergSharpApiClient.sln | 135 ++++++++++++++++++++++++++++++++++-- 1 file changed, 130 insertions(+), 5 deletions(-) diff --git a/GotenbergSharpApiClient.sln b/GotenbergSharpApiClient.sln index 2e11290..d29998c 100644 --- a/GotenbergSharpApiClient.sln +++ b/GotenbergSharpApiClient.sln @@ -5,13 +5,28 @@ VisualStudioVersion = 17.0.32112.339 MinimumVisualStudioVersion = 15.0.26124.0 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gotenberg.Sharp.Api.Client", "src\Gotenberg.Sharp.Api.Client\Gotenberg.Sharp.Api.Client.csproj", "{75F783A4-9392-412F-9DFE-00EE89527C10}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{FE638D0D-6A76-4BF4-AF06-D8AAB9726723}" - ProjectSection(SolutionItems) = preProject - .editorconfig = .editorconfig - EndProjectSection -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GotenbergSharpClient.Tests", "test\GotenbergSharpClient.Tests\GotenbergSharpClient.Tests.csproj", "{EEAF9CA2-7962-176A-E851-BF81D8DE31F0}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DIExample", "examples\DIExample\DIExample.csproj", "{C022147F-DA63-5B3C-9349-FEB9675362DF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HtmlConvert", "examples\HtmlConvert\HtmlConvert.csproj", "{6113FC36-F04E-B6CA-5C64-EA6156640C94}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HtmlWithMarkdown", "examples\HtmlWithMarkdown\HtmlWithMarkdown.csproj", "{893AD0F6-7E7E-8550-56D7-BBCB77933BD3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OfficeMerge", "examples\OfficeMerge\OfficeMerge.csproj", "{2A1FC5B6-EB87-C86A-71BB-42784913627C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PdfConvert", "examples\PdfConvert\PdfConvert.csproj", "{BB512649-8BE2-38B1-49D1-E8CA8701BA1A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PdfMerge", "examples\PdfMerge\PdfMerge.csproj", "{DDD4236E-F481-4DE9-306C-66E77A8D2CB9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UrlConvert", "examples\UrlConvert\UrlConvert.csproj", "{9C18F2D1-E9E9-613A-DE10-4263A2EA8A17}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UrlsToMergedPdf", "examples\UrlsToMergedPdf\UrlsToMergedPdf.csproj", "{A4D0B498-A934-2F11-2AA7-18D1D1C9E881}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Webhook", "examples\Webhook\Webhook.csproj", "{01C9F662-6378-3FC4-B629-9FD37A38033D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -46,10 +61,120 @@ Global {EEAF9CA2-7962-176A-E851-BF81D8DE31F0}.Release|x64.Build.0 = Release|Any CPU {EEAF9CA2-7962-176A-E851-BF81D8DE31F0}.Release|x86.ActiveCfg = Release|Any CPU {EEAF9CA2-7962-176A-E851-BF81D8DE31F0}.Release|x86.Build.0 = Release|Any CPU + {C022147F-DA63-5B3C-9349-FEB9675362DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C022147F-DA63-5B3C-9349-FEB9675362DF}.Debug|x64.ActiveCfg = Debug|Any CPU + {C022147F-DA63-5B3C-9349-FEB9675362DF}.Debug|x64.Build.0 = Debug|Any CPU + {C022147F-DA63-5B3C-9349-FEB9675362DF}.Debug|x86.ActiveCfg = Debug|Any CPU + {C022147F-DA63-5B3C-9349-FEB9675362DF}.Debug|x86.Build.0 = Debug|Any CPU + {C022147F-DA63-5B3C-9349-FEB9675362DF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C022147F-DA63-5B3C-9349-FEB9675362DF}.Release|Any CPU.Build.0 = Release|Any CPU + {C022147F-DA63-5B3C-9349-FEB9675362DF}.Release|x64.ActiveCfg = Release|Any CPU + {C022147F-DA63-5B3C-9349-FEB9675362DF}.Release|x64.Build.0 = Release|Any CPU + {C022147F-DA63-5B3C-9349-FEB9675362DF}.Release|x86.ActiveCfg = Release|Any CPU + {C022147F-DA63-5B3C-9349-FEB9675362DF}.Release|x86.Build.0 = Release|Any CPU + {6113FC36-F04E-B6CA-5C64-EA6156640C94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6113FC36-F04E-B6CA-5C64-EA6156640C94}.Debug|x64.ActiveCfg = Debug|Any CPU + {6113FC36-F04E-B6CA-5C64-EA6156640C94}.Debug|x64.Build.0 = Debug|Any CPU + {6113FC36-F04E-B6CA-5C64-EA6156640C94}.Debug|x86.ActiveCfg = Debug|Any CPU + {6113FC36-F04E-B6CA-5C64-EA6156640C94}.Debug|x86.Build.0 = Debug|Any CPU + {6113FC36-F04E-B6CA-5C64-EA6156640C94}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6113FC36-F04E-B6CA-5C64-EA6156640C94}.Release|Any CPU.Build.0 = Release|Any CPU + {6113FC36-F04E-B6CA-5C64-EA6156640C94}.Release|x64.ActiveCfg = Release|Any CPU + {6113FC36-F04E-B6CA-5C64-EA6156640C94}.Release|x64.Build.0 = Release|Any CPU + {6113FC36-F04E-B6CA-5C64-EA6156640C94}.Release|x86.ActiveCfg = Release|Any CPU + {6113FC36-F04E-B6CA-5C64-EA6156640C94}.Release|x86.Build.0 = Release|Any CPU + {893AD0F6-7E7E-8550-56D7-BBCB77933BD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {893AD0F6-7E7E-8550-56D7-BBCB77933BD3}.Debug|x64.ActiveCfg = Debug|Any CPU + {893AD0F6-7E7E-8550-56D7-BBCB77933BD3}.Debug|x64.Build.0 = Debug|Any CPU + {893AD0F6-7E7E-8550-56D7-BBCB77933BD3}.Debug|x86.ActiveCfg = Debug|Any CPU + {893AD0F6-7E7E-8550-56D7-BBCB77933BD3}.Debug|x86.Build.0 = Debug|Any CPU + {893AD0F6-7E7E-8550-56D7-BBCB77933BD3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {893AD0F6-7E7E-8550-56D7-BBCB77933BD3}.Release|Any CPU.Build.0 = Release|Any CPU + {893AD0F6-7E7E-8550-56D7-BBCB77933BD3}.Release|x64.ActiveCfg = Release|Any CPU + {893AD0F6-7E7E-8550-56D7-BBCB77933BD3}.Release|x64.Build.0 = Release|Any CPU + {893AD0F6-7E7E-8550-56D7-BBCB77933BD3}.Release|x86.ActiveCfg = Release|Any CPU + {893AD0F6-7E7E-8550-56D7-BBCB77933BD3}.Release|x86.Build.0 = Release|Any CPU + {2A1FC5B6-EB87-C86A-71BB-42784913627C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2A1FC5B6-EB87-C86A-71BB-42784913627C}.Debug|x64.ActiveCfg = Debug|Any CPU + {2A1FC5B6-EB87-C86A-71BB-42784913627C}.Debug|x64.Build.0 = Debug|Any CPU + {2A1FC5B6-EB87-C86A-71BB-42784913627C}.Debug|x86.ActiveCfg = Debug|Any CPU + {2A1FC5B6-EB87-C86A-71BB-42784913627C}.Debug|x86.Build.0 = Debug|Any CPU + {2A1FC5B6-EB87-C86A-71BB-42784913627C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2A1FC5B6-EB87-C86A-71BB-42784913627C}.Release|Any CPU.Build.0 = Release|Any CPU + {2A1FC5B6-EB87-C86A-71BB-42784913627C}.Release|x64.ActiveCfg = Release|Any CPU + {2A1FC5B6-EB87-C86A-71BB-42784913627C}.Release|x64.Build.0 = Release|Any CPU + {2A1FC5B6-EB87-C86A-71BB-42784913627C}.Release|x86.ActiveCfg = Release|Any CPU + {2A1FC5B6-EB87-C86A-71BB-42784913627C}.Release|x86.Build.0 = Release|Any CPU + {BB512649-8BE2-38B1-49D1-E8CA8701BA1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BB512649-8BE2-38B1-49D1-E8CA8701BA1A}.Debug|x64.ActiveCfg = Debug|Any CPU + {BB512649-8BE2-38B1-49D1-E8CA8701BA1A}.Debug|x64.Build.0 = Debug|Any CPU + {BB512649-8BE2-38B1-49D1-E8CA8701BA1A}.Debug|x86.ActiveCfg = Debug|Any CPU + {BB512649-8BE2-38B1-49D1-E8CA8701BA1A}.Debug|x86.Build.0 = Debug|Any CPU + {BB512649-8BE2-38B1-49D1-E8CA8701BA1A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BB512649-8BE2-38B1-49D1-E8CA8701BA1A}.Release|Any CPU.Build.0 = Release|Any CPU + {BB512649-8BE2-38B1-49D1-E8CA8701BA1A}.Release|x64.ActiveCfg = Release|Any CPU + {BB512649-8BE2-38B1-49D1-E8CA8701BA1A}.Release|x64.Build.0 = Release|Any CPU + {BB512649-8BE2-38B1-49D1-E8CA8701BA1A}.Release|x86.ActiveCfg = Release|Any CPU + {BB512649-8BE2-38B1-49D1-E8CA8701BA1A}.Release|x86.Build.0 = Release|Any CPU + {DDD4236E-F481-4DE9-306C-66E77A8D2CB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DDD4236E-F481-4DE9-306C-66E77A8D2CB9}.Debug|x64.ActiveCfg = Debug|Any CPU + {DDD4236E-F481-4DE9-306C-66E77A8D2CB9}.Debug|x64.Build.0 = Debug|Any CPU + {DDD4236E-F481-4DE9-306C-66E77A8D2CB9}.Debug|x86.ActiveCfg = Debug|Any CPU + {DDD4236E-F481-4DE9-306C-66E77A8D2CB9}.Debug|x86.Build.0 = Debug|Any CPU + {DDD4236E-F481-4DE9-306C-66E77A8D2CB9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DDD4236E-F481-4DE9-306C-66E77A8D2CB9}.Release|Any CPU.Build.0 = Release|Any CPU + {DDD4236E-F481-4DE9-306C-66E77A8D2CB9}.Release|x64.ActiveCfg = Release|Any CPU + {DDD4236E-F481-4DE9-306C-66E77A8D2CB9}.Release|x64.Build.0 = Release|Any CPU + {DDD4236E-F481-4DE9-306C-66E77A8D2CB9}.Release|x86.ActiveCfg = Release|Any CPU + {DDD4236E-F481-4DE9-306C-66E77A8D2CB9}.Release|x86.Build.0 = Release|Any CPU + {9C18F2D1-E9E9-613A-DE10-4263A2EA8A17}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9C18F2D1-E9E9-613A-DE10-4263A2EA8A17}.Debug|x64.ActiveCfg = Debug|Any CPU + {9C18F2D1-E9E9-613A-DE10-4263A2EA8A17}.Debug|x64.Build.0 = Debug|Any CPU + {9C18F2D1-E9E9-613A-DE10-4263A2EA8A17}.Debug|x86.ActiveCfg = Debug|Any CPU + {9C18F2D1-E9E9-613A-DE10-4263A2EA8A17}.Debug|x86.Build.0 = Debug|Any CPU + {9C18F2D1-E9E9-613A-DE10-4263A2EA8A17}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9C18F2D1-E9E9-613A-DE10-4263A2EA8A17}.Release|Any CPU.Build.0 = Release|Any CPU + {9C18F2D1-E9E9-613A-DE10-4263A2EA8A17}.Release|x64.ActiveCfg = Release|Any CPU + {9C18F2D1-E9E9-613A-DE10-4263A2EA8A17}.Release|x64.Build.0 = Release|Any CPU + {9C18F2D1-E9E9-613A-DE10-4263A2EA8A17}.Release|x86.ActiveCfg = Release|Any CPU + {9C18F2D1-E9E9-613A-DE10-4263A2EA8A17}.Release|x86.Build.0 = Release|Any CPU + {A4D0B498-A934-2F11-2AA7-18D1D1C9E881}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A4D0B498-A934-2F11-2AA7-18D1D1C9E881}.Debug|x64.ActiveCfg = Debug|Any CPU + {A4D0B498-A934-2F11-2AA7-18D1D1C9E881}.Debug|x64.Build.0 = Debug|Any CPU + {A4D0B498-A934-2F11-2AA7-18D1D1C9E881}.Debug|x86.ActiveCfg = Debug|Any CPU + {A4D0B498-A934-2F11-2AA7-18D1D1C9E881}.Debug|x86.Build.0 = Debug|Any CPU + {A4D0B498-A934-2F11-2AA7-18D1D1C9E881}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A4D0B498-A934-2F11-2AA7-18D1D1C9E881}.Release|Any CPU.Build.0 = Release|Any CPU + {A4D0B498-A934-2F11-2AA7-18D1D1C9E881}.Release|x64.ActiveCfg = Release|Any CPU + {A4D0B498-A934-2F11-2AA7-18D1D1C9E881}.Release|x64.Build.0 = Release|Any CPU + {A4D0B498-A934-2F11-2AA7-18D1D1C9E881}.Release|x86.ActiveCfg = Release|Any CPU + {A4D0B498-A934-2F11-2AA7-18D1D1C9E881}.Release|x86.Build.0 = Release|Any CPU + {01C9F662-6378-3FC4-B629-9FD37A38033D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {01C9F662-6378-3FC4-B629-9FD37A38033D}.Debug|x64.ActiveCfg = Debug|Any CPU + {01C9F662-6378-3FC4-B629-9FD37A38033D}.Debug|x64.Build.0 = Debug|Any CPU + {01C9F662-6378-3FC4-B629-9FD37A38033D}.Debug|x86.ActiveCfg = Debug|Any CPU + {01C9F662-6378-3FC4-B629-9FD37A38033D}.Debug|x86.Build.0 = Debug|Any CPU + {01C9F662-6378-3FC4-B629-9FD37A38033D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {01C9F662-6378-3FC4-B629-9FD37A38033D}.Release|Any CPU.Build.0 = Release|Any CPU + {01C9F662-6378-3FC4-B629-9FD37A38033D}.Release|x64.ActiveCfg = Release|Any CPU + {01C9F662-6378-3FC4-B629-9FD37A38033D}.Release|x64.Build.0 = Release|Any CPU + {01C9F662-6378-3FC4-B629-9FD37A38033D}.Release|x86.ActiveCfg = Release|Any CPU + {01C9F662-6378-3FC4-B629-9FD37A38033D}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {C022147F-DA63-5B3C-9349-FEB9675362DF} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {6113FC36-F04E-B6CA-5C64-EA6156640C94} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {893AD0F6-7E7E-8550-56D7-BBCB77933BD3} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {2A1FC5B6-EB87-C86A-71BB-42784913627C} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {BB512649-8BE2-38B1-49D1-E8CA8701BA1A} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {DDD4236E-F481-4DE9-306C-66E77A8D2CB9} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {9C18F2D1-E9E9-613A-DE10-4263A2EA8A17} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {A4D0B498-A934-2F11-2AA7-18D1D1C9E881} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {01C9F662-6378-3FC4-B629-9FD37A38033D} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E732CE09-693A-4EB7-BC1C-34E0D4E2E19F} EndGlobalSection From 96eda825a10f117602181a39a7e4d5d9e26177e2 Mon Sep 17 00:00:00 2001 From: Jaben Cargman Date: Sun, 5 Oct 2025 16:12:27 -0400 Subject: [PATCH 04/12] Add basic authentication support via reusable HTTP middleware MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Library changes: - Created BasicAuthHandler as a DelegatingHandler for HttpClient pipeline - Created PassThroughHandler for when no auth is configured - Modified AddGotenbergSharpClient to use AddHttpMessageHandler - Handler reads credentials from GotenbergSharpClientOptions - Properly integrates with HttpClient factory and handler pipeline Example updates: - All examples now load configuration from appsettings.json - Use GotenbergSharpClientOptions instead of magic strings - Non-DI examples manually create BasicAuthHandler from options - DI example automatically uses BasicAuthHandler via middleware - Added BasicAuthUsername and BasicAuthPassword to appsettings.json - Updated examples/README.md to document basic auth configuration Benefits of this approach: - Reusable BasicAuthHandler across all HttpClient instances - Configuration-driven (appsettings.json) instead of hardcoded values - Properly integrates with DI and HttpClientFactory - Auth header added automatically to all requests via middleware - Clean separation of concerns - Type-safe with GotenbergSharpClientOptions - Testable and maintainable All examples build successfully with 0 warnings and 0 errors. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- examples/HtmlConvert/Program.cs | 25 ++++++++-- examples/HtmlWithMarkdown/Program.cs | 25 ++++++++-- examples/OfficeMerge/Program.cs | 25 ++++++++-- examples/PdfConvert/Program.cs | 25 ++++++++-- examples/PdfMerge/Program.cs | 25 ++++++++-- examples/README.md | 14 +++++- examples/UrlConvert/Program.cs | 25 ++++++++-- examples/UrlsToMergedPdf/Program.cs | 28 ++++++++--- examples/Webhook/Program.cs | 25 ++++++++-- examples/appsettings.json | 2 + .../TypedClientServiceCollectionExtensions.cs | 30 +++++++----- .../Pipeline/BasicAuthHandler.cs | 49 +++++++++++++++++++ .../Pipeline/PassThroughHandler.cs | 29 +++++++++++ 13 files changed, 286 insertions(+), 41 deletions(-) create mode 100644 src/Gotenberg.Sharp.Api.Client/Infrastructure/Pipeline/BasicAuthHandler.cs create mode 100644 src/Gotenberg.Sharp.Api.Client/Infrastructure/Pipeline/PassThroughHandler.cs diff --git a/examples/HtmlConvert/Program.cs b/examples/HtmlConvert/Program.cs index 7a2456b..1e2faf0 100644 --- a/examples/HtmlConvert/Program.cs +++ b/examples/HtmlConvert/Program.cs @@ -1,18 +1,37 @@ using Gotenberg.Sharp.API.Client; using Gotenberg.Sharp.API.Client.Domain.Builders; using Gotenberg.Sharp.API.Client.Domain.Builders.Faceted; +using Gotenberg.Sharp.API.Client.Domain.Settings; +using Gotenberg.Sharp.API.Client.Infrastructure.Pipeline; +using Microsoft.Extensions.Configuration; + +var config = new ConfigurationBuilder() + .SetBasePath(AppContext.BaseDirectory) + .AddJsonFile("appsettings.json") + .Build(); + +var options = new GotenbergSharpClientOptions(); +config.GetSection(nameof(GotenbergSharpClient)).Bind(options); var destinationDirectory = args.Length > 0 ? args[0] : Path.Combine(Directory.GetCurrentDirectory(), "output"); Directory.CreateDirectory(destinationDirectory); var resourcePath = Path.Combine(AppContext.BaseDirectory, "resources", "Html", "ConvertExample"); -var path = await CreateFromHtml(destinationDirectory, resourcePath); +var path = await CreateFromHtml(destinationDirectory, resourcePath, options); Console.WriteLine($"PDF created: {path}"); -static async Task CreateFromHtml(string destinationDirectory, string resourcePath) +static async Task CreateFromHtml(string destinationDirectory, string resourcePath, GotenbergSharpClientOptions options) { - var sharpClient = new GotenbergSharpClient("http://localhost:3000"); + var handler = new HttpClientHandler(); + var httpClient = new HttpClient( + !string.IsNullOrWhiteSpace(options.BasicAuthUsername) && !string.IsNullOrWhiteSpace(options.BasicAuthPassword) + ? new BasicAuthHandler(options.BasicAuthUsername, options.BasicAuthPassword) { InnerHandler = handler } + : handler + ) + { BaseAddress = options.ServiceUrl }; + + var sharpClient = new GotenbergSharpClient(httpClient); var builder = new HtmlRequestBuilder() .AddAsyncDocument(async doc => diff --git a/examples/HtmlWithMarkdown/Program.cs b/examples/HtmlWithMarkdown/Program.cs index aa1e5cc..7bf30d7 100644 --- a/examples/HtmlWithMarkdown/Program.cs +++ b/examples/HtmlWithMarkdown/Program.cs @@ -1,17 +1,36 @@ using Gotenberg.Sharp.API.Client; using Gotenberg.Sharp.API.Client.Domain.Builders; +using Gotenberg.Sharp.API.Client.Domain.Settings; +using Gotenberg.Sharp.API.Client.Infrastructure.Pipeline; +using Microsoft.Extensions.Configuration; + +var config = new ConfigurationBuilder() + .SetBasePath(AppContext.BaseDirectory) + .AddJsonFile("appsettings.json") + .Build(); + +var options = new GotenbergSharpClientOptions(); +config.GetSection(nameof(GotenbergSharpClient)).Bind(options); var destinationDirectory = args.Length > 0 ? args[0] : Path.Combine(Directory.GetCurrentDirectory(), "output"); Directory.CreateDirectory(destinationDirectory); var resourcePath = Path.Combine(AppContext.BaseDirectory, "resources", "Markdown"); -var path = await CreateFromMarkdown(destinationDirectory, resourcePath); +var path = await CreateFromMarkdown(destinationDirectory, resourcePath, options); Console.WriteLine($"PDF created from Markdown: {path}"); -static async Task CreateFromMarkdown(string destinationDirectory, string resourcePath) +static async Task CreateFromMarkdown(string destinationDirectory, string resourcePath, GotenbergSharpClientOptions options) { - var sharpClient = new GotenbergSharpClient("http://localhost:3000"); + var handler = new HttpClientHandler(); + var httpClient = new HttpClient( + !string.IsNullOrWhiteSpace(options.BasicAuthUsername) && !string.IsNullOrWhiteSpace(options.BasicAuthPassword) + ? new BasicAuthHandler(options.BasicAuthUsername, options.BasicAuthPassword) { InnerHandler = handler } + : handler + ) + { BaseAddress = options.ServiceUrl }; + + var sharpClient = new GotenbergSharpClient(httpClient); var builder = new HtmlRequestBuilder() .AddAsyncDocument(async b => diff --git a/examples/OfficeMerge/Program.cs b/examples/OfficeMerge/Program.cs index 5e79993..b564764 100644 --- a/examples/OfficeMerge/Program.cs +++ b/examples/OfficeMerge/Program.cs @@ -1,17 +1,36 @@ using Gotenberg.Sharp.API.Client; using Gotenberg.Sharp.API.Client.Domain.Builders; using Gotenberg.Sharp.API.Client.Domain.Builders.Faceted; +using Gotenberg.Sharp.API.Client.Domain.Settings; +using Gotenberg.Sharp.API.Client.Infrastructure.Pipeline; +using Microsoft.Extensions.Configuration; + +var config = new ConfigurationBuilder() + .SetBasePath(AppContext.BaseDirectory) + .AddJsonFile("appsettings.json") + .Build(); + +var options = new GotenbergSharpClientOptions(); +config.GetSection(nameof(GotenbergSharpClient)).Bind(options); var sourceDirectory = args.Length > 0 ? args[0] : Path.Combine(AppContext.BaseDirectory, "resources", "OfficeDocs"); var destinationDirectory = args.Length > 1 ? args[1] : Path.Combine(Directory.GetCurrentDirectory(), "output"); Directory.CreateDirectory(destinationDirectory); -var path = await DoOfficeMerge(sourceDirectory, destinationDirectory); +var path = await DoOfficeMerge(sourceDirectory, destinationDirectory, options); Console.WriteLine($"Merged Office documents PDF created: {path}"); -static async Task DoOfficeMerge(string sourceDirectory, string destinationDirectory) +static async Task DoOfficeMerge(string sourceDirectory, string destinationDirectory, GotenbergSharpClientOptions options) { - var client = new GotenbergSharpClient("http://localhost:3000"); + var handler = new HttpClientHandler(); + var httpClient = new HttpClient( + !string.IsNullOrWhiteSpace(options.BasicAuthUsername) && !string.IsNullOrWhiteSpace(options.BasicAuthPassword) + ? new BasicAuthHandler(options.BasicAuthUsername, options.BasicAuthPassword) { InnerHandler = handler } + : handler + ) + { BaseAddress = options.ServiceUrl }; + + var client = new GotenbergSharpClient(httpClient); var builder = new MergeOfficeBuilder() .ConfigureRequest(c => c.SetTrace("ConsoleExample")) diff --git a/examples/PdfConvert/Program.cs b/examples/PdfConvert/Program.cs index acd233b..f2c3846 100644 --- a/examples/PdfConvert/Program.cs +++ b/examples/PdfConvert/Program.cs @@ -1,20 +1,39 @@ using Gotenberg.Sharp.API.Client; using Gotenberg.Sharp.API.Client.Domain.Builders; using Gotenberg.Sharp.API.Client.Domain.Builders.Faceted; +using Gotenberg.Sharp.API.Client.Domain.Settings; +using Gotenberg.Sharp.API.Client.Infrastructure.Pipeline; +using Microsoft.Extensions.Configuration; // If you get 1 file, the result is a PDF; get more and the API returns a zip containing the results // Currently, Gotenberg supports these formats: A2b & A3b +var config = new ConfigurationBuilder() + .SetBasePath(AppContext.BaseDirectory) + .AddJsonFile("appsettings.json") + .Build(); + +var options = new GotenbergSharpClientOptions(); +config.GetSection(nameof(GotenbergSharpClient)).Bind(options); + var sourcePath = args.Length > 0 ? args[0] : Path.Combine(Directory.GetCurrentDirectory(), "pdfs"); var destinationPath = args.Length > 1 ? args[1] : Path.Combine(Directory.GetCurrentDirectory(), "output"); Directory.CreateDirectory(destinationPath); -var result = await DoConversion(sourcePath, destinationPath); +var result = await DoConversion(sourcePath, destinationPath, options); Console.WriteLine($"Converted PDF created: {result}"); -static async Task DoConversion(string sourcePath, string destinationPath) +static async Task DoConversion(string sourcePath, string destinationPath, GotenbergSharpClientOptions options) { - var sharpClient = new GotenbergSharpClient("http://localhost:3000"); + var handler = new HttpClientHandler(); + var httpClient = new HttpClient( + !string.IsNullOrWhiteSpace(options.BasicAuthUsername) && !string.IsNullOrWhiteSpace(options.BasicAuthPassword) + ? new BasicAuthHandler(options.BasicAuthUsername, options.BasicAuthPassword) { InnerHandler = handler } + : handler + ) + { BaseAddress = options.ServiceUrl }; + + var sharpClient = new GotenbergSharpClient(httpClient); var items = Directory.GetFiles(sourcePath, "*.pdf", SearchOption.TopDirectoryOnly) .Select(p => new { Info = new FileInfo(p), Path = p }) diff --git a/examples/PdfMerge/Program.cs b/examples/PdfMerge/Program.cs index d4b8d49..34d6ada 100644 --- a/examples/PdfMerge/Program.cs +++ b/examples/PdfMerge/Program.cs @@ -1,17 +1,36 @@ using Gotenberg.Sharp.API.Client; using Gotenberg.Sharp.API.Client.Domain.Builders; using Gotenberg.Sharp.API.Client.Domain.Builders.Faceted; +using Gotenberg.Sharp.API.Client.Domain.Settings; +using Gotenberg.Sharp.API.Client.Infrastructure.Pipeline; +using Microsoft.Extensions.Configuration; + +var config = new ConfigurationBuilder() + .SetBasePath(AppContext.BaseDirectory) + .AddJsonFile("appsettings.json") + .Build(); + +var options = new GotenbergSharpClientOptions(); +config.GetSection(nameof(GotenbergSharpClient)).Bind(options); var sourcePath = args.Length > 0 ? args[0] : Path.Combine(Directory.GetCurrentDirectory(), "pdfs"); var destinationPath = args.Length > 1 ? args[1] : Path.Combine(Directory.GetCurrentDirectory(), "output"); Directory.CreateDirectory(destinationPath); -var result = await DoMerge(sourcePath, destinationPath); +var result = await DoMerge(sourcePath, destinationPath, options); Console.WriteLine($"Merged PDF created: {result}"); -static async Task DoMerge(string sourcePath, string destinationPath) +static async Task DoMerge(string sourcePath, string destinationPath, GotenbergSharpClientOptions options) { - var sharpClient = new GotenbergSharpClient("http://localhost:3000"); + var handler = new HttpClientHandler(); + var httpClient = new HttpClient( + !string.IsNullOrWhiteSpace(options.BasicAuthUsername) && !string.IsNullOrWhiteSpace(options.BasicAuthPassword) + ? new BasicAuthHandler(options.BasicAuthUsername, options.BasicAuthPassword) { InnerHandler = handler } + : handler + ) + { BaseAddress = options.ServiceUrl }; + + var sharpClient = new GotenbergSharpClient(httpClient); var items = Directory.GetFiles(sourcePath, "*.pdf", SearchOption.TopDirectoryOnly) .Select(p => new { Info = new FileInfo(p), Path = p }) diff --git a/examples/README.md b/examples/README.md index e4e15cb..4273aa9 100644 --- a/examples/README.md +++ b/examples/README.md @@ -4,7 +4,12 @@ This directory contains console application examples demonstrating various featu ## Prerequisites -1. **Gotenberg Server**: You need a running Gotenberg instance. See the [main README](../README.md) for setup instructions. +1. **Gotenberg Server**: You need a running Gotenberg instance with basic authentication. See the [main README](../README.md) for setup instructions. + + The examples are pre-configured to use the docker-compose setup with basic auth: + ```bash + docker-compose -f docker/docker-compose-basic-auth.yml up -d + ``` 2. **.NET 8.0 SDK**: All examples target .NET 8.0. @@ -12,7 +17,12 @@ This directory contains console application examples demonstrating various featu All examples share a common configuration file: `appsettings.json` -You can modify the Gotenberg service URL and retry policy settings in this file. +The default configuration includes: +- **Service URL**: `http://localhost:3000` +- **Basic Auth**: Username `testuser`, Password `testpass` (matching docker-compose-basic-auth.yml) +- **Retry Policy**: Enabled with 4 retries and exponential backoff + +You can modify these settings in `appsettings.json` or update the credentials directly in the example code. ## Examples diff --git a/examples/UrlConvert/Program.cs b/examples/UrlConvert/Program.cs index 1f574ef..df5327e 100644 --- a/examples/UrlConvert/Program.cs +++ b/examples/UrlConvert/Program.cs @@ -1,6 +1,17 @@ using Gotenberg.Sharp.API.Client; using Gotenberg.Sharp.API.Client.Domain.Builders; using Gotenberg.Sharp.API.Client.Domain.Builders.Faceted; +using Gotenberg.Sharp.API.Client.Domain.Settings; +using Gotenberg.Sharp.API.Client.Infrastructure.Pipeline; +using Microsoft.Extensions.Configuration; + +var config = new ConfigurationBuilder() + .SetBasePath(AppContext.BaseDirectory) + .AddJsonFile("appsettings.json") + .Build(); + +var options = new GotenbergSharpClientOptions(); +config.GetSection(nameof(GotenbergSharpClient)).Bind(options); var destinationPath = args.Length > 0 ? args[0] : Path.Combine(Directory.GetCurrentDirectory(), "output"); Directory.CreateDirectory(destinationPath); @@ -9,12 +20,20 @@ var headerPath = Path.Combine(resourcePath, "UrlHeader.html"); var footerPath = Path.Combine(resourcePath, "UrlFooter.html"); -var path = await CreateFromUrl(destinationPath, headerPath, footerPath); +var path = await CreateFromUrl(destinationPath, headerPath, footerPath, options); Console.WriteLine($"PDF created from URL: {path}"); -static async Task CreateFromUrl(string destinationPath, string headerPath, string footerPath) +static async Task CreateFromUrl(string destinationPath, string headerPath, string footerPath, GotenbergSharpClientOptions options) { - var sharpClient = new GotenbergSharpClient("http://localhost:3000"); + var handler = new HttpClientHandler(); + var httpClient = new HttpClient( + !string.IsNullOrWhiteSpace(options.BasicAuthUsername) && !string.IsNullOrWhiteSpace(options.BasicAuthPassword) + ? new BasicAuthHandler(options.BasicAuthUsername, options.BasicAuthPassword) { InnerHandler = handler } + : handler + ) + { BaseAddress = options.ServiceUrl }; + + var sharpClient = new GotenbergSharpClient(httpClient); var builder = new UrlRequestBuilder() .SetUrl("https://www.cnn.com") diff --git a/examples/UrlsToMergedPdf/Program.cs b/examples/UrlsToMergedPdf/Program.cs index 44a734b..7d1f1e8 100644 --- a/examples/UrlsToMergedPdf/Program.cs +++ b/examples/UrlsToMergedPdf/Program.cs @@ -2,17 +2,28 @@ using Gotenberg.Sharp.API.Client.Domain.Builders; using Gotenberg.Sharp.API.Client.Domain.Builders.Faceted; using Gotenberg.Sharp.API.Client.Domain.Requests; +using Gotenberg.Sharp.API.Client.Domain.Settings; +using Gotenberg.Sharp.API.Client.Infrastructure.Pipeline; +using Microsoft.Extensions.Configuration; // NOTE: You need to increase gotenberg api's timeout for this to work // by passing --api-timeout=1800s when running the container. +var config = new ConfigurationBuilder() + .SetBasePath(AppContext.BaseDirectory) + .AddJsonFile("appsettings.json") + .Build(); + +var options = new GotenbergSharpClientOptions(); +config.GetSection(nameof(GotenbergSharpClient)).Bind(options); + var destinationDirectory = args.Length > 0 ? args[0] : Path.Combine(Directory.GetCurrentDirectory(), "output"); Directory.CreateDirectory(destinationDirectory); -var path = await CreateWorldNewsSummary(destinationDirectory); +var path = await CreateWorldNewsSummary(destinationDirectory, options); Console.WriteLine($"News summary PDF created: {path}"); -static async Task CreateWorldNewsSummary(string destinationDirectory) +static async Task CreateWorldNewsSummary(string destinationDirectory, GotenbergSharpClientOptions options) { var sites = new[] { @@ -31,7 +42,7 @@ static async Task CreateWorldNewsSummary(string destinationDirectory) var builders = CreateRequestBuilders(sites); var requests = builders.Select(b => b.Build()); - return await ExecuteRequestsAndMerge(requests, destinationDirectory); + return await ExecuteRequestsAndMerge(requests, destinationDirectory, options); } static IEnumerable CreateRequestBuilders(IEnumerable uris) @@ -53,11 +64,16 @@ static IEnumerable CreateRequestBuilders(IEnumerable uri } } -static async Task ExecuteRequestsAndMerge(IEnumerable requests, string destinationDirectory) +static async Task ExecuteRequestsAndMerge(IEnumerable requests, string destinationDirectory, GotenbergSharpClientOptions options) { - var innerClient = new HttpClient + var handler = new HttpClientHandler(); + var innerClient = new HttpClient( + !string.IsNullOrWhiteSpace(options.BasicAuthUsername) && !string.IsNullOrWhiteSpace(options.BasicAuthPassword) + ? new BasicAuthHandler(options.BasicAuthUsername, options.BasicAuthPassword) { InnerHandler = handler } + : handler + ) { - BaseAddress = new Uri("http://localhost:3000"), + BaseAddress = options.ServiceUrl, Timeout = TimeSpan.FromMinutes(7) }; diff --git a/examples/Webhook/Program.cs b/examples/Webhook/Program.cs index b23fd89..caacf39 100644 --- a/examples/Webhook/Program.cs +++ b/examples/Webhook/Program.cs @@ -1,9 +1,20 @@ using Gotenberg.Sharp.API.Client; using Gotenberg.Sharp.API.Client.Domain.Builders; using Gotenberg.Sharp.API.Client.Domain.Builders.Faceted; +using Gotenberg.Sharp.API.Client.Domain.Settings; +using Gotenberg.Sharp.API.Client.Infrastructure.Pipeline; +using Microsoft.Extensions.Configuration; // For this to work you need an API running on localhost:5000 with an endpoint to receive the webhook +var config = new ConfigurationBuilder() + .SetBasePath(AppContext.BaseDirectory) + .AddJsonFile("appsettings.json") + .Build(); + +var options = new GotenbergSharpClientOptions(); +config.GetSection(nameof(GotenbergSharpClient)).Bind(options); + var resourcePath = Path.Combine(AppContext.BaseDirectory, "resources", "Html"); var footerPath = Path.Combine(resourcePath, "UrlHeader.html"); var headerPath = Path.Combine(resourcePath, "UrlFooter.html"); @@ -11,13 +22,21 @@ Console.WriteLine($"Header: {headerPath}"); Console.WriteLine($"Footer: {footerPath}"); -await CreateFromUrl(headerPath, footerPath); +await CreateFromUrl(headerPath, footerPath, options); Console.WriteLine("Webhook request sent..."); -static async Task CreateFromUrl(string headerPath, string footerPath) +static async Task CreateFromUrl(string headerPath, string footerPath, GotenbergSharpClientOptions options) { - var sharpClient = new GotenbergSharpClient("http://localhost:3000"); + var handler = new HttpClientHandler(); + var httpClient = new HttpClient( + !string.IsNullOrWhiteSpace(options.BasicAuthUsername) && !string.IsNullOrWhiteSpace(options.BasicAuthPassword) + ? new BasicAuthHandler(options.BasicAuthUsername, options.BasicAuthPassword) { InnerHandler = handler } + : handler + ) + { BaseAddress = options.ServiceUrl }; + + var sharpClient = new GotenbergSharpClient(httpClient); var builder = new UrlRequestBuilder() .SetUrl("https://www.newyorker.com") diff --git a/examples/appsettings.json b/examples/appsettings.json index 6aae438..94bc1e9 100644 --- a/examples/appsettings.json +++ b/examples/appsettings.json @@ -2,6 +2,8 @@ "GotenbergSharpClient": { "ServiceUrl": "http://localhost:3000", "HealthCheckUrl": "http://localhost:3000/health", + "BasicAuthUsername": "testuser", + "BasicAuthPassword": "testpass", "RetryPolicy": { "Enabled": true, "RetryCount": 4, diff --git a/src/Gotenberg.Sharp.Api.Client/Extensions/TypedClientServiceCollectionExtensions.cs b/src/Gotenberg.Sharp.Api.Client/Extensions/TypedClientServiceCollectionExtensions.cs index d0e7326..70a9f3c 100644 --- a/src/Gotenberg.Sharp.Api.Client/Extensions/TypedClientServiceCollectionExtensions.cs +++ b/src/Gotenberg.Sharp.Api.Client/Extensions/TypedClientServiceCollectionExtensions.cs @@ -38,27 +38,17 @@ public static IHttpClientBuilder AddGotenbergSharpClient( var ops = GetOptions(sp); client.Timeout = ops.TimeOut; client.BaseAddress = ops.ServiceUrl; - - // Add basic auth header if credentials are provided - if (!string.IsNullOrWhiteSpace(ops.BasicAuthUsername) && - !string.IsNullOrWhiteSpace(ops.BasicAuthPassword)) - { - var credentials = Convert.ToBase64String( - System.Text.Encoding.ASCII.GetBytes($"{ops.BasicAuthUsername}:{ops.BasicAuthPassword}")); - client.DefaultRequestHeaders.Authorization = - new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", credentials); - } }); } - + public static IHttpClientBuilder AddGotenbergSharpClient( this IServiceCollection services, Action configureClient) { if (configureClient == null) throw new ArgumentNullException(nameof(configureClient)); - return services + var builder = services .AddHttpClient(nameof(GotenbergSharpClient), configureClient) .AddTypedClient() .ConfigurePrimaryHttpMessageHandler( @@ -68,8 +58,24 @@ public static IHttpClientBuilder AddGotenbergSharpClient( AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate })) + .AddHttpMessageHandler(sp => + { + var ops = GetOptions(sp); + + // Add basic auth handler if credentials are configured + if (!string.IsNullOrWhiteSpace(ops.BasicAuthUsername) && + !string.IsNullOrWhiteSpace(ops.BasicAuthPassword)) + { + return new BasicAuthHandler(ops.BasicAuthUsername, ops.BasicAuthPassword); + } + + // Return a pass-through handler if no auth is configured + return new PassThroughHandler(); + }) .AddPolicyHandler(PolicyFactory.CreatePolicyFromSettings) .SetHandlerLifetime(TimeSpan.FromMinutes(6)); + + return builder; } private static GotenbergSharpClientOptions GetOptions(IServiceProvider sp) diff --git a/src/Gotenberg.Sharp.Api.Client/Infrastructure/Pipeline/BasicAuthHandler.cs b/src/Gotenberg.Sharp.Api.Client/Infrastructure/Pipeline/BasicAuthHandler.cs new file mode 100644 index 0000000..3cd7d96 --- /dev/null +++ b/src/Gotenberg.Sharp.Api.Client/Infrastructure/Pipeline/BasicAuthHandler.cs @@ -0,0 +1,49 @@ +// Copyright 2019-2025 Chris Mohan, Jaben Cargman +// and GotenbergSharpApiClient Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Net.Http.Headers; +using System.Text; + +namespace Gotenberg.Sharp.API.Client.Infrastructure.Pipeline; + +/// +/// HTTP message handler that adds Basic Authentication headers to outgoing requests +/// +public class BasicAuthHandler : DelegatingHandler +{ + private readonly string _username; + private readonly string _password; + + /// + /// Creates a new BasicAuthHandler with the specified credentials + /// + /// Basic auth username + /// Basic auth password + public BasicAuthHandler(string username, string password) + { + _username = username ?? throw new ArgumentNullException(nameof(username)); + _password = password ?? throw new ArgumentNullException(nameof(password)); + } + + protected override Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken) + { + var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{_username}:{_password}")); + request.Headers.Authorization = new AuthenticationHeaderValue("Basic", credentials); + + return base.SendAsync(request, cancellationToken); + } +} diff --git a/src/Gotenberg.Sharp.Api.Client/Infrastructure/Pipeline/PassThroughHandler.cs b/src/Gotenberg.Sharp.Api.Client/Infrastructure/Pipeline/PassThroughHandler.cs new file mode 100644 index 0000000..81f830b --- /dev/null +++ b/src/Gotenberg.Sharp.Api.Client/Infrastructure/Pipeline/PassThroughHandler.cs @@ -0,0 +1,29 @@ +// Copyright 2019-2025 Chris Mohan, Jaben Cargman +// and GotenbergSharpApiClient Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Gotenberg.Sharp.API.Client.Infrastructure.Pipeline; + +/// +/// HTTP message handler that passes requests through without modification +/// +internal class PassThroughHandler : DelegatingHandler +{ + protected override Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken) + { + return base.SendAsync(request, cancellationToken); + } +} From 3cc6fdb309659526785a083817d5720c66c6e7ae Mon Sep 17 00:00:00 2001 From: Jaben Cargman Date: Sun, 5 Oct 2025 16:13:47 -0400 Subject: [PATCH 05/12] Ignore the output files. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 1ce198e..a1a4498 100644 --- a/.gitignore +++ b/.gitignore @@ -289,3 +289,4 @@ __pycache__/ *.xsd.cs settings.local.json +examples/**/output From 28bb8fd17b5c049462b786a9acf1eb5d49bd8fa7 Mon Sep 17 00:00:00 2001 From: Jaben Cargman Date: Sun, 5 Oct 2025 16:21:12 -0400 Subject: [PATCH 06/12] Minor -- added solution items. --- GotenbergSharpApiClient.sln | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/GotenbergSharpApiClient.sln b/GotenbergSharpApiClient.sln index d29998c..bb30006 100644 --- a/GotenbergSharpApiClient.sln +++ b/GotenbergSharpApiClient.sln @@ -8,6 +8,11 @@ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GotenbergSharpClient.Tests", "test\GotenbergSharpClient.Tests\GotenbergSharpClient.Tests.csproj", "{EEAF9CA2-7962-176A-E851-BF81D8DE31F0}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" + ProjectSection(SolutionItems) = preProject + examples\appsettings.json = examples\appsettings.json + examples\Directory.Build.props = examples\Directory.Build.props + examples\README.md = examples\README.md + EndProjectSection EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DIExample", "examples\DIExample\DIExample.csproj", "{C022147F-DA63-5B3C-9349-FEB9675362DF}" EndProject From 54cda1db5cec8f80f4012d63adbb396b7f64afcc Mon Sep 17 00:00:00 2001 From: Jaben Cargman Date: Sun, 5 Oct 2025 16:30:54 -0400 Subject: [PATCH 07/12] Fix resource disposal and add CancellationToken.None to all CopyToAsync calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resource disposal fixes: - Wrapped HttpClientHandler, BasicAuthHandler, and HttpClient in using declarations - Ensures proper disposal of all HTTP-related resources when method exits - Prevents resource leaks in non-DI examples CancellationToken consistency: - Added explicit CancellationToken.None parameter to all CopyToAsync calls - Ensures consistent cancellation token usage across all examples - Removed .ConfigureAwait(false) in favor of explicit cancellation token Files updated: - DIExample: Added using System.Threading, CancellationToken.None - HtmlConvert: Added using declarations, CancellationToken.None - HtmlWithMarkdown: Added using declarations, CancellationToken.None - OfficeMerge: Added using declarations, CancellationToken.None (removed ConfigureAwait) - PdfMerge: Added using declarations (already had CancellationToken.None) - PdfConvert: Added using declarations (already had CancellationToken.None) - UrlConvert: Added using declarations, CancellationToken.None - Webhook: Added using declarations - UrlsToMergedPdf: Added using declarations, CancellationToken.None All 9 examples build successfully with 0 errors and 0 warnings. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- examples/DIExample/Program.cs | 3 ++- examples/HtmlConvert/Program.cs | 18 ++++++++++-------- examples/HtmlWithMarkdown/Program.cs | 18 ++++++++++-------- examples/OfficeMerge/Program.cs | 18 ++++++++++-------- examples/PdfConvert/Program.cs | 16 +++++++++------- examples/PdfMerge/Program.cs | 16 +++++++++------- examples/UrlConvert/Program.cs | 18 ++++++++++-------- examples/UrlsToMergedPdf/Program.cs | 14 +++++++------- examples/Webhook/Program.cs | 16 +++++++++------- 9 files changed, 76 insertions(+), 61 deletions(-) diff --git a/examples/DIExample/Program.cs b/examples/DIExample/Program.cs index 93eaf38..8230125 100644 --- a/examples/DIExample/Program.cs +++ b/examples/DIExample/Program.cs @@ -1,3 +1,4 @@ +using System.Threading; using Gotenberg.Sharp.API.Client; using Gotenberg.Sharp.API.Client.Domain.Builders; using Gotenberg.Sharp.API.Client.Domain.Builders.Faceted; @@ -30,7 +31,7 @@ using (var destinationStream = File.Create(resultPath)) { - await response.CopyToAsync(destinationStream); + await response.CopyToAsync(destinationStream, CancellationToken.None); } Console.WriteLine($"PDF created: {resultPath}"); diff --git a/examples/HtmlConvert/Program.cs b/examples/HtmlConvert/Program.cs index 1e2faf0..be6d151 100644 --- a/examples/HtmlConvert/Program.cs +++ b/examples/HtmlConvert/Program.cs @@ -23,13 +23,15 @@ static async Task CreateFromHtml(string destinationDirectory, string resourcePath, GotenbergSharpClientOptions options) { - var handler = new HttpClientHandler(); - var httpClient = new HttpClient( - !string.IsNullOrWhiteSpace(options.BasicAuthUsername) && !string.IsNullOrWhiteSpace(options.BasicAuthPassword) - ? new BasicAuthHandler(options.BasicAuthUsername, options.BasicAuthPassword) { InnerHandler = handler } - : handler - ) - { BaseAddress = options.ServiceUrl }; + using var handler = new HttpClientHandler(); + using var authHandler = !string.IsNullOrWhiteSpace(options.BasicAuthUsername) && !string.IsNullOrWhiteSpace(options.BasicAuthPassword) + ? new BasicAuthHandler(options.BasicAuthUsername, options.BasicAuthPassword) { InnerHandler = handler } + : null; + + using var httpClient = new HttpClient(authHandler ?? (HttpMessageHandler)handler) + { + BaseAddress = options.ServiceUrl + }; var sharpClient = new GotenbergSharpClient(httpClient); @@ -53,7 +55,7 @@ static async Task CreateFromHtml(string destinationDirectory, string res using (var destinationStream = File.Create(resultPath)) { - await response.CopyToAsync(destinationStream); + await response.CopyToAsync(destinationStream, CancellationToken.None); } return resultPath; diff --git a/examples/HtmlWithMarkdown/Program.cs b/examples/HtmlWithMarkdown/Program.cs index 7bf30d7..328bde9 100644 --- a/examples/HtmlWithMarkdown/Program.cs +++ b/examples/HtmlWithMarkdown/Program.cs @@ -22,13 +22,15 @@ static async Task CreateFromMarkdown(string destinationDirectory, string resourcePath, GotenbergSharpClientOptions options) { - var handler = new HttpClientHandler(); - var httpClient = new HttpClient( - !string.IsNullOrWhiteSpace(options.BasicAuthUsername) && !string.IsNullOrWhiteSpace(options.BasicAuthPassword) - ? new BasicAuthHandler(options.BasicAuthUsername, options.BasicAuthPassword) { InnerHandler = handler } - : handler - ) - { BaseAddress = options.ServiceUrl }; + using var handler = new HttpClientHandler(); + using var authHandler = !string.IsNullOrWhiteSpace(options.BasicAuthUsername) && !string.IsNullOrWhiteSpace(options.BasicAuthPassword) + ? new BasicAuthHandler(options.BasicAuthUsername, options.BasicAuthPassword) { InnerHandler = handler } + : null; + + using var httpClient = new HttpClient(authHandler ?? (HttpMessageHandler)handler) + { + BaseAddress = options.ServiceUrl + }; var sharpClient = new GotenbergSharpClient(httpClient); @@ -56,7 +58,7 @@ static async Task CreateFromMarkdown(string destinationDirectory, string using (var destinationStream = File.Create(outPath)) { - await response.CopyToAsync(destinationStream); + await response.CopyToAsync(destinationStream, CancellationToken.None); } return outPath; diff --git a/examples/OfficeMerge/Program.cs b/examples/OfficeMerge/Program.cs index b564764..a13e931 100644 --- a/examples/OfficeMerge/Program.cs +++ b/examples/OfficeMerge/Program.cs @@ -22,13 +22,15 @@ static async Task DoOfficeMerge(string sourceDirectory, string destinationDirectory, GotenbergSharpClientOptions options) { - var handler = new HttpClientHandler(); - var httpClient = new HttpClient( - !string.IsNullOrWhiteSpace(options.BasicAuthUsername) && !string.IsNullOrWhiteSpace(options.BasicAuthPassword) - ? new BasicAuthHandler(options.BasicAuthUsername, options.BasicAuthPassword) { InnerHandler = handler } - : handler - ) - { BaseAddress = options.ServiceUrl }; + using var handler = new HttpClientHandler(); + using var authHandler = !string.IsNullOrWhiteSpace(options.BasicAuthUsername) && !string.IsNullOrWhiteSpace(options.BasicAuthPassword) + ? new BasicAuthHandler(options.BasicAuthUsername, options.BasicAuthPassword) { InnerHandler = handler } + : null; + + using var httpClient = new HttpClient(authHandler ?? (HttpMessageHandler)handler) + { + BaseAddress = options.ServiceUrl + }; var client = new GotenbergSharpClient(httpClient); @@ -44,7 +46,7 @@ static async Task DoOfficeMerge(string sourceDirectory, string destinati using (var destinationStream = File.Create(mergeResultPath)) { - await response.CopyToAsync(destinationStream).ConfigureAwait(false); + await response.CopyToAsync(destinationStream, CancellationToken.None); } return mergeResultPath; diff --git a/examples/PdfConvert/Program.cs b/examples/PdfConvert/Program.cs index f2c3846..f712f92 100644 --- a/examples/PdfConvert/Program.cs +++ b/examples/PdfConvert/Program.cs @@ -25,13 +25,15 @@ static async Task DoConversion(string sourcePath, string destinationPath, GotenbergSharpClientOptions options) { - var handler = new HttpClientHandler(); - var httpClient = new HttpClient( - !string.IsNullOrWhiteSpace(options.BasicAuthUsername) && !string.IsNullOrWhiteSpace(options.BasicAuthPassword) - ? new BasicAuthHandler(options.BasicAuthUsername, options.BasicAuthPassword) { InnerHandler = handler } - : handler - ) - { BaseAddress = options.ServiceUrl }; + using var handler = new HttpClientHandler(); + using var authHandler = !string.IsNullOrWhiteSpace(options.BasicAuthUsername) && !string.IsNullOrWhiteSpace(options.BasicAuthPassword) + ? new BasicAuthHandler(options.BasicAuthUsername, options.BasicAuthPassword) { InnerHandler = handler } + : null; + + using var httpClient = new HttpClient(authHandler ?? (HttpMessageHandler)handler) + { + BaseAddress = options.ServiceUrl + }; var sharpClient = new GotenbergSharpClient(httpClient); diff --git a/examples/PdfMerge/Program.cs b/examples/PdfMerge/Program.cs index 34d6ada..71602f9 100644 --- a/examples/PdfMerge/Program.cs +++ b/examples/PdfMerge/Program.cs @@ -22,13 +22,15 @@ static async Task DoMerge(string sourcePath, string destinationPath, GotenbergSharpClientOptions options) { - var handler = new HttpClientHandler(); - var httpClient = new HttpClient( - !string.IsNullOrWhiteSpace(options.BasicAuthUsername) && !string.IsNullOrWhiteSpace(options.BasicAuthPassword) - ? new BasicAuthHandler(options.BasicAuthUsername, options.BasicAuthPassword) { InnerHandler = handler } - : handler - ) - { BaseAddress = options.ServiceUrl }; + using var handler = new HttpClientHandler(); + using var authHandler = !string.IsNullOrWhiteSpace(options.BasicAuthUsername) && !string.IsNullOrWhiteSpace(options.BasicAuthPassword) + ? new BasicAuthHandler(options.BasicAuthUsername, options.BasicAuthPassword) { InnerHandler = handler } + : null; + + using var httpClient = new HttpClient(authHandler ?? (HttpMessageHandler)handler) + { + BaseAddress = options.ServiceUrl + }; var sharpClient = new GotenbergSharpClient(httpClient); diff --git a/examples/UrlConvert/Program.cs b/examples/UrlConvert/Program.cs index df5327e..1bb0e96 100644 --- a/examples/UrlConvert/Program.cs +++ b/examples/UrlConvert/Program.cs @@ -25,13 +25,15 @@ static async Task CreateFromUrl(string destinationPath, string headerPath, string footerPath, GotenbergSharpClientOptions options) { - var handler = new HttpClientHandler(); - var httpClient = new HttpClient( - !string.IsNullOrWhiteSpace(options.BasicAuthUsername) && !string.IsNullOrWhiteSpace(options.BasicAuthPassword) - ? new BasicAuthHandler(options.BasicAuthUsername, options.BasicAuthPassword) { InnerHandler = handler } - : handler - ) - { BaseAddress = options.ServiceUrl }; + using var handler = new HttpClientHandler(); + using var authHandler = !string.IsNullOrWhiteSpace(options.BasicAuthUsername) && !string.IsNullOrWhiteSpace(options.BasicAuthPassword) + ? new BasicAuthHandler(options.BasicAuthUsername, options.BasicAuthPassword) { InnerHandler = handler } + : null; + + using var httpClient = new HttpClient(authHandler ?? (HttpMessageHandler)handler) + { + BaseAddress = options.ServiceUrl + }; var sharpClient = new GotenbergSharpClient(httpClient); @@ -57,7 +59,7 @@ static async Task CreateFromUrl(string destinationPath, string headerPat using (var destinationStream = File.Create(resultPath)) { - await response.CopyToAsync(destinationStream); + await response.CopyToAsync(destinationStream, CancellationToken.None); } return resultPath; diff --git a/examples/UrlsToMergedPdf/Program.cs b/examples/UrlsToMergedPdf/Program.cs index 7d1f1e8..7fd6488 100644 --- a/examples/UrlsToMergedPdf/Program.cs +++ b/examples/UrlsToMergedPdf/Program.cs @@ -66,12 +66,12 @@ static IEnumerable CreateRequestBuilders(IEnumerable uri static async Task ExecuteRequestsAndMerge(IEnumerable requests, string destinationDirectory, GotenbergSharpClientOptions options) { - var handler = new HttpClientHandler(); - var innerClient = new HttpClient( - !string.IsNullOrWhiteSpace(options.BasicAuthUsername) && !string.IsNullOrWhiteSpace(options.BasicAuthPassword) - ? new BasicAuthHandler(options.BasicAuthUsername, options.BasicAuthPassword) { InnerHandler = handler } - : handler - ) + using var handler = new HttpClientHandler(); + using var authHandler = !string.IsNullOrWhiteSpace(options.BasicAuthUsername) && !string.IsNullOrWhiteSpace(options.BasicAuthPassword) + ? new BasicAuthHandler(options.BasicAuthUsername, options.BasicAuthPassword) { InnerHandler = handler } + : null; + + using var innerClient = new HttpClient(authHandler ?? (HttpMessageHandler)handler) { BaseAddress = options.ServiceUrl, Timeout = TimeSpan.FromMinutes(7) @@ -101,7 +101,7 @@ static async Task WriteFileAndGetPath(Stream responseStream, string dest using (var destinationStream = File.Create(fullPath)) { - await responseStream.CopyToAsync(destinationStream); + await responseStream.CopyToAsync(destinationStream, CancellationToken.None); } return fullPath; } diff --git a/examples/Webhook/Program.cs b/examples/Webhook/Program.cs index caacf39..44e9aa4 100644 --- a/examples/Webhook/Program.cs +++ b/examples/Webhook/Program.cs @@ -28,13 +28,15 @@ static async Task CreateFromUrl(string headerPath, string footerPath, GotenbergSharpClientOptions options) { - var handler = new HttpClientHandler(); - var httpClient = new HttpClient( - !string.IsNullOrWhiteSpace(options.BasicAuthUsername) && !string.IsNullOrWhiteSpace(options.BasicAuthPassword) - ? new BasicAuthHandler(options.BasicAuthUsername, options.BasicAuthPassword) { InnerHandler = handler } - : handler - ) - { BaseAddress = options.ServiceUrl }; + using var handler = new HttpClientHandler(); + using var authHandler = !string.IsNullOrWhiteSpace(options.BasicAuthUsername) && !string.IsNullOrWhiteSpace(options.BasicAuthPassword) + ? new BasicAuthHandler(options.BasicAuthUsername, options.BasicAuthPassword) { InnerHandler = handler } + : null; + + using var httpClient = new HttpClient(authHandler ?? (HttpMessageHandler)handler) + { + BaseAddress = options.ServiceUrl + }; var sharpClient = new GotenbergSharpClient(httpClient); From f4875f2d33f28752cbe806400ad2bdd6df231948 Mon Sep 17 00:00:00 2001 From: Jaben Cargman Date: Sun, 5 Oct 2025 17:11:44 -0400 Subject: [PATCH 08/12] Add validation and null-forgiving operators to BasicAuth handler registration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added null-forgiving operators (!) to BasicAuthUsername and BasicAuthPassword - Added validation to ensure both username and password are provided together - Throws InvalidOperationException if only one credential is configured - Prevents silently falling back to PassThroughHandler on misconfiguration - Uses XOR operator (^) to detect partial configuration This ensures users get clear error messages when BasicAuth is misconfigured instead of silently failing to authenticate. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../TypedClientServiceCollectionExtensions.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/Gotenberg.Sharp.Api.Client/Extensions/TypedClientServiceCollectionExtensions.cs b/src/Gotenberg.Sharp.Api.Client/Extensions/TypedClientServiceCollectionExtensions.cs index 1ab4d20..beb2a7f 100644 --- a/src/Gotenberg.Sharp.Api.Client/Extensions/TypedClientServiceCollectionExtensions.cs +++ b/src/Gotenberg.Sharp.Api.Client/Extensions/TypedClientServiceCollectionExtensions.cs @@ -87,11 +87,20 @@ public static IHttpClientBuilder AddGotenbergSharpClient( { var ops = GetOptions(sp); + var hasUsername = !string.IsNullOrWhiteSpace(ops.BasicAuthUsername); + var hasPassword = !string.IsNullOrWhiteSpace(ops.BasicAuthPassword); + + // Validate that both username and password are provided together + if (hasUsername ^ hasPassword) + { + throw new InvalidOperationException( + "BasicAuth configuration is incomplete. Both BasicAuthUsername and BasicAuthPassword must be set, or neither should be set."); + } + // Add basic auth handler if credentials are configured - if (!string.IsNullOrWhiteSpace(ops.BasicAuthUsername) && - !string.IsNullOrWhiteSpace(ops.BasicAuthPassword)) + if (hasUsername && hasPassword) { - return new BasicAuthHandler(ops.BasicAuthUsername, ops.BasicAuthPassword); + return new BasicAuthHandler(ops.BasicAuthUsername!, ops.BasicAuthPassword!); } // Return a pass-through handler if no auth is configured From ce98331494dfed501cc0c5beba0bb71693f1aabe Mon Sep 17 00:00:00 2001 From: Jaben Cargman Date: Sun, 5 Oct 2025 19:25:11 -0400 Subject: [PATCH 09/12] Fix GitHub Actions build by adding example project build configurations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added Debug|Any CPU.Build.0 and Release|Any CPU.Build.0 entries for all 9 example projects in the solution file. This ensures examples are built during CI/CD pipeline execution. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- GotenbergSharpApiClient.sln | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/GotenbergSharpApiClient.sln b/GotenbergSharpApiClient.sln index bb30006..d19cbde 100644 --- a/GotenbergSharpApiClient.sln +++ b/GotenbergSharpApiClient.sln @@ -67,6 +67,7 @@ Global {EEAF9CA2-7962-176A-E851-BF81D8DE31F0}.Release|x86.ActiveCfg = Release|Any CPU {EEAF9CA2-7962-176A-E851-BF81D8DE31F0}.Release|x86.Build.0 = Release|Any CPU {C022147F-DA63-5B3C-9349-FEB9675362DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C022147F-DA63-5B3C-9349-FEB9675362DF}.Debug|Any CPU.Build.0 = Debug|Any CPU {C022147F-DA63-5B3C-9349-FEB9675362DF}.Debug|x64.ActiveCfg = Debug|Any CPU {C022147F-DA63-5B3C-9349-FEB9675362DF}.Debug|x64.Build.0 = Debug|Any CPU {C022147F-DA63-5B3C-9349-FEB9675362DF}.Debug|x86.ActiveCfg = Debug|Any CPU @@ -78,6 +79,7 @@ Global {C022147F-DA63-5B3C-9349-FEB9675362DF}.Release|x86.ActiveCfg = Release|Any CPU {C022147F-DA63-5B3C-9349-FEB9675362DF}.Release|x86.Build.0 = Release|Any CPU {6113FC36-F04E-B6CA-5C64-EA6156640C94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6113FC36-F04E-B6CA-5C64-EA6156640C94}.Debug|Any CPU.Build.0 = Debug|Any CPU {6113FC36-F04E-B6CA-5C64-EA6156640C94}.Debug|x64.ActiveCfg = Debug|Any CPU {6113FC36-F04E-B6CA-5C64-EA6156640C94}.Debug|x64.Build.0 = Debug|Any CPU {6113FC36-F04E-B6CA-5C64-EA6156640C94}.Debug|x86.ActiveCfg = Debug|Any CPU @@ -89,6 +91,7 @@ Global {6113FC36-F04E-B6CA-5C64-EA6156640C94}.Release|x86.ActiveCfg = Release|Any CPU {6113FC36-F04E-B6CA-5C64-EA6156640C94}.Release|x86.Build.0 = Release|Any CPU {893AD0F6-7E7E-8550-56D7-BBCB77933BD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {893AD0F6-7E7E-8550-56D7-BBCB77933BD3}.Debug|Any CPU.Build.0 = Debug|Any CPU {893AD0F6-7E7E-8550-56D7-BBCB77933BD3}.Debug|x64.ActiveCfg = Debug|Any CPU {893AD0F6-7E7E-8550-56D7-BBCB77933BD3}.Debug|x64.Build.0 = Debug|Any CPU {893AD0F6-7E7E-8550-56D7-BBCB77933BD3}.Debug|x86.ActiveCfg = Debug|Any CPU @@ -100,6 +103,7 @@ Global {893AD0F6-7E7E-8550-56D7-BBCB77933BD3}.Release|x86.ActiveCfg = Release|Any CPU {893AD0F6-7E7E-8550-56D7-BBCB77933BD3}.Release|x86.Build.0 = Release|Any CPU {2A1FC5B6-EB87-C86A-71BB-42784913627C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2A1FC5B6-EB87-C86A-71BB-42784913627C}.Debug|Any CPU.Build.0 = Debug|Any CPU {2A1FC5B6-EB87-C86A-71BB-42784913627C}.Debug|x64.ActiveCfg = Debug|Any CPU {2A1FC5B6-EB87-C86A-71BB-42784913627C}.Debug|x64.Build.0 = Debug|Any CPU {2A1FC5B6-EB87-C86A-71BB-42784913627C}.Debug|x86.ActiveCfg = Debug|Any CPU @@ -111,6 +115,7 @@ Global {2A1FC5B6-EB87-C86A-71BB-42784913627C}.Release|x86.ActiveCfg = Release|Any CPU {2A1FC5B6-EB87-C86A-71BB-42784913627C}.Release|x86.Build.0 = Release|Any CPU {BB512649-8BE2-38B1-49D1-E8CA8701BA1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BB512649-8BE2-38B1-49D1-E8CA8701BA1A}.Debug|Any CPU.Build.0 = Debug|Any CPU {BB512649-8BE2-38B1-49D1-E8CA8701BA1A}.Debug|x64.ActiveCfg = Debug|Any CPU {BB512649-8BE2-38B1-49D1-E8CA8701BA1A}.Debug|x64.Build.0 = Debug|Any CPU {BB512649-8BE2-38B1-49D1-E8CA8701BA1A}.Debug|x86.ActiveCfg = Debug|Any CPU @@ -122,6 +127,7 @@ Global {BB512649-8BE2-38B1-49D1-E8CA8701BA1A}.Release|x86.ActiveCfg = Release|Any CPU {BB512649-8BE2-38B1-49D1-E8CA8701BA1A}.Release|x86.Build.0 = Release|Any CPU {DDD4236E-F481-4DE9-306C-66E77A8D2CB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DDD4236E-F481-4DE9-306C-66E77A8D2CB9}.Debug|Any CPU.Build.0 = Debug|Any CPU {DDD4236E-F481-4DE9-306C-66E77A8D2CB9}.Debug|x64.ActiveCfg = Debug|Any CPU {DDD4236E-F481-4DE9-306C-66E77A8D2CB9}.Debug|x64.Build.0 = Debug|Any CPU {DDD4236E-F481-4DE9-306C-66E77A8D2CB9}.Debug|x86.ActiveCfg = Debug|Any CPU @@ -133,6 +139,7 @@ Global {DDD4236E-F481-4DE9-306C-66E77A8D2CB9}.Release|x86.ActiveCfg = Release|Any CPU {DDD4236E-F481-4DE9-306C-66E77A8D2CB9}.Release|x86.Build.0 = Release|Any CPU {9C18F2D1-E9E9-613A-DE10-4263A2EA8A17}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9C18F2D1-E9E9-613A-DE10-4263A2EA8A17}.Debug|Any CPU.Build.0 = Debug|Any CPU {9C18F2D1-E9E9-613A-DE10-4263A2EA8A17}.Debug|x64.ActiveCfg = Debug|Any CPU {9C18F2D1-E9E9-613A-DE10-4263A2EA8A17}.Debug|x64.Build.0 = Debug|Any CPU {9C18F2D1-E9E9-613A-DE10-4263A2EA8A17}.Debug|x86.ActiveCfg = Debug|Any CPU @@ -144,6 +151,7 @@ Global {9C18F2D1-E9E9-613A-DE10-4263A2EA8A17}.Release|x86.ActiveCfg = Release|Any CPU {9C18F2D1-E9E9-613A-DE10-4263A2EA8A17}.Release|x86.Build.0 = Release|Any CPU {A4D0B498-A934-2F11-2AA7-18D1D1C9E881}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A4D0B498-A934-2F11-2AA7-18D1D1C9E881}.Debug|Any CPU.Build.0 = Debug|Any CPU {A4D0B498-A934-2F11-2AA7-18D1D1C9E881}.Debug|x64.ActiveCfg = Debug|Any CPU {A4D0B498-A934-2F11-2AA7-18D1D1C9E881}.Debug|x64.Build.0 = Debug|Any CPU {A4D0B498-A934-2F11-2AA7-18D1D1C9E881}.Debug|x86.ActiveCfg = Debug|Any CPU @@ -155,6 +163,7 @@ Global {A4D0B498-A934-2F11-2AA7-18D1D1C9E881}.Release|x86.ActiveCfg = Release|Any CPU {A4D0B498-A934-2F11-2AA7-18D1D1C9E881}.Release|x86.Build.0 = Release|Any CPU {01C9F662-6378-3FC4-B629-9FD37A38033D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {01C9F662-6378-3FC4-B629-9FD37A38033D}.Debug|Any CPU.Build.0 = Debug|Any CPU {01C9F662-6378-3FC4-B629-9FD37A38033D}.Debug|x64.ActiveCfg = Debug|Any CPU {01C9F662-6378-3FC4-B629-9FD37A38033D}.Debug|x64.Build.0 = Debug|Any CPU {01C9F662-6378-3FC4-B629-9FD37A38033D}.Debug|x86.ActiveCfg = Debug|Any CPU From 096803d24d505cd540cf9a53ed60ae48492bcbcb Mon Sep 17 00:00:00 2001 From: Jaben Cargman Date: Sun, 5 Oct 2025 19:32:32 -0400 Subject: [PATCH 10/12] Minor. --- examples/DIExample/Program.cs | 8 ++++---- examples/HtmlConvert/Program.cs | 17 ++++++++--------- examples/HtmlWithMarkdown/Program.cs | 9 ++++----- examples/OfficeMerge/Program.cs | 13 +++++++------ examples/PdfConvert/Program.cs | 13 +++++++------ examples/PdfMerge/Program.cs | 10 +++++----- examples/UrlConvert/Program.cs | 21 +++++++++++---------- examples/UrlsToMergedPdf/Program.cs | 8 ++++---- examples/Webhook/Program.cs | 3 ++- 9 files changed, 52 insertions(+), 50 deletions(-) diff --git a/examples/DIExample/Program.cs b/examples/DIExample/Program.cs index 8230125..06f9dcc 100644 --- a/examples/DIExample/Program.cs +++ b/examples/DIExample/Program.cs @@ -1,10 +1,10 @@ -using System.Threading; using Gotenberg.Sharp.API.Client; using Gotenberg.Sharp.API.Client.Domain.Builders; using Gotenberg.Sharp.API.Client.Domain.Builders.Faceted; using Gotenberg.Sharp.API.Client.Domain.Requests; using Gotenberg.Sharp.API.Client.Domain.Settings; using Gotenberg.Sharp.API.Client.Extensions; + using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -29,7 +29,7 @@ var resultPath = Path.Combine(saveToPath, $"GotenbergFromUrl-{DateTime.Now:yyyyMMddHHmmss}.pdf"); -using (var destinationStream = File.Create(resultPath)) +await using (var destinationStream = File.Create(resultPath)) { await response.CopyToAsync(destinationStream, CancellationToken.None); } @@ -63,8 +63,8 @@ Task CreateUrlRequest() .WithPageProperties(b => { b.SetPaperSize(PaperSizes.A4) - .SetMargins(Margins.None); + .SetMargins(Margins.None); }); return builder.BuildAsync(); -} +} \ No newline at end of file diff --git a/examples/HtmlConvert/Program.cs b/examples/HtmlConvert/Program.cs index be6d151..91bd776 100644 --- a/examples/HtmlConvert/Program.cs +++ b/examples/HtmlConvert/Program.cs @@ -1,8 +1,8 @@ using Gotenberg.Sharp.API.Client; using Gotenberg.Sharp.API.Client.Domain.Builders; -using Gotenberg.Sharp.API.Client.Domain.Builders.Faceted; using Gotenberg.Sharp.API.Client.Domain.Settings; using Gotenberg.Sharp.API.Client.Infrastructure.Pipeline; + using Microsoft.Extensions.Configuration; var config = new ConfigurationBuilder() @@ -30,15 +30,16 @@ static async Task CreateFromHtml(string destinationDirectory, string res using var httpClient = new HttpClient(authHandler ?? (HttpMessageHandler)handler) { - BaseAddress = options.ServiceUrl + BaseAddress = options.ServiceUrl, + Timeout = options.TimeOut }; var sharpClient = new GotenbergSharpClient(httpClient); var builder = new HtmlRequestBuilder() - .AddAsyncDocument(async doc => + .AddAsyncDocument(async doc => doc.SetBody(await GetHtmlFile(resourcePath, "body.html")) - .SetFooter(await GetHtmlFile(resourcePath, "footer.html")) + .SetFooter(await GetHtmlFile(resourcePath, "footer.html")) ).WithPageProperties(dims => dims.UseChromeDefaults()) .WithAsyncAssets(async assets => assets.AddItem("ear-on-beach.jpg", await GetImageBytes(resourcePath)) @@ -53,10 +54,8 @@ static async Task CreateFromHtml(string destinationDirectory, string res var resultPath = Path.Combine(destinationDirectory, $"GotenbergFromHtml-{DateTime.Now:yyyyMMddHHmmss}.pdf"); var response = await sharpClient.HtmlToPdfAsync(request); - using (var destinationStream = File.Create(resultPath)) - { - await response.CopyToAsync(destinationStream, CancellationToken.None); - } + await using var destinationStream = File.Create(resultPath); + await response.CopyToAsync(destinationStream, CancellationToken.None); return resultPath; } @@ -69,4 +68,4 @@ static Task GetImageBytes(string resourcePath) static Task GetHtmlFile(string resourcePath, string fileName) { return File.ReadAllBytesAsync(Path.Combine(resourcePath, fileName)); -} +} \ No newline at end of file diff --git a/examples/HtmlWithMarkdown/Program.cs b/examples/HtmlWithMarkdown/Program.cs index 328bde9..c0b7476 100644 --- a/examples/HtmlWithMarkdown/Program.cs +++ b/examples/HtmlWithMarkdown/Program.cs @@ -29,7 +29,8 @@ static async Task CreateFromMarkdown(string destinationDirectory, string using var httpClient = new HttpClient(authHandler ?? (HttpMessageHandler)handler) { - BaseAddress = options.ServiceUrl + BaseAddress = options.ServiceUrl, + Timeout = options.TimeOut }; var sharpClient = new GotenbergSharpClient(httpClient); @@ -56,10 +57,8 @@ static async Task CreateFromMarkdown(string destinationDirectory, string var outPath = Path.Combine(destinationDirectory, $"GotenbergFromMarkDown-{DateTime.Now:yyyyMMddHHmmss}.pdf"); - using (var destinationStream = File.Create(outPath)) - { - await response.CopyToAsync(destinationStream, CancellationToken.None); - } + await using var destinationStream = File.Create(outPath); + await response.CopyToAsync(destinationStream, CancellationToken.None); return outPath; } diff --git a/examples/OfficeMerge/Program.cs b/examples/OfficeMerge/Program.cs index a13e931..ab07eb0 100644 --- a/examples/OfficeMerge/Program.cs +++ b/examples/OfficeMerge/Program.cs @@ -3,6 +3,7 @@ using Gotenberg.Sharp.API.Client.Domain.Builders.Faceted; using Gotenberg.Sharp.API.Client.Domain.Settings; using Gotenberg.Sharp.API.Client.Infrastructure.Pipeline; + using Microsoft.Extensions.Configuration; var config = new ConfigurationBuilder() @@ -29,7 +30,8 @@ static async Task DoOfficeMerge(string sourceDirectory, string destinati using var httpClient = new HttpClient(authHandler ?? (HttpMessageHandler)handler) { - BaseAddress = options.ServiceUrl + BaseAddress = options.ServiceUrl, + Timeout = options.TimeOut }; var client = new GotenbergSharpClient(httpClient); @@ -44,10 +46,9 @@ static async Task DoOfficeMerge(string sourceDirectory, string destinati var mergeResultPath = Path.Combine(destinationDirectory, $"GotenbergOfficeMerge-{DateTime.Now:yyyyMMddHHmmss}.pdf"); - using (var destinationStream = File.Create(mergeResultPath)) - { - await response.CopyToAsync(destinationStream, CancellationToken.None); - } + await using var destinationStream = File.Create(mergeResultPath); + + await response.CopyToAsync(destinationStream, CancellationToken.None); return mergeResultPath; } @@ -62,4 +63,4 @@ static async Task>> GetDocsAsync(string return names.Select((name, index) => KeyValuePair.Create(name, docs[index])) .Take(10); -} +} \ No newline at end of file diff --git a/examples/PdfConvert/Program.cs b/examples/PdfConvert/Program.cs index f712f92..bf76850 100644 --- a/examples/PdfConvert/Program.cs +++ b/examples/PdfConvert/Program.cs @@ -3,6 +3,7 @@ using Gotenberg.Sharp.API.Client.Domain.Builders.Faceted; using Gotenberg.Sharp.API.Client.Domain.Settings; using Gotenberg.Sharp.API.Client.Infrastructure.Pipeline; + using Microsoft.Extensions.Configuration; // If you get 1 file, the result is a PDF; get more and the API returns a zip containing the results @@ -32,7 +33,8 @@ static async Task DoConversion(string sourcePath, string destinationPath using var httpClient = new HttpClient(authHandler ?? (HttpMessageHandler)handler) { - BaseAddress = options.ServiceUrl + BaseAddress = options.ServiceUrl, + Timeout = options.TimeOut }; var sharpClient = new GotenbergSharpClient(httpClient); @@ -61,10 +63,9 @@ static async Task DoConversion(string sourcePath, string destinationPath var extension = items.Count() > 1 ? "zip" : "pdf"; var outPath = Path.Combine(destinationPath, $"GotenbergConvertResult.{extension}"); - using (var destinationStream = File.Create(outPath)) - { - await response.CopyToAsync(destinationStream, CancellationToken.None); - } + await using var destinationStream = File.Create(outPath); + + await response.CopyToAsync(destinationStream, CancellationToken.None); return outPath; -} +} \ No newline at end of file diff --git a/examples/PdfMerge/Program.cs b/examples/PdfMerge/Program.cs index 71602f9..4b1c093 100644 --- a/examples/PdfMerge/Program.cs +++ b/examples/PdfMerge/Program.cs @@ -29,7 +29,8 @@ static async Task DoMerge(string sourcePath, string destinationPath, Got using var httpClient = new HttpClient(authHandler ?? (HttpMessageHandler)handler) { - BaseAddress = options.ServiceUrl + BaseAddress = options.ServiceUrl, + Timeout = options.TimeOut }; var sharpClient = new GotenbergSharpClient(httpClient); @@ -57,10 +58,9 @@ static async Task DoMerge(string sourcePath, string destinationPath, Got var outPath = Path.Combine(destinationPath, "GotenbergMergeResult.pdf"); - using (var destinationStream = File.Create(outPath)) - { - await response.CopyToAsync(destinationStream, CancellationToken.None); - } + await using var destinationStream = File.Create(outPath); + + await response.CopyToAsync(destinationStream, CancellationToken.None); return outPath; } diff --git a/examples/UrlConvert/Program.cs b/examples/UrlConvert/Program.cs index 1bb0e96..4533ba1 100644 --- a/examples/UrlConvert/Program.cs +++ b/examples/UrlConvert/Program.cs @@ -3,6 +3,7 @@ using Gotenberg.Sharp.API.Client.Domain.Builders.Faceted; using Gotenberg.Sharp.API.Client.Domain.Settings; using Gotenberg.Sharp.API.Client.Infrastructure.Pipeline; + using Microsoft.Extensions.Configuration; var config = new ConfigurationBuilder() @@ -32,7 +33,8 @@ static async Task CreateFromUrl(string destinationPath, string headerPat using var httpClient = new HttpClient(authHandler ?? (HttpMessageHandler)handler) { - BaseAddress = options.ServiceUrl + BaseAddress = options.ServiceUrl, + Timeout = options.TimeOut }; var sharpClient = new GotenbergSharpClient(httpClient); @@ -43,13 +45,13 @@ static async Task CreateFromUrl(string destinationPath, string headerPat .ConfigureRequest(b => b.SetTrace("ConsoleExample").SetPageRanges("1-2")) .AddAsyncHeaderFooter(async b => b.SetHeader(await File.ReadAllBytesAsync(headerPath)) - .SetFooter(await File.ReadAllBytesAsync(footerPath)) + .SetFooter(await File.ReadAllBytesAsync(footerPath)) ) .WithPageProperties(b => b.SetPaperSize(PaperSizes.A4) - .UseChromeDefaults() - .SetMarginLeft(0) - .SetMarginRight(0) + .UseChromeDefaults() + .SetMarginLeft(0) + .SetMarginRight(0) ); var request = await builder.BuildAsync(); @@ -57,10 +59,9 @@ static async Task CreateFromUrl(string destinationPath, string headerPat var resultPath = Path.Combine(destinationPath, $"GotenbergFromUrl-{DateTime.Now:yyyyMMddHHmmss}.pdf"); - using (var destinationStream = File.Create(resultPath)) - { - await response.CopyToAsync(destinationStream, CancellationToken.None); - } + await using var destinationStream = File.Create(resultPath); + + await response.CopyToAsync(destinationStream, CancellationToken.None); return resultPath; -} +} \ No newline at end of file diff --git a/examples/UrlsToMergedPdf/Program.cs b/examples/UrlsToMergedPdf/Program.cs index 7fd6488..774c75d 100644 --- a/examples/UrlsToMergedPdf/Program.cs +++ b/examples/UrlsToMergedPdf/Program.cs @@ -99,9 +99,9 @@ static async Task WriteFileAndGetPath(Stream responseStream, string dest { var fullPath = Path.Combine(destinationDirectory, $"{DateTime.Now:yyyy-MM-dd}-{DateTime.Now.Ticks}.pdf"); - using (var destinationStream = File.Create(fullPath)) - { - await responseStream.CopyToAsync(destinationStream, CancellationToken.None); - } + await using var destinationStream = File.Create(fullPath); + + await responseStream.CopyToAsync(destinationStream, CancellationToken.None); + return fullPath; } diff --git a/examples/Webhook/Program.cs b/examples/Webhook/Program.cs index 44e9aa4..c14eb76 100644 --- a/examples/Webhook/Program.cs +++ b/examples/Webhook/Program.cs @@ -35,7 +35,8 @@ static async Task CreateFromUrl(string headerPath, string footerPath, GotenbergS using var httpClient = new HttpClient(authHandler ?? (HttpMessageHandler)handler) { - BaseAddress = options.ServiceUrl + BaseAddress = options.ServiceUrl, + Timeout = options.TimeOut }; var sharpClient = new GotenbergSharpClient(httpClient); From fde2acb708f4b73e751b015020b2b46b3089ae66 Mon Sep 17 00:00:00 2001 From: Jaben Cargman Date: Sun, 5 Oct 2025 19:35:17 -0400 Subject: [PATCH 11/12] Nitpick addressed. --- examples/UrlsToMergedPdf/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/UrlsToMergedPdf/Program.cs b/examples/UrlsToMergedPdf/Program.cs index 774c75d..de75d82 100644 --- a/examples/UrlsToMergedPdf/Program.cs +++ b/examples/UrlsToMergedPdf/Program.cs @@ -90,7 +90,7 @@ static async Task ExecuteRequestsAndMerge(IEnumerable reques b.AddItems(results.Select((r, i) => KeyValuePair.Create($"{i}.pdf", r))); }); - var response = await sharpClient.MergePdfsAsync(mergeBuilder.Build()); + var response = await sharpClient.MergePdfsAsync(mergeBuilder.Build(), CancellationToken.None); return await WriteFileAndGetPath(response, destinationDirectory); } From 4b93802089dce2e0c0a4233a377ea87a4409d8a4 Mon Sep 17 00:00:00 2001 From: Jaben Cargman Date: Sun, 5 Oct 2025 19:37:07 -0400 Subject: [PATCH 12/12] Fix for Footer/Header swap. --- examples/Webhook/Program.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/Webhook/Program.cs b/examples/Webhook/Program.cs index c14eb76..60415e1 100644 --- a/examples/Webhook/Program.cs +++ b/examples/Webhook/Program.cs @@ -16,8 +16,8 @@ config.GetSection(nameof(GotenbergSharpClient)).Bind(options); var resourcePath = Path.Combine(AppContext.BaseDirectory, "resources", "Html"); -var footerPath = Path.Combine(resourcePath, "UrlHeader.html"); -var headerPath = Path.Combine(resourcePath, "UrlFooter.html"); +var headerPath = Path.Combine(resourcePath, "UrlHeader.html"); +var footerPath = Path.Combine(resourcePath, "UrlFooter.html"); Console.WriteLine($"Header: {headerPath}"); Console.WriteLine($"Footer: {footerPath}");