Creating PDFs programmatically in .NET is essential for modern applications. Every business system eventually needs PDF generation — invoices for accounting integration, receipts for customer records, reports for executive dashboards, contracts for document management, shipping labels for logistics. The challenge is choosing an approach that's both maintainable and legally compliant.
I've tried every major .NET PDF library over fifteen years of building document systems. The landscape has changed dramatically. Early projects used iTextSharp because it was the only mature option. Then licensing changed to AGPL (copyleft), making it legally risky for commercial applications unless you purchased expensive licenses. I've seen companies get compliance letters demanding license fees for iTextSharp usage they didn't realize violated AGPL terms.
wkhtmltopdf became popular because it finally supported HTML-to-PDF conversion — write HTML templates instead of drawing PDF primitives in code. The problem is the project is abandoned, based on a WebKit snapshot from 2015, and incompatible with modern CSS. I'm actively migrating systems away from wkhtmltopdf because it's a security liability and breaks with current web development practices.
PDFSharp is open-source and license-friendly (MIT), but it doesn't handle HTML. You draw text, rectangles, and images at specific coordinates. This works for simple forms but becomes unmaintainable for complex documents. I've inherited PDFSharp invoice generators where changing the layout required recalculating dozens of coordinate positions throughout 500+ lines of drawing code.
IronPDF emerged as the solution that balances ease-of-use, licensing clarity, and modern web standards support. It uses Chromium's rendering engine, so HTML/CSS/JavaScript that works in Chrome renders identically in PDFs. The licensing is straightforward commercial licensing with no AGPL gotchas. The API is .NET-native with no external dependencies or subprocess management.
I migrated an invoice system from iTextSharp to IronPDF and reduced code size from 800 lines to 120 lines while improving PDF visual quality. The HTML template approach meant designers could modify invoice layouts without touching code. Stakeholder requested changes went from requiring developer sprints to simple template edits.
Understanding the different approaches to PDF creation helps you choose appropriately. HTML-to-PDF conversion is best for documents with complex layouts — use standard HTML/CSS skills everyone has. Programmatic construction is better for simple, repetitive documents where you're assembling data into fixed templates — labels, barcodes, forms. Most business documents benefit from HTML conversion.
using IronPdf;
// Install via NuGet: Install-Package IronPdf
var renderer = new [[ChromePdfRenderer](https://ironpdf.com/blog/videos/how-to-render-an-html-file-to-pdf-in-csharp-ironpdf/)](https://ironpdf.com/blog/videos/how-to-render-html-string-to-pdf-in-csharp-ironpdf/)();
var html = @"
<html>
<head>
<style>
body { font-family: Arial; margin: 40px; }
h1 { color: #333; }
</style>
</head>
<body>
<h1>Invoice #12345</h1>
<p>Customer: Acme Corp</p>
<p>Total: $1,234.56</p>
</body>
</html>
";
var pdf = renderer.RenderHtmlAsPdf(html);
pdf.SaveAs("invoice.pdf");
That's the fundamental pattern — create HTML with embedded styles, render to PDF, save the result. For production systems, you'd load HTML templates from files, substitute placeholders with data, then render. This separates document design (HTML) from data assembly (C#).
Why Don't .NET Frameworks Include Built-In PDF Support?
Unlike some frameworks that bundle PDF libraries, Microsoft deliberately excluded PDF functionality from .NET Framework, .NET Core, and modern .NET. This design philosophy keeps the framework lean, letting developers choose libraries suited to their specific needs rather than forcing one implementation on everyone.
The practical impact is that every .NET application needing PDF output must integrate a third-party library. This creates choice paralysis — dozens of PDF libraries exist with different capabilities, licensing models, and API designs. Choosing incorrectly means technical debt that's expensive to fix later.
I've made poor library choices that cost months of migration effort. Using iTextSharp before understanding AGPL implications meant either purchasing expensive licenses or rewriting all PDF code. Using wkhtmltopdf meant eventual migration when security audits flagged the unmaintained dependency. These mistakes taught me to evaluate licensing, maintenance, and long-term viability upfront.
The key evaluation criteria I use now are: licensing clarity (commercial-friendly terms without AGPL), active maintenance (regular updates, security patches), modern standards support (current HTML/CSS if using HTML conversion), .NET integration quality (native APIs, not subprocess wrappers), and performance characteristics (throughput for batch processing).
What Are the Main Approaches to Creating PDFs in C#?
PDF creation divides into two fundamental approaches: programmatic construction and HTML conversion. Each suits different use cases.
Programmatic construction means using PDF library APIs to create document objects, add text blocks, draw shapes, insert images, and manage layout through code. Libraries like PDFSharp and iTextSharp's core functionality work this way.
I use programmatic construction for simple, highly structured documents — shipping labels with fixed layouts, barcodes that embed data, forms where every element's position is precisely specified. The code explicitly controls every aspect:
// PDFSharp example (not IronPDF)
var document = new PdfDocument();
var page = document.AddPage();
var graphics = XGraphics.FromPdfPage(page);
var font = new XFont("Arial", 12);
graphics.DrawString("Invoice #12345", font, XBrushes.Black,
new XPoint(100, 100));
graphics.DrawString("Total: $1,234.56", font, XBrushes.Black,
new XPoint(100, 120));
document.Save("invoice.pdf");
This draws text at specific coordinates. Changing the layout means recalculating positions. For complex documents with tables, multiple columns, or responsive layouts, the code becomes unmaintainable.
HTML conversion renders HTML/CSS as PDFs using browser engines. This leverages existing web development skills. Designers create document templates as HTML, developers populate them with data, the library renders PDFs. Libraries like IronPDF, wkhtmltopdf (deprecated), and Puppeteer work this way.
I use HTML conversion for all complex business documents — invoices with line item tables, reports with charts and graphics, contracts with multi-column layouts, letters with letterhead branding. The HTML template approach separates concerns:
// IronPDF HTML approach
var template = File.ReadAllText("invoice-template.html");
var html = template
.Replace("{{INVOICE_NUMBER}}", "12345")
.Replace("{{CUSTOMER}}", "Acme Corp")
.Replace("{{TOTAL}}", "$1,234.56");
var renderer = new ChromePdfRenderer();
var pdf = renderer.RenderHtmlAsPdf(html);
The template is standard HTML. Designers edit it without touching C#. The C# code substitutes data. The library handles rendering. This separation makes systems maintainable.
How Do I Install IronPDF for PDF Creation?
IronPDF installs via NuGet Package Manager. In Visual Studio, use the Package Manager Console:
Install-Package IronPdf
Or using the .NET CLI:
dotnet add package IronPdf
The package includes the Chromium rendering engine, so there are no additional runtime dependencies. This simplifies deployment — the NuGet package contains everything needed.
For .NET Framework 4.6.2+, .NET Core 3.1+, .NET 5+, .NET 6+, .NET 8+, .NET 9+, and .NET 10+, the same package works across all versions. IronPDF targets .NET Standard, ensuring forward compatibility.
After installation, verify it works:
using IronPdf;
var renderer = new ChromePdfRenderer();
var pdf = renderer.RenderHtmlAsPdf("<h1>Hello PDF</h1>");
pdf.SaveAs("test.pdf");
This creates a minimal PDF with "Hello PDF" as a heading. If the file generates without errors, IronPDF is installed correctly.
What's the Best Way to Create PDFs from HTML Templates?
Template-based PDF generation separates document design from data assembly. HTML templates live in files that designers edit. C# code loads templates, substitutes data, renders PDFs. This workflow is maintainable and scales to complex documents.
Create an HTML template (invoice-template.html):
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: Arial, sans-serif;
margin: 40px;
}
.header {
font-size: 24px;
font-weight: bold;
margin-bottom: 20px;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
text-align: left;
padding: 8px;
border-bottom: 1px solid #ddd;
}
.total {
text-align: right;
font-size: 18px;
font-weight: bold;
margin-top: 20px;
}
</style>
</head>
<body>
<div class="header">Invoice #{{INVOICE_NUMBER}}</div>
<p><strong>Customer:</strong> {{CUSTOMER_NAME}}</p>
<p><strong>Date:</strong> {{INVOICE_DATE}}</p>
<table>
<thead>
<tr>
<th>Description</th>
<th>Quantity</th>
<th>Price</th>
<th>Total</th>
</tr>
</thead>
<tbody>
{{LINE_ITEMS}}
</tbody>
</table>
<div class="total">Total: {{TOTAL_AMOUNT}}</div>
</body>
</html>
Generate PDFs from the template:
var template = File.ReadAllText("invoice-template.html");
var lineItems = new StringBuilder();
foreach (var item in invoice.Items)
{
lineItems.AppendLine($@"
<tr>
<td>{item.Description}</td>
<td>{item.Quantity}</td>
<td>{item.Price:C}</td>
<td>{(item.Quantity * item.Price):C}</td>
</tr>
");
}
var html = template
.Replace("{{INVOICE_NUMBER}}", invoice.Number)
.Replace("{{CUSTOMER_NAME}}", invoice.CustomerName)
.Replace("{{INVOICE_DATE}}", invoice.Date.ToShortDateString())
.Replace("{{LINE_ITEMS}}", lineItems.ToString())
.Replace("{{TOTAL_AMOUNT}}", invoice.Total.ToString("C"));
var renderer = new ChromePdfRenderer();
var pdf = renderer.RenderHtmlAsPdf(html);
pdf.SaveAs($"invoice-{invoice.Number}.pdf");
This loads the template, substitutes data (invoice number, customer, line items, total), and renders the PDF. Template changes happen in HTML without code changes. Adding company logo? Edit HTML. Changing colors? Edit CSS. Restructuring layout? Edit HTML.
For complex templates with loops and conditionals, use a templating engine like Handlebars.NET or Razor instead of string replacement:
// Using Handlebars.NET
var handlebars = Handlebars.Create();
var template = handlebars.Compile(File.ReadAllText("invoice-template.html"));
var data = new
{
InvoiceNumber = "12345",
CustomerName = "Acme Corp",
InvoiceDate = DateTime.Now.ToShortDateString(),
Items = invoice.Items,
TotalAmount = invoice.Total.ToString("C")
};
var html = template(data);
var pdf = renderer.RenderHtmlAsPdf(html);
Handlebars templates support {{#each}} loops, {{#if}} conditionals, and helpers for formatting. This is cleaner than building HTML in C# with string concatenation.
How Do I Add Headers, Footers, and Page Numbers?
Headers and footers add consistent branding, page numbers, and document metadata across all pages. IronPDF generates them from HTML with support for dynamic placeholders.
Add headers and footers:
var renderer = new ChromePdfRenderer();
renderer.RenderingOptions.HtmlHeader = new IronPdf.Rendering.HtmlHeaderFooter
{
HtmlFragment = @"
<div style='text-align: center; font-size: 10px; color: #666;'>
Company Confidential - Invoice
</div>
",
DrawDividerLine = true
};
renderer.RenderingOptions.HtmlFooter = new IronPdf.Rendering.HtmlHeaderFooter
{
HtmlFragment = @"
<div style='text-align: center; font-size: 10px;'>
Page {page} of {total-pages} | Generated: {date:yyyy-MM-dd}
</div>
"
};
var pdf = renderer.RenderHtmlAsPdf(html);
The {page} and {total-pages} placeholders are replaced automatically. The {date} placeholder supports .NET date format strings. The DrawDividerLine option adds a horizontal rule separating header from content.
For complex headers with logos and multiple elements:
renderer.RenderingOptions.HtmlHeader = new IronPdf.Rendering.HtmlHeaderFooter
{
HtmlFragment = @"
<div style='display: flex; justify-content: space-between; align-items: center; padding: 0 20px;'>
<img src='data:image/png;base64,{LOGO_BASE64}' style='height: 40px;' />
<div style='text-align: right; font-size: 10px;'>
<div>Acme Corporation</div>
<div>123 Business St, Suite 100</div>
</div>
</div>
"
};
This creates a two-column header with logo on the left and company info on the right using Flexbox. Full CSS works in headers and footers, allowing complex designs.
Can I Control Page Size, Margins, and Orientation?
Yes, rendering options control PDF physical dimensions and layout.
var renderer = new ChromePdfRenderer();
renderer.RenderingOptions.PaperSize = IronPdf.Rendering.PdfPaperSize.A4;
renderer.RenderingOptions.PaperOrientation = IronPdf.Rendering.PdfPaperOrientation.Portrait;
renderer.RenderingOptions.MarginTop = 40; // Millimeters
renderer.RenderingOptions.MarginBottom = 40;
renderer.RenderingOptions.MarginLeft = 20;
renderer.RenderingOptions.MarginRight = 20;
var pdf = renderer.RenderHtmlAsPdf(html);
Paper sizes include Letter (US standard), A4 (international standard), Legal, and custom dimensions. I use Letter for US-focused documents, A4 for international distribution. Orientation is Portrait (tall) or Landscape (wide).
Margins create whitespace around content. I typically use 40mm top/bottom for header/footer space and 20mm sides for comfortable reading. Margins don't include headers and footers — those render in the margin space.
For custom page sizes (labels, specialized forms):
renderer.RenderingOptions.CustomPaperWidth = 100; // Millimeters
renderer.RenderingOptions.CustomPaperHeight = 150;
This defines exact page dimensions for non-standard documents.
How Do I Add [Watermarks](https://ironpdf.com/java/blog/using-ironpdf-for-java/java-watermark-pdf-tutorial/) and Backgrounds?
Watermarks brand documents or indicate status (DRAFT, CONFIDENTIAL). Backgrounds add letterheads or decorative elements.
Add a watermark:
var pdf = renderer.RenderHtmlAsPdf(html);
pdf.ApplyWatermark(@"
<div style='
transform: rotate(-45deg);
font-size: 100px;
font-weight: bold;
color: red;
opacity: 0.3;
'>DRAFT</div>
", opacity: 30, rotation: -45,
verticalAlignment: IronPdf.Editing.VerticalAlignment.Middle);
pdf.SaveAs("watermarked-invoice.pdf");
The watermark HTML uses CSS for styling and positioning. The opacity parameter adds transparency (0-100). The rotation parameter angles the watermark. I use 20-40% opacity for subtle watermarks, 50-70% for prominent notices.
Add a background image:
var background = PdfDocument.FromFile("letterhead.pdf");
pdf.AddBackgroundPdf(background);
This overlays the PDF content on top of a background PDF (typically a letterhead with logo and borders). The background appears behind all content. I use this for branded documents where letterhead is designed separately from content.
What About PDF/A for Archival and Compliance?
PDF/A is an ISO standard for long-term document archiving. It ensures PDFs remain readable decades later by embedding all fonts, prohibiting encryption, and mandating specific PDF features. Required for government submissions, legal archival, and regulatory compliance in many industries.
Create PDF/A documents:
renderer.RenderingOptions.PdfACompliant = true;
var pdf = renderer.RenderHtmlAsPdf(html);
pdf.SaveAs("archive.pdf");
The PDF/A conversion embeds fonts, converts colors to device-independent color spaces, and removes features incompatible with archival requirements. Adobe Reader displays "PDF/A-compliant document" when opening these files.
I use PDF/A for financial reports requiring 7+ year retention, legal documents submitted to courts, and healthcare records requiring HIPAA-compliant archival. The archival format ensures documents remain accessible regardless of software changes over time.
How Do I Merge Multiple PDFs or Add Pages?
Document assembly often requires combining multiple PDFs — adding terms and conditions to contracts, merging monthly reports into annual summaries, concatenating scanned documents.
Merge PDFs:
var invoice = PdfDocument.FromFile("invoice.pdf");
var terms = PdfDocument.FromFile("terms.pdf");
var signature = PdfDocument.FromFile("signature.pdf");
invoice.Merge(terms);
invoice.Merge(signature);
invoice.SaveAs("complete-contract.pdf");
This appends pages from each PDF to create a combined document. The order matches the order of merge operations. I use this for generating complete contract packages where each section comes from separate templates.
Add blank pages for printing requirements:
pdf.AddBlankPage(); // Adds at end
Blank pages ensure double-sided printing alignment or create space for physical signatures.
What's the Performance Like for Batch Processing?
For generating thousands of PDFs — monthly statement runs, bulk invoices, batch receipts — performance optimization matters.
Use async methods for concurrent processing:
var renderer = new ChromePdfRenderer();
var tasks = new List<Task<PdfDocument>>();
foreach (var invoice in invoices)
{
var html = GenerateInvoiceHtml(invoice);
var task = renderer.RenderHtmlAsPdfAsync(html);
tasks.Add(task);
}
var pdfs = await Task.WhenAll(tasks);
for (int i = 0; i < pdfs.Length; i++)
{
pdfs[i].SaveAs($"invoice-{i}.pdf");
}
This processes multiple PDFs concurrently, utilizing available CPU cores. I've generated 10,000 invoices in 15 minutes using this pattern on an 8-core server — roughly 11 PDFs per second.
For very large batches, process in chunks to manage memory:
var batchSize = 100;
for (int i = 0; i < invoices.Count; i += batchSize)
{
var batch = invoices.Skip(i).Take(batchSize);
var tasks = batch.Select(inv => renderer.RenderHtmlAsPdfAsync(GenerateHtml(inv)));
var pdfs = await Task.WhenAll(tasks);
for (int j = 0; j < pdfs.Length; j++)
{
pdfs[j].SaveAs($"invoice-{i + j}.pdf");
pdfs[j].Dispose();
}
}
Processing 100 documents at a time prevents memory exhaustion from concurrent operations on tens of thousands of PDFs.
Quick Reference
| Task | Code | Notes |
|---|---|---|
| Install | dotnet add package IronPdf |
NuGet installation |
| HTML to PDF | renderer.RenderHtmlAsPdf(html) |
Most common pattern |
| From template | RenderHtmlFileAsPdf("template.html") |
File-based templates |
| Add header | RenderingOptions.HtmlHeader = ... |
HTML with placeholders |
| Add footer | RenderingOptions.HtmlFooter = ... |
Page numbers, dates |
| Set page size | RenderingOptions.PaperSize = A4 |
Letter, A4, Legal, custom |
| Set margins | RenderingOptions.MarginTop = 40 |
Millimeters |
| Add watermark | pdf.ApplyWatermark(html) |
After PDF creation |
| PDF/A archival | RenderingOptions.PdfACompliant = true |
ISO standard |
| Merge PDFs | pdf1.Merge(pdf2) |
Combine documents |
| Async batch | RenderHtmlAsPdfAsync(html) |
Concurrent processing |
Key Principles:
- HTML conversion is easier than programmatic construction for complex documents
- Template-based workflows separate design (HTML) from data (C#)
- IronPDF uses Chromium engine for modern CSS/JavaScript support
- Avoid iTextSharp (AGPL licensing issues), wkhtmltopdf (unmaintained)
- Use async methods for batch processing (10+ PDFs)
- PDF/A required for archival and many compliance scenarios
- Headers/footers support dynamic placeholders (page numbers, dates)
- Works across .NET Framework, .NET Core, .NET 6-10
The complete C# PDF creation tutorial includes advanced scenarios like form creation, digital signatures, and custom fonts.
Written by Jacob Mellor, CTO at Iron Software. Jacob created IronPDF and leads a team of 50+ engineers building .NET document processing libraries.
Top comments (0)