Invoices are the most common PDF generation use case. You need them for e-commerce, freelancing, SaaS billing, or enterprise invoicing systems.
Many developers assume a polished invoice requires hundreds of lines of coordinate math, manual pagination, and boilerplate. That's true with low-level libraries. But with TerraPDF's modern fluent API, you can build a production-ready invoice generator in 30 lines of actual generation logic.
The Approach: TerraPDF's Declarative Fluent API
TerraPDF is a zero-dependency, MIT-licensed .NET library with a layout-first design. Instead of drawing at coordinates, you describe the document structure: pages, headers, content columns, tables. TerraPDF handles pagination, spacing, and positioning automatically.
📦 Install TerraPDF from NuGet: https://www.nuget.org/packages/TerraPDF/
dotnet add package TerraPDF
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 (2 lines):
var inv = new Invoice("INV-2025-001", DateTime.Today, "Acme Corp\n123 Main St", new()
{
new("Web Development", 40, 150m),
new("Server Maintenance", 10, 100m),
new("UI/UX Design", 20, 120m),
});
File.WriteAllBytes("invoice.pdf", InvoiceGenerator.Generate(inv));
That's it — 30 lines of actual generation code (the lambda body inside Document.Create). The invoice includes:
- Header with company branding
- Bill-from/bill-to columns
- Multi-line item table (auto-handles page breaks)
- Grand total with tax calculation
- Payment terms box
- Auto page numbers in footer
- Professional styling
Breaking Down the 30 Lines
Counting the meaningful lines inside the Document.Create callback:
page.Size(PageSize.A4);page.Margin(2, Unit.Centimetre);page.DefaultTextStyle(s => s.FontSize(10));page.Header().Column(h => {h.Item().Text("INVOICE").Bold()...h.Item().Row(r => { ... });h.Item().LineHorizontal(1, "#1a4a8a");page.Content().Column(c => {c.Spacing(12);c.Item().Row(r => { ... });// Bill-from/toc.Item().Table(t => {t.ColumnsDefinition(cols => { ... });t.HeaderRow(row => { ... });foreach (var item in inv.Items) { t.Row(row => { ... }); }c.Item().Row(r => { ... });// Totalsc.Item().MarginTop(8).Border(1, ...).Padding(8).Text(...);c.Item().AlignCenter().Text("Thank you...");page.Footer().AlignCenter().Text(t => {t.Span("Page "); t.CurrentPageNumber(); t.Span(" of "); t.TotalPages();});
Total: ~20 structural lines, plus the 8 lines for table column definitions, header styling, and row content = 30 lines of meaningful document construction.
Braces ({}) and usings are not counted. The record definitions and Main call are separate.
Why This Is Actually 30 Lines (Not 120)
With TerraPDF, you're not:
- Manually calculating Y positions for each row (
y -= 18loops) - Checking for page overflow (
if (y < 100) AddPage();) - Drawing individual grid lines for tables
- Creating font objects repeatedly (
new PdfFont(...)) - Managing graphics contexts (
XGraphics g = ...)
Instead, TerraPDF's layout containers handle:
- ✅ Automatic text wrapping within column bounds
- ✅ Automatic pagination (text flows to next page)
- ✅ Table row splitting across pages with header repeat
- ✅ Consistent spacing via
.Spacing()on containers - ✅ Built-in page number tracking (
CurrentPageNumber(),TotalPages())
You describe what to render, not how to position every pixel.
Alternative: QuestPDF (Slightly More Declarative)
If you prefer even more declarative syntax:
Document.Create(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(50);
page.Content().Column(col =>
{
col.Item().Text($"INVOICE #{inv.Number}").FontSize(24).FontColor(Colors.Blue.Medium);
col.Item().Text($"Date: {inv.Date:yyyy-MM-dd} | Due: {inv.Date.AddDays(30):yyyy-MM-dd}");
col.Item().LineHorizontal(1).LineColor(Colors.Grey.Lighten2);
col.Item().Row(row =>
{
row.RelativeItem().Text("Your Company\n123 Main St");
row.RelativeItem().AlignRight().Text($"BILL TO:\n{inv.Customer}");
});
col.Item().Table(table =>
{
table.ColumnsDefinition(cols =>
{
cols.RelativeColumn(3); cols.RelativeColumn(1);
cols.RelativeColumn(1); cols.RelativeColumn(1);
});
table.Header(h =>
{
h.Cell().Background(Colors.Blue.Medium).Padding(5)
.Text("Description").FontColor(Colors.White).Bold();
// ... Qty, Price, Total headers
});
foreach (var item in inv.Items)
{
table.Cell().Text(item.Description);
table.Cell().AlignRight().Text(item.Quantity.ToString());
table.Cell().AlignRight().Text($"{item.UnitPrice:C}");
table.Cell().AlignRight().Text($"{item.LineTotal:C}");
}
});
col.Item().AlignRight().Text($"TOTAL: {inv.Total:C}")
.FontSize(14).FontColor(Colors.Blue.Darken2).Bold();
});
});
}).GeneratePdf();
That's ~40 lines but arguably more readable. Both TerraPDF and QuestPDF are excellent modern choices vs legacy PdfSharp/iText.
Advanced: Multi-Page Automatic Tables
TerraPDF's Table automatically splits across pages and repeats the header row on each continuation page — no code needed.
c.Item().Table(table =>
{
table.ColumnsDefinition(cols =>
{
cols.RelativeColumn(4);
cols.RelativeColumn(1);
cols.RelativeColumn(2);
cols.RelativeColumn(2);
});
// This header row repeats on every page automatically
table.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("Amount").Bold().FontColor(Color.White);
});
// 100 items - automatically splits across as many pages as needed
foreach (var item in largeItemList)
table.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.Total:C}");
});
});
This behavior is extremely hard to implement correctly in PdfSharp or DIY approaches.
Comparison: TerraPDF vs PdfSharp vs DIY for Invoices
| Feature | TerraPDF (30 lines) | PdfSharp (100+ lines) | DIY Custom (150+ lines) |
|---|---|---|---|
| Code brevity | ✅ 30 | ❌ 100+ | ❌ 150+ |
| Automatic pagination | ✅ Yes | ❌ Manual | ❌ Manual |
| Table with repeating headers | ✅ Native | ❌ Manual split | ❌ Manual |
| Page numbers (X of Y) | ✅ Built-in | ❌ Manual count | ❌ Manual |
| Header/footer regions | ✅ Yes | ❌ Manual per-page | ❌ Manual |
| Text wrapping | ✅ Automatic | ❌ Clipping | ❌ Manual |
| Maintenance burden | ✅ Low | ⚠️ Medium | ❌ High |
| Production ready | ✅ Yes | ✅ Yes | ❌ No |
Real-World Reusable Component Pattern
For maintainability, wrap the generator in a reusable component:
public class InvoiceDocument : IComponent
{
private readonly Invoice _data;
public InvoiceDocument(Invoice data) => _data = data;
public void Compose(IContainer container)
{
container.Page(page =>
{
page.Size(PageSize.A4);
page.Margin(2, Unit.Centimetre);
page.Header().Column(h =>
{
h.Item().Text("INVOICE").Bold().FontSize(22).FontColor("#1a4a8a");
h.Item().Text($"#: {_data.Number} | Date: {_data.Date:yyyy-MM-dd}");
h.Item().LineHorizontal(1, "#1a4a8a");
});
page.Content().Column(c =>
{
c.Item().Table(BuildTable());
c.Item().AlignRight().Text($"TOTAL: {_data.Total:C}")
.Bold().FontSize(14).FontColor("#1a4a8a");
});
page.Footer().AlignCenter().Text(t =>
{
t.Span("Page "); t.CurrentPageNumber();
t.Span(" of "); t.TotalPages();
});
});
}
private IElement BuildTable() => new TableElement(_data.Items); // extract table to separate method
}
Usage:
var invoice = new Invoice(...);
Document.Create(new InvoiceDocument(invoice)).PublishPdf("invoice.pdf");
Clean separation: data → component → output.
When 30 Lines Isn't Enough
The 30-line solution works for simple invoices. For complex needs:
Add a logo: +3 lines
page.Header().Column(h =>
{
h.Item().Row(r =>
{
r.ConstantItem(120).Image("logo.png", 40);
r.RelativeItem().Text("INVOICE").Bold()...;
});
// ...
});
Add QR code for payment: If you can generate a QR code image, embed it with .Image() — TerraPDF supports PNG/JPEG.
Multi-currency: +1 line per calculation (use CultureInfo for formatting).
Conditional sections: Use .ShowIf(condition) decorator.
Best Practices
- Use records for data models — concise and immutable
- Centralise constants (colors, sizes) as
constfields - Extract table building to a separate method for readability
- Validate input before generation (null checks, quantity > 0)
- Unit test by verifying PDF byte length or using a PDF parser to check text presence
- Reuse styles via
page.DefaultTextStyle(s => s.FontSize(10))instead of repeating font size on every.Text() - Use named colors from
Colorclass where possible for consistency
Conclusion: Is 30 Lines Realistic?
Yes, genuinely. With TerraPDF's fluent, layout-driven API, the entire invoice generation logic fits comfortably in 30 lines of meaningful code. You're not counting DataFrame setup or record definitions — just the document-composition lambda.
How is this possible?
- Automatic pagination (no manual
ycoordinate tracking) - Declarative tables (no cell drawing loops)
- Built-in page numbering (no counter variables)
- Layout containers (Column/Row) handle spacing automatically
Who benefits?
- Developers building billing systems
- SaaS platforms needing PDF receipts
- Freelancers automating client invoices
- Any .NET app requiring well-formatted multi-page documents
What you get for 30 lines:
- Professional invoice with header, footer, table, totals
- Automatic page breaks if table overflows
- Clean, maintainable code
- Zero deployment dependencies
- MIT license — unlimited commercial use
If you've been avoiding PDF generation due to complexity, TerraPDF changes the equation. Install it, write declarative layout code, and generate professional invoices in minutes, not days.