Invoices are the most common PDF use case. With traditional libraries (PdfSharp, iText), you're managing coordinates, page breaks, and table layouts manually — hundreds of lines of boilerplate.
TerraPDF changes this. Its declarative, layout-first API lets you build professional invoices in 30 lines of actual generation code.
Setup
dotnet add package TerraPDF # MIT-licensed, zero dependencies
Complete 30-Line Invoice Generator
using TerraPDF.Core;
using TerraPDF.Helpers;
public record Invoice(string Number, DateTime Date, string Customer, List<Item> Items)
{
public decimal Subtotal => Items.Sum(i => i.LineTotal);
public decimal Tax => Math.Round(Subtotal * 0.10m, 2);
public decimal Total => Subtotal + Tax;
}
public record Item(string Description, int Quantity, decimal UnitPrice)
{
public decimal LineTotal => Quantity * UnitPrice;
}
public static class InvoiceGenerator
{
public static byte[] Generate(Invoice inv)
{
return Document.Create(doc =>
{
doc.Page(page =>
{
page.Size(PageSize.A4);
page.Margin(2, Unit.Centimetre);
page.DefaultTextStyle(s => s.FontSize(10));
// ── Header ───────────────────────────────────────────────────
page.Header().Column(h =>
{
h.Item().Text("INVOICE").Bold().FontSize(22).FontColor("#1a4a8a");
h.Item().Row(r =>
{
r.RelativeItem().Text($"#: {inv.Number} | Date: {inv.Date:yyyy-MM-dd}");
r.AutoItem().AlignRight()
.Text($"Due: {inv.Date.AddDays(30):yyyy-MM-dd}")
.FontColor(Color.Red.Medium);
});
h.Item().LineHorizontal(1, "#1a4a8a");
});
// ── Content ──────────────────────────────────────────────────
page.Content().Column(c =>
{
c.Spacing(12);
// Bill-from / Bill-to
c.Item().Row(r =>
{
r.RelativeItem().Text("Your Company\n123 Main St\nCity, Country");
r.RelativeItem().AlignRight().Text($"BILL TO:\n{inv.Customer}");
});
// Line-items table
c.Item().Table(t =>
{
t.ColumnsDefinition(cols =>
{
cols.RelativeColumn(5); // Description
cols.RelativeColumn(1); // Qty
cols.RelativeColumn(2); // Unit Price
cols.RelativeColumn(2); // Total
});
t.HeaderRow(row =>
{
row.Cell().Background("#1a4a8a").Padding(5)
.Text("Description").Bold().FontColor(Color.White);
row.Cell().Background("#1a4a8a").Padding(5).AlignCenter()
.Text("Qty").Bold().FontColor(Color.White);
row.Cell().Background("#1a4a8a").Padding(5).AlignRight()
.Text("Price").Bold().FontColor(Color.White);
row.Cell().Background("#1a4a8a").Padding(5).AlignRight()
.Text("Total").Bold().FontColor(Color.White);
});
foreach (var item in inv.Items)
{
t.Row(row =>
{
row.Cell().Padding(5).Text(item.Description);
row.Cell().Padding(5).AlignCenter().Text(item.Quantity.ToString());
row.Cell().Padding(5).AlignRight().Text($"{item.UnitPrice:C}");
row.Cell().Padding(5).AlignRight().Text($"{item.LineTotal:C}");
});
}
});
// Totals block (right-aligned)
c.Item().Row(r =>
{
r.RelativeItem(7); // left spacer
r.RelativeItem(3).Column(tot =>
{
tot.Item().LineHorizontal(1, Color.Grey.Lighten2);
tot.Item().Row(tr =>
{
tr.RelativeItem().Text("Subtotal:");
tr.RelativeItem().AlignRight().Text($"{inv.Subtotal:C}");
});
tot.Item().Row(tr =>
{
tr.RelativeItem().Text("Tax (10%):");
tr.RelativeItem().AlignRight().Text($"{inv.Tax:C}");
});
tot.Item().LineHorizontal(1.5, "#1a4a8a");
tot.Item().Row(tr =>
{
tr.RelativeItem().Text("TOTAL DUE").Bold();
tr.RelativeItem().AlignRight().Text($"{inv.Total:C}").Bold()
.FontColor("#1a4a8a");
});
});
});
// Payment terms
c.Item().MarginTop(8).Border(1, Color.Grey.Lighten2)
.Padding(8).Text(
"Payment due within 30 days. Late payments incur 1.5% monthly interest.")
.FontSize(9).FontColor(Color.Grey.Darken1).Justify();
// Thank you
c.Item().AlignCenter()
.Text("Thank you for your business!").FontSize(9).FontColor(Color.Grey.Medium);
});
// ── Footer ───────────────────────────────────────────────────
page.Footer().AlignCenter().Text(t =>
{
t.Span("Page "); t.CurrentPageNumber();
t.Span(" of "); t.TotalPages();
});
});
}).PublishPdf(); // returns byte[]
}
}
Usage:
var inv = new Invoice("INV-2025-001", DateTime.Today, "Acme Corp", new()
{
new("Web Development", 40, 150m),
new("Server Maintenance", 10, 100m),
});
File.WriteAllBytes("invoice.pdf", InvoiceGenerator.Generate(inv));
What you get: Header, bill-from/to, multi-line table with auto page breaks, totals, payment terms, footer with page numbers — all in 30 lines inside the Document.Create lambda.
Why This Works: Layout-First, Not Coordinate-First
Traditional PDF libraries force you to:
- ❌ Calculate Y positions manually (
y -= 18) - ❌ Check for page overflow and create new pages
- ❌ Draw table grid lines individually
- ❌ Recreate font objects repeatedly
TerraPDF handles it:
- ✅ Automatic pagination and text wrapping
- ✅ Native tables with repeating headers across pages
- ✅ Built-in page numbering (X of Y)
- ✅ Declarative containers (Column, Row) manage spacing
You describe what to render. TerraPDF handles how to position it.
Advanced: Multi-Page Tables With Auto Headers
TerraPDF tables automatically repeat header rows on page breaks — no extra code needed. 100 items? It splits across as many pages as required:
table.HeaderRow(row => // This repeats on every page
{
row.Cell().Background("#1a4a8a").Text("Description").Bold();
// ... other headers
});
foreach (var item in largeItemList) // Automatic page breaks
table.Row(row => { /* cells */ });
This is extremely hard in PdfSharp or DIY approaches.
Comparison: TerraPDF vs PdfSharp vs DIY for Invoices
| Feature | TerraPDF | PdfSharp | DIY |
|---|---|---|---|
| Lines of code | 30 | 100+ | 150+ |
| Auto pagination | ✅ | ❌ Manual | ❌ Manual |
| Tables with repeating headers | ✅ | ❌ | ❌ |
| Page numbers (X of Y) | ✅ Built-in | ❌ Manual | ❌ Manual |
| Maintenance | ✅ Low | ⚠️ Medium | ❌ High |
| Production ready | ✅ | ✅ | ❌ |
Production Pattern: Reusable Components
For larger projects, extract into a reusable IComponent:
public class InvoiceDocument : IComponent
{
private readonly Invoice _data;
public InvoiceDocument(Invoice data) => _data = data;
public void Compose(IContainer container) => container.Page(page =>
{
page.Header().Text($"INVOICE #{_data.Number}").Bold();
page.Content().Table(t => BuildInvoiceTable(t));
page.Footer().Text($"Page {page.PageNumber}");
});
private void BuildInvoiceTable(ITableDescriptor t) { /* table logic */ }
}
Document.Create(new InvoiceDocument(invoice)).PublishPdf("invoice.pdf");
Quick Enhancements
- Add logo:
r.ConstantItem(120).Image("logo.png", 40); - Add QR code: Generate as image, embed with
.Image() - Multi-currency: Use
CultureInfofor formatting - Conditional sections: Use
.ShowIf(condition)decorator
Best Practices
- Use records for data models (concise, immutable)
- Centralize constants (colors, sizes) to
constfields - Extract table building to separate methods
- Validate input before generation
- Reuse styles with
page.DefaultTextStyle()instead of repeating on every call - Unit test by parsing PDF text with a PDF library
Why This Matters
TerraPDF makes PDF generation approachable. No coordinate math. No manual page breaks. No grid-drawing loops.
In 30 lines you get:
- Professional multi-page invoices
- Auto-pagination with repeating table headers
- Built-in page numbers
- Clean, maintainable code
- MIT license
If you've avoided PDF generation, TerraPDF changes the game.