Common Mistakes
Patterns that look reasonable but don’t fit the module’s design. Read before you fight the API.
1. Treating the folder name as a package name
Section titled “1. Treating the folder name as a package name”Pragmatic.Documents is the umbrella module. You install the specific runtime package you need:
Pragmatic.Documents.Pdffor PDF outputPragmatic.Documents.Docxfor DOCXPragmatic.Documents.Xlsxfor XLSXPragmatic.Documents.Csvfor CSVPragmatic.Documents.Emailfor HTML email
dotnet add package Pragmatic.Documents will fail — there is no package with that exact name.
2. Mixing DocumentModel and EmailModel
Section titled “2. Mixing DocumentModel and EmailModel”These are intentionally different models. HTML email has constraints that page layout doesn’t share (inline CSS, table-based layout, 600-800px width, preheader text, MSO conditional sections). Forcing one into the other produces output that renders badly in email clients.
// ❌ Building a DocumentModel, rendering as emailvar doc = new DocumentBuilder().Page(p => p.Heading("Hi")).Build();new EmailHtmlRenderer().Render(doc); // no — wrong model type
// ✅ Use EmailModel for emailvar email = new EmailBuilder().Section(s => s.Column(c => c.Heading("Hi"))).Build();new EmailHtmlRenderer().Render(email);3. Using DocumentModel for tabular exports
Section titled “3. Using DocumentModel for tabular exports”If the deliverable is “a table the user opens in Excel”, use SpreadsheetModel + XLSX. DocumentModel is for page-oriented output. A table in a PDF is a fine use case; a table as the entire payload isn’t.
// ❌ Hundreds of rows in a DocumentBuilder.Table(...).Page(p => p.Table(t => { foreach (var row in rows) t.Row(...); // one 10,000-row table in a PDF}))
// ✅var book = new SpreadsheetBuilder() .Sheet("Data", s => { s.HeaderRow("A", "B"); foreach (var r in rows) s.Row(r.A, r.B); }) .Build();XlsxRenderer.RenderTo(fs, book);4. Forgetting RenderTo(Stream) for large outputs
Section titled “4. Forgetting RenderTo(Stream) for large outputs”Render(model) allocates a full byte[]. For multi-megabyte PDFs or XLSX workbooks, stream instead:
// ❌ 50MB PDF — pays the allocation twice (model + byte array)var bytes = PdfRenderer.Render(doc);File.WriteAllBytes("big.pdf", bytes);
// ✅using var fs = File.Create("big.pdf");PdfRenderer.RenderTo(fs, doc);5. Expecting CSV to round-trip multi-sheet workbooks
Section titled “5. Expecting CSV to round-trip multi-sheet workbooks”CSV is a single flat table. If your SpreadsheetModel has multiple sheets and you call CsvWriter.Write(book), only the first sheet is written. Iterate yourself or switch to XLSX.
// Explicit, intent-clear alternativeforeach (var sheet in book.Sheets){ using var fs = File.Create($"{sheet.Name}.csv"); CsvWriter.Write(fs, sheet);}6. CSV for data you plan to open in Excel — without formula hardening
Section titled “6. CSV for data you plan to open in Excel — without formula hardening”Excel executes leading =, +, -, @ as formulas. If any cell value contains untrusted input and happens to start with one of those characters, you’ve shipped a formula-injection vector.
// ✅ Harden when the CSV goes to an untrusted viewervar options = new CsvOptions { HardenAgainstFormulaInjection = true };CsvWriter.Write(fs, book, options);Do this for every CSV with user-supplied content.
7. Treating template expressions as C# code
Section titled “7. Treating template expressions as C# code”{{ expr }} is a small expression language, not C#. It supports property access, array indexing, and pipes. It does not support:
- arbitrary method calls (
invoice.GetTotal()) - conditionals as expressions (
total > 0 ? "yes" : "no") - arithmetic (
price * 1.22)
If you need logic, do it in the data source:
// ❌ tried {{ price * 1.22 | currency:"EUR" }} — won't work// ✅ compute in datadata.AddSource("invoice", new { subtotal = 6000m, vat = 6000m * 0.22m, total = 6000m * 1.22m });Then bind {{ invoice.total | currency:"EUR" }}.
8. Missing data in templates surfaces as empty string, not an error
Section titled “8. Missing data in templates surfaces as empty string, not an error”By design, {{ customer.nonexistent }} renders as nothing rather than throwing. This keeps templates robust in the face of optional fields, but it hides typos.
Always check resolver.Warnings after ResolveAsync:
var model = await resolver.ResolveAsync(template, data);if (resolver.Warnings.Count > 0) logger.LogWarning("Template warnings: {@Warnings}", resolver.Warnings);Or use | default:"…" to make fallbacks explicit:
<text>Hello, {{ customer.firstName | default:"there" }}!</text>9. Assuming Heading level controls font size in DOCX
Section titled “9. Assuming Heading level controls font size in DOCX”In DOCX, Heading(level: 1..9) maps to the Heading1..Heading9 built-in paragraph style. The actual font size comes from the theme (DocxTheme or the viewer’s defaults), not from a hard-coded value.
If you want a specific font size, use a TextNode with explicit NodeStyle.FontSize, not a heading.
10. Treating TocNode as a static table
Section titled “10. Treating TocNode as a static table”TocNode in both PDF and DOCX is a field. Content viewers populate it when they open the file. If you inspect the raw DOCX XML and the TOC body looks empty, that’s expected — Word fills it in.
For PDF the TOC is pre-populated with heading bookmarks at render time, so it’s correct immediately.
11. Embedding fonts when they’re already installed
Section titled “11. Embedding fonts when they’re already installed”You only need to ship a font with the PDF/DOCX when the viewer might not have it. For system fonts (Arial, Times New Roman, Helvetica), don’t embed — save the bytes.
var resources = new PdfResources();resources.AddFont("Acme Serif", File.ReadAllBytes("fonts/AcmeSerif.ttf")); // custom — embed// Arial is everywhere — don't add it, just reference it by name12. Markup parser expects XML, not Markdown
Section titled “12. Markup parser expects XML, not Markdown”PDX-Doc looks vaguely HTML-ish because it’s XML-based. Markdown-style (# Heading, **bold**, | a | b |) does not parse.
<!-- ✅ --><heading level="1">Title</heading><text style="bold">Important</text>
<!-- ❌ --># Title**Important**13. Sending HTML email without a plain-text alternative
Section titled “13. Sending HTML email without a plain-text alternative”Some clients show only the text part; spam filters score higher when both parts exist; accessibility tools prefer plain text.
var html = new EmailHtmlRenderer().Render(email);var text = StripToPlainText(html); // implement or use a library
await smtpClient.SendAsync(new Message { HtmlBody = html, TextBody = text });Related
Section titled “Related”- troubleshooting.md — runtime errors and their fixes
- concepts.md — the architecture rationale