Getting Started
Five concrete scenarios, each ~5 minutes. Pick the one that matches your immediate need.
Scenario 1 — PDF from C#
Section titled “Scenario 1 — PDF from C#”Produce an invoice PDF with a heading, a paragraph, and a simple table.
Install
Section titled “Install”dotnet add package Pragmatic.Documents.Modeldotnet add package Pragmatic.Documents.Pdfusing Pragmatic.Documents.Model;using Pragmatic.Documents.Pdf;
var doc = new DocumentBuilder() .Title("Invoice 2026-001") .Author("Acme Corp") .Size(PageSize.A4) .WithMargins(Margins.Normal) .Page(p => p .Heading("Invoice 2026-001", level: 1) .Paragraph(new TextNode { Content = "Thank you for your business." }) .Spacer(10) .Table(t => t .HeaderRow("Item", "Qty", "Price") .Row("Design review", "1", "€250.00") .Row("Implementation", "3", "€900.00")) .Spacer(20) .Paragraph(new TextNode { Content = "Total: €1150.00", Style = new NodeStyle { FontWeight = FontWeight.Bold } })) .Build();
using var fs = File.Create("invoice.pdf");PdfRenderer.RenderTo(fs, doc);What happens
Section titled “What happens”DocumentBuilderconstructs an immutableDocumentModelwith document metadata and a single pagePdfRenderer.RenderTowrites the PDF directly into the stream — no intermediate byte array
Open invoice.pdf in any viewer. See pdf-rendering.md for fonts, images, TOCs, and page numbering.
Scenario 2 — Same content, PDF + DOCX
Section titled “Scenario 2 — Same content, PDF + DOCX”The document model is renderer-agnostic. Build once, render to both.
dotnet add package Pragmatic.Documents.Docxusing Pragmatic.Documents.Model;using Pragmatic.Documents.Pdf;using Pragmatic.Documents.Docx;
DocumentModel doc = BuildInvoice(); // same builder as before
using (var pdf = File.Create("invoice.pdf")) PdfRenderer.RenderTo(pdf, doc);
using (var docx = File.Create("invoice.docx")) new DocxRenderer().RenderTo(docx, doc);Features like Toc(...) and page numbering Fields render correctly in both formats (viewers that auto-update fields refresh them on open). See docx-rendering.md for DOCX-specific options.
Scenario 3 — XLSX from tabular data
Section titled “Scenario 3 — XLSX from tabular data”Export a list of guests to a styled XLSX.
dotnet add package Pragmatic.Documents.Spreadsheetdotnet add package Pragmatic.Documents.Xlsxusing Pragmatic.Documents.Spreadsheet;using Pragmatic.Documents.Xlsx;
var book = new SpreadsheetBuilder() .Title("Guest Export") .Author("Front Desk") .Sheet("Guests", s => s .Column(width: 8) // ID column .Column(width: 20) // First name .Column(width: 20) // Last name .Column(width: 28) // Email .Freeze(rows: 1, columns: 0) // header row always visible .HeaderRow("Id", "First", "Last", "Email") .Row(1, "Ada", "Lovelace", "ada@example.com") .Row(2, "Grace", "Hopper", "grace@example.com") .FormulaRow("=COUNTA(A2:A3)", "", "", "")) // count of rows .Build();
using var fs = File.Create("guests.xlsx");XlsxRenderer.RenderTo(fs, book);The freeze pane and formula work when opened in Excel or LibreOffice. See xlsx-rendering.md for multi-sheet, merged cells, and per-cell styling.
Scenario 4 — CSV read + write
Section titled “Scenario 4 — CSV read + write”Read an existing CSV, add a column, write it back.
dotnet add package Pragmatic.Documents.Csvusing Pragmatic.Documents.Csv;using Pragmatic.Documents.Spreadsheet;
using var input = File.OpenRead("guests.csv");var book = CsvReader.ReadAsModel(input, sheetName: "Guests");
// Add a "FullName" columnvar sheet = book.Sheets[0];sheet.Rows[0].Cells.Add(new Cell { Value = "FullName" });for (int i = 1; i < sheet.Rows.Count; i++){ var first = sheet.Rows[i].Cells[1].Value; var last = sheet.Rows[i].Cells[2].Value; sheet.Rows[i].Cells.Add(new Cell { Value = $"{first} {last}" });}
using var output = File.Create("guests-enriched.csv");CsvWriter.Write(output, book);CSV handles quoting, CRLF, embedded commas, and RFC 4180 edge cases out of the box. For locale-aware variants (; separator, , decimal) and formula-injection hardening see csv-io.md.
Scenario 5 — HTML email
Section titled “Scenario 5 — HTML email”Build a verification email with a hero section and a call-to-action.
dotnet add package Pragmatic.Email.Modeldotnet add package Pragmatic.Documents.Emailusing Pragmatic.Email.Model;using Pragmatic.Documents.Email;
var email = new EmailBuilder() .Subject("Verify your email address") .Preheader("One click to finish signing up.") .Language("en") .Width(600) .BackgroundColor("#f4f5f7") .Section(s => s .Heading("Welcome, Alice!") .Paragraph("Thanks for signing up for Acme. We need to confirm your email before you can get started.") .Button("Verify your email", "https://app.acme.com/verify?t=abc123") .Paragraph("If you didn't create an account, you can safely ignore this email.")) .Build();
string html = new EmailHtmlRenderer().Render(email);File.WriteAllText("verify.html", html);
// Hand the HTML to Pragmatic.Email or your SMTP libraryThe renderer emits email-client-safe HTML: inline CSS, MSO-compatible tables, preheader text, proper DOCTYPE. See email-rendering.md for advanced layouts (two-column, hero image, footer).
Beyond the five scenarios
Section titled “Beyond the five scenarios”Templates and markup
Section titled “Templates and markup”When designers or non-core developers own layout, use the markup DSL:
dotnet add package Pragmatic.Documents.Markupdotnet add package Pragmatic.Documents.TemplatingWrite invoice.pdxdoc:
@title Invoice {{number}}@author Pragmatic
# Invoice {{number}}
Thank you for your business, {{customer.name}}.
| Item | Qty | Price ||------|-----|-------|{{for item in items}}| {{item.name}} | {{item.qty}} | {{item.price | currency:'EUR'}} |{{endfor}}
**Total: {{total | currency:'EUR'}}**Load, bind, render:
using Pragmatic.Documents.Markup;using Pragmatic.Documents.Templating;using Pragmatic.Documents.Pdf;
var template = PdxDocParser.ParseFile("invoice.pdxdoc");var model = template.Resolve(new { number = "2026-001", customer = new { name = "Alice" }, items = ..., total = 1150m });
using var fs = File.Create("invoice.pdf");PdfRenderer.RenderTo(fs, model);See markup-parser.md for syntax and templating.md for expressions and pipes.
Typed CSV with AOT
Section titled “Typed CSV with AOT”For AOT apps where the generator emits CSV serialisation at compile time:
dotnet add package Pragmatic.Documents.Csvdotnet add package Pragmatic.Documents.Csv.Generator[CsvSerializable<Guest>]public partial class GuestCsv { }
public record Guest(int Id, string First, string Last);
// Zero reflection — generated at compile timeGuestCsv.Write(stream, guests);var decoded = GuestCsv.Read(stream);Runnable samples
Section titled “Runnable samples”Every renderer ships with a runnable samples/ project you can dotnet run:
Pragmatic.Documents.Pdf.Samples— basic / table / multi-page / landscapePragmatic.Documents.Docx.Samples— basic / table / multi-page with TOC / hyperlinksPragmatic.Documents.Xlsx.Samples— basic / multiple sheets / formulas + freeze / styledPragmatic.Documents.Csv.Samples— roundtrip / locale / formula injection / edge casesPragmatic.Documents.Markup.Samples— simple markup / data-bound / batchPragmatic.Documents.Email.Samples— hero / columns / footer / markup-boundPragmatic.Documents.Templating.Spreadsheet.Samples— CSV / XLSX / in-memory data sources