Teams building invoice generators face a choice: describe the layout in code (position this text, draw this line, create this table cell) or describe it in HTML/CSS and let a rendering engine handle layout. Both approaches produce PDFs, but the development experience differs dramatically.
With MigraDoc, a 50-line invoice template expands to 200+ lines of C# that explicitly positions every element. Change the header logo size? Recalculate vertical offsets for everything below. Add a new table column? Adjust width calculations across row definitions. The programmatic approach gives precise control but couples visual design to code structure—debugging a misaligned footer means stepping through layout calculations rather than inspecting CSS.
Understanding IronPDF
IronPDF renders HTML and CSS into PDFs using a Chrome-based engine. Instead of positioning elements through code, you write standard HTML and let the browser layout engine calculate positions, handle text wrapping, and manage responsive sizing. The HTML to PDF conversion process translates web page rendering directly into PDF format.
For teams building documents from dynamic data, this means designers can prototype in a browser, developers can use familiar templating (Razor, string interpolation), and the PDF output matches the browser preview pixel-perfectly. No layout calculations in C#, no manual pagination logic, and no recalculating positions when content changes.
Key Limitations of MigraDoc
Product Status
MigraDoc is actively maintained with regular updates supporting current .NET versions. The MIT license and active community make it a viable long-term choice for teams that prefer programmatic document construction. Verify the release cadence and issue response times match your support requirements.
Missing Capabilities
- No HTML rendering: Built for code-driven document construction, not HTML-to-PDF conversion
- Manual layout calculation: Developer explicitly positions elements; no automatic reflow
- Limited web asset support: Cannot directly consume CSS frameworks, web fonts, or complex layouts
- No JavaScript execution: Static document generation only
Technical Issues
- Verbose syntax for complex layouts: Multi-page documents with dynamic content require significant code
- Design iteration cycle: Changes to visual appearance require C# modification and recompilation
- Text rendering limitations: Advanced typography features (ligatures, kerning) depend on PDFsharp's rendering
- Image handling: Requires explicit image loading and sizing calculations
Support Status
MIT open source with community support via GitHub discussions. Commercial support packages available through empira Software—verify terms and pricing for production use.
Architecture Problems
The programmatic approach tightly couples document structure to code. A simple layout change (increase header margin, adjust table cell padding) requires code modification, testing, and redeployment. For documents with frequently changing designs or A/B testing requirements, this coupling creates friction. Templates stored as C# classes also complicate version control and design collaboration with non-developers.
Feature Comparison Overview
| Dimension | MigraDoc | IronPDF |
|---|---|---|
| Current Status | Active, MIT license | Active, commercial |
| HTML Support | Not applicable | Chrome rendering |
| Rendering Quality | Programmatic precision | Browser-equivalent |
| Installation | Single NuGet package | Single NuGet package |
| Support | Community + commercial options | Commercial with SLA |
| Future Viability | Stable, actively maintained | Native .NET, Chrome updates |
Code Comparison: Core Operations
Operation 1: Simple Invoice with Header and Table
MigraDoc — Programmatic Invoice
using MigraDoc.DocumentObjectModel;
using MigraDoc.DocumentObjectModel.Tables;
using MigraDoc.Rendering;
using System;
public class MigraDocInvoiceGenerator
{
public void GenerateInvoice(string outputPath)
{
// Create document
Document document = new Document();
Section section = document.AddSection();
// Configure page
section.PageSetup.PageFormat = PageFormat.Letter;
section.PageSetup.LeftMargin = "2cm";
section.PageSetup.RightMargin = "2cm";
section.PageSetup.TopMargin = "2cm";
section.PageSetup.BottomMargin = "2cm";
// Add header text
Paragraph header = section.AddParagraph("Invoice #12345");
header.Format.Font.Size = 24;
header.Format.Font.Bold = true;
header.Format.SpaceAfter = "0.5cm";
// Add invoice date
Paragraph dateP = section.AddParagraph($"Date: {DateTime.Now:yyyy-MM-dd}");
dateP.Format.SpaceAfter = "1cm";
// Customer information
Paragraph customer = section.AddParagraph("Customer: Acme Corp");
customer.Format.Font.Size = 12;
customer.Format.SpaceAfter = "1cm";
// Create items table
Table table = section.AddTable();
table.Borders.Width = 0.75;
// Define columns with explicit widths
Column column = table.AddColumn("5cm");
column = table.AddColumn("3cm");
column = table.AddColumn("3cm");
column = table.AddColumn("3cm");
// Header row
Row row = table.AddRow();
row.Shading.Color = Colors.LightGray;
row.Cells[0].AddParagraph("Item");
row.Cells[1].AddParagraph("Quantity");
row.Cells[2].AddParagraph("Unit Price");
row.Cells[3].AddParagraph("Total");
// Data rows - each requires explicit row creation
AddItemRow(table, "Widget Pro", 10, 29.99m);
AddItemRow(table, "Gadget Ultra", 5, 49.99m);
// Total row
Row totalRow = table.AddRow();
totalRow.Cells[2].AddParagraph("Grand Total:");
totalRow.Cells[2].Format.Font.Bold = true;
totalRow.Cells[2].Format.Alignment = ParagraphAlignment.Right;
totalRow.Cells[3].AddParagraph("$549.85");
totalRow.Cells[3].Format.Font.Bold = true;
// Render to PDF
PdfDocumentRenderer pdfRenderer = new PdfDocumentRenderer();
pdfRenderer.Document = document;
pdfRenderer.RenderDocument();
pdfRenderer.PdfDocument.Save(outputPath);
}
private void AddItemRow(Table table, string name, int quantity, decimal price)
{
Row row = table.AddRow();
row.Cells[0].AddParagraph(name);
row.Cells[1].AddParagraph(quantity.ToString());
row.Cells[1].Format.Alignment = ParagraphAlignment.Center;
row.Cells[2].AddParagraph($"${price:F2}");
row.Cells[2].Format.Alignment = ParagraphAlignment.Right;
row.Cells[3].AddParagraph($"${quantity * price:F2}");
row.Cells[3].Format.Alignment = ParagraphAlignment.Right;
}
}
// Usage
var generator = new MigraDocInvoiceGenerator();
generator.GenerateInvoice("invoice.pdf");
Technical Limitations:
- Layout calculations are manual—column widths must sum correctly or table renders incorrectly
- Adding/removing columns requires updating width definitions and all row cell references
- Font sizes, margins, and spacing specified as magic numbers scattered through code
- No CSS-style inheritance; each paragraph needs explicit formatting
- Alignment and styling set individually per cell; no stylesheet concept
- Changes to visual design require code modification and redeployment
Benchmark Characteristics (Typical):
- Cold start (first PDF): Faster than Chrome initialization (~50-100ms for simple docs)
- Warm generation (subsequent PDFs): Minimal overhead, sub-20ms for simple documents
- Memory usage: Low baseline, grows with document complexity
- Threading: Safe to use in multi-threaded scenarios with separate Document instances
IronPDF — HTML Invoice
using IronPdf;
var invoiceData = new
{
Number = "12345",
Date = DateTime.Now.ToString("yyyy-MM-dd"),
Customer = "Acme Corp",
Items = new[]
{
new { Name = "Widget Pro", Quantity = 10, Price = 29.99m },
new { Name = "Gadget Ultra", Quantity = 5, Price = 49.99m }
},
Total = 549.85m
};
var html = $@"
<style>
body {{ font-family: Arial, sans-serif; margin: 40px; }}
h1 {{ color: #333; }}
table {{ width: 100%; border-collapse: collapse; margin-top: 20px; }}
th {{ background: #f0f0f0; padding: 10px; text-align: left; }}
td {{ padding: 8px; border-bottom: 1px solid #ddd; }}
.right {{ text-align: right; }}
.total {{ font-weight: bold; }}
</style>
<h1>Invoice #{invoiceData.Number}</h1>
<p>Date: {invoiceData.Date}</p>
<p>Customer: {invoiceData.Customer}</p>
<table>
<tr><th>Item</th><th>Quantity</th><th>Unit Price</th><th>Total</th></tr>
{string.Join("", invoiceData.Items.Select(item => $@"
<tr>
<td>{item.Name}</td>
<td class='right'>{item.Quantity}</td>
<td class='right'>${item.Price:F2}</td>
<td class='right'>${item.Quantity * item.Price:F2}</td>
</tr>
"))}
<tr class='total'>
<td colspan='3' class='right'>Grand Total:</td>
<td class='right'>${invoiceData.Total:F2}</td>
</tr>
</table>";
var renderer = new ChromePdfRenderer();
var pdf = renderer.RenderHtmlAsPdf(html);
pdf.SaveAs("invoice.pdf");
The HTML to PDF documentation shows how CSS handles styling globally while C# handles data. Change the header color? Edit CSS. Add a column? Update the HTML template. No layout recalculation needed.
Benchmark Characteristics (Typical):
- Cold start (first PDF): Chrome initialization adds ~200-400ms first time
- Warm generation (subsequent PDFs): ~100-200ms per document depending on complexity
- Memory usage: Higher baseline due to Chrome engine, stable during bulk generation
- Threading: Async-friendly with proper renderer instance management
Operation 2: Multi-Page Document with Headers and Footers
MigraDoc — Multi-Page Report
using MigraDoc.DocumentObjectModel;
using MigraDoc.DocumentObjectModel.Tables;
using MigraDoc.Rendering;
using System;
using System.Linq;
public void GenerateMultiPageReport(string outputPath)
{
Document document = new Document();
Section section = document.AddSection();
// Page setup
section.PageSetup.PageFormat = PageFormat.Letter;
section.PageSetup.TopMargin = "3cm"; // Space for header
section.PageSetup.BottomMargin = "3cm"; // Space for footer
// Define header (appears on every page)
Paragraph headerPara = section.Headers.Primary.AddParagraph();
headerPara.AddText("Monthly Sales Report - Company Confidential");
headerPara.Format.Font.Size = 10;
headerPara.Format.Font.Color = Colors.Gray;
headerPara.Format.Borders.Bottom.Width = 0.5;
headerPara.Format.SpaceBefore = "1cm";
// Define footer (appears on every page)
Paragraph footerPara = section.Footers.Primary.AddParagraph();
footerPara.AddText("Page ");
footerPara.AddPageField();
footerPara.AddText(" of ");
footerPara.AddNumPagesField();
footerPara.Format.Alignment = ParagraphAlignment.Center;
footerPara.Format.Font.Size = 9;
// Title
Paragraph title = section.AddParagraph("Q4 2025 Sales Analysis");
title.Format.Font.Size = 20;
title.Format.Font.Bold = true;
title.Format.SpaceAfter = "0.5cm";
// Generate 50 data rows to force pagination
Table table = section.AddTable();
table.Borders.Width = 0.5;
Column col1 = table.AddColumn("4cm");
Column col2 = table.AddColumn("3cm");
Column col3 = table.AddColumn("3cm");
Column col4 = table.AddColumn("3cm");
// Header row
Row headerRow = table.AddRow();
headerRow.HeadingFormat = true; // Repeat on each page
headerRow.Shading.Color = Colors.LightBlue;
headerRow.Cells[0].AddParagraph("Product");
headerRow.Cells[1].AddParagraph("Region");
headerRow.Cells[2].AddParagraph("Units Sold");
headerRow.Cells[3].AddParagraph("Revenue");
// Data rows (simulate large dataset)
var products = new[] { "Widget", "Gadget", "Doohickey", "Thingamajig" };
var regions = new[] { "North", "South", "East", "West" };
var random = new Random(42);
for (int i = 0; i < 50; i++)
{
Row row = table.AddRow();
row.Cells[0].AddParagraph(products[i % products.Length]);
row.Cells[1].AddParagraph(regions[i % regions.Length]);
row.Cells[2].AddParagraph(random.Next(100, 1000).ToString());
row.Cells[2].Format.Alignment = ParagraphAlignment.Right;
row.Cells[3].AddParagraph($"${random.Next(1000, 50000):N0}");
row.Cells[3].Format.Alignment = ParagraphAlignment.Right;
// MigraDoc handles pagination automatically once configured
}
// Render
PdfDocumentRenderer pdfRenderer = new PdfDocumentRenderer();
pdfRenderer.Document = document;
pdfRenderer.RenderDocument();
pdfRenderer.PdfDocument.Save(outputPath);
}
Technical Limitations:
- Header/footer setup requires understanding MigraDoc's section structure
- Page number fields (
AddPageField) use specific API; no simple text interpolation - Table header repetition requires
HeadingFormat = true; easy to miss - Pagination happens automatically but customization options are limited
- Cannot use CSS media queries or print-specific styling
- Debugging page break behavior requires understanding MigraDoc's layout engine
Benchmark Characteristics:
- Rendering 50-row table: ~100-200ms depending on complexity
- Memory usage scales linearly with document size
- Pagination calculation: Automatic but no preview/control from code
IronPDF — Multi-Page HTML Report
using IronPdf;
using System;
using System.Linq;
var products = new[] { "Widget", "Gadget", "Doohickey", "Thingamajig" };
var regions = new[] { "North", "South", "East", "West" };
var random = new Random(42);
var rows = string.Join("", Enumerable.Range(0, 50).Select(i => $@"
<tr>
<td>{products[i % products.Length]}</td>
<td>{regions[i % regions.Length]}</td>
<td class='right'>{random.Next(100, 1000)}</td>
<td class='right'>${random.Next(1000, 50000):N0}</td>
</tr>
"));
var html = $@"
<style>
@page {{ margin: 1in; }}
body {{ font-family: Arial, sans-serif; }}
h1 {{ page-break-after: avoid; }}
table {{ width: 100%; border-collapse: collapse; }}
thead {{ display: table-header-group; }}
th {{ background: #4A90E2; color: white; padding: 10px; }}
td {{ padding: 8px; border-bottom: 1px solid #ddd; }}
.right {{ text-align: right; }}
</style>
<h1>Q4 2025 Sales Analysis</h1>
<table>
<thead>
<tr><th>Product</th><th>Region</th><th>Units Sold</th><th>Revenue</th></tr>
</thead>
<tbody>{rows}</tbody>
</table>
<div style='position: running(header); text-align: center; color: #666;'>
Monthly Sales Report - Company Confidential
</div>";
var renderer = new ChromePdfRenderer();
renderer.RenderingOptions.MarginTop = 50;
renderer.RenderingOptions.MarginBottom = 50;
renderer.RenderingOptions.FirstPageNumber = 1;
var pdf = renderer.RenderHtmlAsPdf(html);
pdf.SaveAs("report.pdf");
CSS @page rules handle headers and footers, display: table-header-group repeats table headers on each page, and page-break-after: avoid prevents orphaned headings. The HTML to PDF tutorial explains these print-specific CSS features.
Benchmark Characteristics:
- 50-row table rendering: ~150-250ms (includes Chrome layout engine)
- Memory: Higher base but efficient for large tables due to streaming
- Pagination: Handled by Chrome's layout engine with CSS control
Operation 3: Dynamic Content with Conditional Formatting
MigraDoc — Conditional Styling
using MigraDoc.DocumentObjectModel;
using MigraDoc.DocumentObjectModel.Tables;
using MigraDoc.Rendering;
using System.Collections.Generic;
public void GenerateConditionalReport(List<SalesRecord> records, string outputPath)
{
Document document = new Document();
Section section = document.AddSection();
Paragraph title = section.AddParagraph("Sales Performance Report");
title.Format.Font.Size = 18;
title.Format.Font.Bold = true;
title.Format.SpaceAfter = "1cm";
Table table = section.AddTable();
table.Borders.Width = 0.5;
table.AddColumn("5cm");
table.AddColumn("3cm");
table.AddColumn("3cm");
table.AddColumn("3cm");
// Header
Row headerRow = table.AddRow();
headerRow.Shading.Color = Colors.LightGray;
headerRow.Cells[0].AddParagraph("Salesperson");
headerRow.Cells[1].AddParagraph("Target");
headerRow.Cells[2].AddParagraph("Actual");
headerRow.Cells[3].AddParagraph("Status");
foreach (var record in records)
{
Row row = table.AddRow();
row.Cells[0].AddParagraph(record.SalespersonName);
row.Cells[1].AddParagraph($"${record.Target:N0}");
row.Cells[1].Format.Alignment = ParagraphAlignment.Right;
row.Cells[2].AddParagraph($"${record.Actual:N0}");
row.Cells[2].Format.Alignment = ParagraphAlignment.Right;
// Conditional formatting based on performance
Paragraph statusPara;
if (record.Actual >= record.Target * 1.1m)
{
statusPara = row.Cells[3].AddParagraph("Exceeds");
row.Cells[3].Shading.Color = new Color(200, 255, 200); // Light green
statusPara.Format.Font.Color = new Color(0, 128, 0); // Dark green
}
else if (record.Actual >= record.Target)
{
statusPara = row.Cells[3].AddParagraph("Meets");
row.Cells[3].Shading.Color = new Color(255, 255, 200); // Light yellow
}
else
{
statusPara = row.Cells[3].AddParagraph("Below");
row.Cells[3].Shading.Color = new Color(255, 200, 200); // Light red
statusPara.Format.Font.Color = Colors.DarkRed;
}
statusPara.Format.Alignment = ParagraphAlignment.Center;
statusPara.Format.Font.Bold = true;
}
PdfDocumentRenderer pdfRenderer = new PdfDocumentRenderer();
pdfRenderer.Document = document;
pdfRenderer.RenderDocument();
pdfRenderer.PdfDocument.Save(outputPath);
}
public class SalesRecord
{
public string SalespersonName { get; set; }
public decimal Target { get; set; }
public decimal Actual { get; set; }
}
Technical Limitations:
- Conditional logic embedded in document generation code; hard to modify without C# changes
- Color specifications use MigraDoc's Color class; can't use standard hex colors directly
- Each conditional branch requires explicit cell formatting; verbose for complex rules
- No CSS-like classes or reusable styling; must repeat formatting logic
- Difficult to preview style changes without running code
IronPDF — CSS-Based Conditional Styling
using IronPdf;
using System.Linq;
var records = new[]
{
new { Name = "Alice", Target = 100000m, Actual = 115000m },
new { Name = "Bob", Target = 100000m, Actual = 102000m },
new { Name = "Charlie", Target = 100000m, Actual = 85000m }
};
var rows = string.Join("", records.Select(r =>
{
var performance = r.Actual >= r.Target * 1.1m ? "exceeds" :
r.Actual >= r.Target ? "meets" : "below";
var statusText = r.Actual >= r.Target * 1.1m ? "Exceeds" :
r.Actual >= r.Target ? "Meets" : "Below";
return $@"
<tr class='{performance}'>
<td>{r.Name}</td>
<td class='right'>${r.Target:N0}</td>
<td class='right'>${r.Actual:N0}</td>
<td class='status'>{statusText}</td>
</tr>";
}));
var html = $@"
<style>
table {{ width: 100%; border-collapse: collapse; }}
th {{ background: #333; color: white; padding: 10px; }}
td {{ padding: 8px; border-bottom: 1px solid #ddd; }}
.right {{ text-align: right; }}
.status {{ text-align: center; font-weight: bold; }}
.exceeds {{ background: #c8ffc8; color: #008000; }}
.meets {{ background: #ffffc8; }}
.below {{ background: #ffc8c8; color: #8b0000; }}
</style>
<h1>Sales Performance Report</h1>
<table>
<tr><th>Salesperson</th><th>Target</th><th>Actual</th><th>Status</th></tr>
{rows}
</table>";
var renderer = new ChromePdfRenderer();
var pdf = renderer.RenderHtmlAsPdf(html);
pdf.SaveAs("performance.pdf");
CSS classes handle styling; C# logic determines which class to apply. Change colors? Edit CSS. Add new status categories? Add CSS classes. See the HTML string to PDF guide for templating patterns.
API Mapping Reference
| MigraDoc Concept | IronPDF Equivalent | Notes |
|---|---|---|
Document object |
HTML string | Structure in HTML |
Section |
HTML page/body | Page setup via CSS |
Paragraph |
<p>, <h1>, etc. |
Standard HTML |
Table / Row / Cell
|
<table> / <tr> / <td>
|
Standard HTML tables |
Format.Font.Size |
CSS font-size
|
Standard CSS |
Format.Font.Bold |
CSS font-weight: bold
|
Standard CSS |
Format.Alignment |
CSS text-align
|
Standard CSS |
Shading.Color |
CSS background-color
|
Standard CSS |
Borders.Width |
CSS border
|
Standard CSS |
AddColumn(width) |
CSS width on <td>
|
Or CSS Grid/Flexbox |
Headers.Primary |
CSS @page with running()
|
Print-specific CSS |
Footers.Primary |
CSS @page margins |
Print-specific CSS |
AddPageField() |
CSS counters or JS | Page numbers via CSS |
PdfDocumentRenderer |
ChromePdfRenderer |
Different rendering approach |
.RenderDocument() |
.RenderHtmlAsPdf() |
Core conversion method |
Comprehensive Feature Comparison
| Feature | MigraDoc | IronPDF |
|---|---|---|
| Status & Support | ||
| Active Development | Yes (MIT, empira) | Yes (Commercial) |
| .NET Framework Support | 4.6.2+ | 4.6.2+ |
| .NET Core/5+/6+/8+ Support | Native | Native |
| Commercial Support | Available via empira | Included with license |
| Content Creation | ||
| HTML to PDF | No (not designed for HTML) | Yes (core feature) |
| Programmatic Layout | Yes (primary method) | No (uses HTML/CSS) |
| Table Support | Comprehensive API | HTML <table>
|
| Image Embedding | Explicit API calls |
<img> tags |
| Custom Fonts | Font resolver API | Web fonts, @font-face |
| Page Numbers | Field API | CSS counters/custom code |
| Headers/Footers | Section-based | CSS @page rules |
| Design Iteration | ||
| Visual Changes | Requires code changes | Edit HTML/CSS |
| Designer Involvement | Limited (code-only) | Full (browser tools) |
| Preview Workflow | Run code to see output | Browser preview = PDF |
| Templating | C# classes | Razor, string interpolation |
| PDF Manipulation | ||
| Merge PDFs | Via PDFsharp | Native |
| Split PDFs | Via PDFsharp | Native |
| Form Filling | Via PDFsharp | Native |
| Text Extraction | Via PDFsharp | Native |
| Benchmark Characteristics (Typical) | ||
| Cold Start Performance | Faster (~50-100ms) | Slower (Chrome init ~200-400ms) |
| Warm Performance | Very fast (<50ms simple docs) | Moderate (~100-200ms) |
| Memory Baseline | Low | Higher (Chrome engine) |
| Complex Layout Speed | Linear with code complexity | Deferred to Chrome engine |
| Development | ||
| Installation | Single NuGet | Single NuGet |
| Learning Curve | MigraDoc-specific API | HTML/CSS knowledge |
| Code Verbosity | High for complex layouts | Low (HTML is concise) |
| IDE Support | IntelliSense for API | HTML/CSS tooling |
| Debugging | Standard C# debugging | Browser DevTools + C# |
Installation Comparison
MigraDoc:
dotnet add package PDFsharp-MigraDoc
using MigraDoc.DocumentObjectModel;
using MigraDoc.DocumentObjectModel.Tables;
using MigraDoc.Rendering;
IronPDF:
dotnet add package IronPdf
using IronPdf;
Conclusion
MigraDoc excels when you need programmatic precision, have simple documents with predictable layouts, or work in environments where HTML/CSS knowledge is unavailable. The low-level control and fast rendering make it suitable for high-volume generation of structured documents like reports from templates that rarely change.
For modern .NET teams building documents from dynamic data, especially when designs evolve frequently or non-developers need to modify layouts, the HTML/CSS approach eliminates a category of friction. Instead of translating design requirements into positioning code, designers work in browsers and developers use standard templating. The separation of content (C#) from presentation (CSS) matches web development patterns most teams already know.
Migration becomes mandatory when layout changes become a bottleneck (requiring developer cycles for what should be design tweaks), when you need complex layouts that would require excessive positioning calculations in code, or when integrating web assets (fonts, CSS frameworks, responsive designs) that MigraDoc can't consume.
IronPDF provides HTML to PDF rendering with a Chrome-based engine that handles layout automatically. The approach trades MigraDoc's cold-start speed and minimal memory footprint for design flexibility and web technology compatibility. For teams that view PDFs as rendered web pages rather than programmatically constructed documents, IronPDF aligns better with existing skills and workflows.
Have you found programmatic layout worth the control, or does HTML/CSS productivity outweigh the rendering overhead?
For implementation patterns, see the HTML file to PDF documentation and comprehensive API reference.
Top comments (0)