Generating PDFs from HTML is one of those .NET tasks that looks solved until you're the one shipping it. Invoices, order confirmations, reports — the HTML already exists; the question is which rendering engine turns it into a faithful PDF, and what that engine costs you to operate.
Way 1 — wkhtmltopdf wrappers (DinkToPdf and friends)
The old workhorse: a WebKit-based CLI with .NET wrappers.
var converter = new SynchronizedConverter(new PdfTools());
var doc = new HtmlToPdfDocument {
GlobalSettings = { PaperSize = PaperKind.A4 },
Objects = { new ObjectSettings { HtmlContent = html } }
};
byte[] pdf = converter.Convert(doc);
Upside: free, offline, battle-tested. Downside: the underlying engine froze years ago — its WebKit predates flexbox and grid as you know them, so modern CSS silently breaks. Native-library deployment on Linux containers is its own small hobby. Choose it only for simple, table-based layouts you control completely.
Way 2 — Headless Chromium (PuppeteerSharp / Playwright for .NET)
Run a real browser and print:
await using var browser = await Puppeteer.LaunchAsync(new LaunchOptions { Headless = true });
await using var page = await browser.NewPageAsync();
await page.SetContentAsync(html, new NavigationOptions { WaitUntil = new[] { WaitUntilNavigation.Networkidle0 } });
byte[] pdf = await page.PdfAsync(new PdfOptions { Format = PaperFormat.A4, PrintBackground = true });
Upside: perfect modern-CSS fidelity — it is Chrome. Downside: you now operate a browser fleet: ~300–500 MB memory per instance, zombie processes to reap, Chromium security patches to track, and container images that balloon. Fine for low volume on infrastructure you already babysit.
Way 3 — REST API
Move the browser to someone else's fleet:
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key", apiKey);
var payload = JsonSerializer.Serialize(new {
html = "<h1>Invoice #1042</h1>",
options = new { format = "A4", printBackground = true }
});
var resp = await client.PostAsync(
"https://www.ashdocs.com/api/v1/tools/html-to-pdf",
new StringContent(payload, Encoding.UTF8, "application/json"));
byte[] pdf = await resp.Content.ReadAsByteArrayAsync();
Upside: Chromium fidelity with zero Chromium ops; scales without capacity planning; works identically from an Azure Function, a container, or a background worker. Downside: per-conversion cost and a network dependency — mitigate the latter with retries and an idempotency key.
Picking one
Static, simple markup and zero budget → wkhtmltopdf. Modern CSS, low volume, existing browser ops → PuppeteerSharp. Modern CSS at production volume, or a team that would rather not patch Chromium → the API. A useful tiebreak: if PDF generation is your product, own the fleet; if it's a feature, rent it.
FAQ
Why does my C# HTML to PDF output look different from the browser? Almost always the rendering engine: wkhtmltopdf's WebKit predates modern flexbox/grid. Rendering through actual Chromium (locally or via API) makes output match Chrome's print preview.
Can I generate PDFs in Azure Functions? Headless Chromium inside Functions is possible but fights cold starts and memory caps. An HTTP call to a conversion API sidesteps both.
How do I keep backgrounds and colors?
Set printBackground: true (API/Puppeteer) — the print pipeline strips backgrounds by default.
Is there a free way to test? The free tier includes 100 credits monthly, and sandbox keys render without consuming credits.
First PDF in five minutes — get a free API key.