Architecture and Core Concepts
This guide explains why Pragmatic.Documents is split the way it is, how the packages fit together, and how to choose the right entry point for each use case.
The Problem
Section titled “The Problem”.NET document generation typically means one of three things:
- A large closed-source library (iText, Aspose, Syncfusion) that does everything but is expensive, carries unclear licensing for commercial products, or drags a large native dependency
- A managed renderer for a single format (OpenXml SDK for DOCX, QuestPDF for PDF, ClosedXML for XLSX) — you end up with 4 different APIs for 4 different outputs
- A templating engine (Razor, Liquid, Handlebars) that covers binding but leaves actual rendering to you
Each path solves part of the problem. None of them gives you a consistent model-driven workflow across PDF, DOCX, XLSX, CSV, and HTML email.
The Approach
Section titled “The Approach”Pragmatic.Documents is a family of packages organised around three intentional concepts:
- Models — serialisable data structures that describe a document, an email, or a spreadsheet
- Renderers — pure functions that take a model and produce a byte stream
- Templates and markup — optional layer that builds models from external definitions bound to data
Each layer is independent. You can use the builders alone, or plug the markup parser and templating engine in front of them.
┌──────────────┐│ Markup │ optional: author-friendly DSL (PDX-Doc, PDX-Email)│ (.pdxdoc) │└──────┬───────┘ │ parse ▼┌──────────────┐│ Template │ optional: binds to data via pipes and expressions└──────┬───────┘ │ resolve(data) ▼┌──────────────┐│ Model │ always: DocumentModel / EmailModel / SpreadsheetModel│ (sealed rec) │└──────┬───────┘ │ render ▼┌──────────────┐│ Renderer │ PDF, DOCX, XLSX, CSV, HTML email│ (byte stream)│└──────────────┘Three model families
Section titled “Three model families”Pragmatic.Documents keeps three model families separate because their layout constraints are fundamentally different.
| Family | Core package | Natural outputs | Layout unit |
|---|---|---|---|
| Document | Pragmatic.Documents.Model | PDF, DOCX | Page (with margins, headers, footers) |
Pragmatic.Email.Model | HTML email | Section (table-based, inline CSS) | |
| Spreadsheet | Pragmatic.Documents.Spreadsheet | XLSX, CSV | Sheet (rows × columns, formulas, merges) |
Forcing all three into a single abstraction would either lose format-specific features (merged cells in spreadsheets, preheader text in emails, page breaks in documents) or expose every concept in every context. The split is a design choice, not an oversight.
Document model
Section titled “Document model”DocumentModel represents page-oriented output. Every page has a set of nodes (Heading, Paragraph, Table, Image, Barcode, TOC, PageBreak, Footnote, …) plus optional header/footer templates. The same model renders to both PDF and DOCX.
var doc = new DocumentBuilder() .Title("Invoice 2026-001") .Author("Pragmatic") .Size(PageSize.A4) .Page(p => p .Heading("Invoice", level: 1) .Paragraph(new TextNode { Content = "Thank you for your business." }) .Table(t => t .HeaderRow("Item", "Qty", "Price") .Row("Design review", "1", "250.00"))) .Build();Email model
Section titled “Email model”EmailModel represents an HTML email — table-based layout, inline CSS, preheader, width constraints. The renderer emits email-client-safe HTML (MSO-compatible tables, no external CSS).
var email = new EmailBuilder() .Subject("Welcome to Pragmatic") .Preheader("Your account is ready") .Width(600) .Section(s => s .Heading("Welcome!") .Paragraph("Click below to verify your email.")) .Build();Spreadsheet model
Section titled “Spreadsheet model”SpreadsheetModel represents tabular data across one or more sheets, with support for column widths, row freezing, cell merges, formulas, and per-cell styling. The same model renders to both XLSX and CSV (CSV flattens formulas and drops styling).
var book = new SpreadsheetBuilder() .Title("Guest Export") .Sheet("Guests", s => s .HeaderRow("Id", "First", "Last") .Row(1, "Ada", "Lovelace") .Row(2, "Grace", "Hopper")) .Build();Package map
Section titled “Package map”Models (zero-dep data types)
Section titled “Models (zero-dep data types)”| Package | Type | Purpose |
|---|---|---|
Pragmatic.Documents.Model | DocumentModel, DocumentBuilder, node types | Page-oriented document data |
Pragmatic.Email.Model | EmailModel, EmailBuilder, section types | HTML email data |
Pragmatic.Documents.Spreadsheet | SpreadsheetModel, SpreadsheetBuilder, Sheet, Row, Cell | Tabular data |
Models are pure records. They serialise to JSON out of the box, which makes them easy to cache, snapshot, or send over a wire.
Renderers (model → bytes)
Section titled “Renderers (model → bytes)”| Package | Input | Output | Implementation |
|---|---|---|---|
Pragmatic.Documents.Pdf | DocumentModel | PDF byte stream | native-backed |
Pragmatic.Documents.Docx | DocumentModel | DOCX byte stream | managed OOXML |
Pragmatic.Documents.Xlsx | SpreadsheetModel | XLSX byte stream | managed OOXML |
Pragmatic.Documents.Csv | SpreadsheetModel or raw rows | CSV byte stream | managed |
Pragmatic.Documents.Email | EmailModel | HTML string | managed |
Each renderer exposes a RenderTo(Stream, Model, ...) static method. PDF also accepts an optional PdfResources (fonts, embedded assets); DOCX accepts DocxResources and DocxRenderOptions (page numbering, TOC behaviour).
Readers
Section titled “Readers”| Package | Capability |
|---|---|
Pragmatic.Documents.Xlsx.XlsxReader | Read(Stream) → SpreadsheetModel |
Pragmatic.Documents.Csv.CsvReader | ReadAsModel(Stream, sheetName) → SpreadsheetModel |
Renderers and readers share the same model, so roundtrips are idempotent for the model features the format supports.
Templates and markup
Section titled “Templates and markup”| Package | Purpose |
|---|---|
Pragmatic.Documents.Templates | DocumentTemplate — a DocumentModel with placeholders |
Pragmatic.Email.Templates | EmailTemplate — an EmailModel with placeholders |
Pragmatic.Documents.Templating | Expression engine, pipe registry, data context resolver |
Pragmatic.Documents.Templating.I18N | Date / currency / percent pipes + translation integration |
Pragmatic.Documents.Templating.Spreadsheet | Data-source catalogue for spreadsheet workflows |
Pragmatic.Documents.Markup | PdxDocParser (PDX-Doc) and PdxEmailParser (PDX-Email) |
Shared infrastructure
Section titled “Shared infrastructure”| Package | Purpose |
|---|---|
Pragmatic.Documents.Ooxml | Shared OOXML writer used by Docx and Xlsx (zip + content-types + relationships) |
Pragmatic.Documents.Csv.Generator | AOT-safe typed CSV source generator ([CsvSerializable<T>]) |
Rendering pipeline
Section titled “Rendering pipeline”Every output follows the same three-stage pipeline:
Build or parse → Model → RenderStage 1 — build or parse
Section titled “Stage 1 — build or parse”- Builder: imperative C# API (
DocumentBuilder,EmailBuilder,SpreadsheetBuilder). Best when you have full control and want strongly-typed code paths. - Markup parser:
PdxDocParser.Parse(markup)/PdxEmailParser.Parse(markup)reads a.pdxdoc/.pdxemailfile into a template. Best when authors (designers, content teams) own the layout.
Stage 2 — model
Section titled “Stage 2 — model”All three paths land on a fully-typed, immutable model. You can:
- serialise it with
DocumentSerializer/EmailSerializerfor caching or wire transfer - inspect/modify it before rendering
- attach resources (fonts, embedded images) via renderer-specific resources objects
Stage 3 — render
Section titled “Stage 3 — render”Renderers are pure functions: same model → same output bytes. No hidden state, no ambient environment.
// In-memory: byte[]var pdfBytes = PdfRenderer.Render(model);
// Streaming: writes directly to an open streamusing var fs = File.Create("invoice.pdf");PdfRenderer.RenderTo(fs, model);The streaming API is preferred for large outputs (reports with thousands of rows) to avoid materialising the whole byte array in memory.
Templates and data binding
Section titled “Templates and data binding”Templates are a thin layer on top of models that adds:
- placeholders (
{{customer.name}}) - loops (
{% for item in items %}...{% endfor %}) - conditionals (
{% if total > 0 %}...{% endif %}) - pipes for formatting (
{{price | currency:'EUR'}},{{date | format:'yyyy-MM-dd'}})
When you resolve a template with a data context, you get back a regular DocumentModel, EmailModel, or equivalent — so rendering proceeds exactly as it would for hand-built models.
See templating.md and markup-parser.md for the full syntax.
When to use what
Section titled “When to use what”| Situation | Start with |
|---|---|
| ”I need a PDF from C# with full layout control” | DocumentBuilder + Pragmatic.Documents.Pdf |
| ”Same content, two outputs (PDF + DOCX)“ | DocumentBuilder + both renderers |
| ”Tabular export from a query result” | SpreadsheetBuilder + Xlsx (or Csv for plain CSV) |
| “Transactional email (verification, reset, invoice)“ | EmailBuilder + Pragmatic.Documents.Email |
”Designers author .pdxdoc templates, runtime binds data” | Pragmatic.Documents.Markup + Pragmatic.Documents.Templating + the right renderer |
| ”I have CSV fixtures I want to round-trip through a model” | CsvReader.ReadAsModel + CsvWriter.Write |
| ”AOT app that serialises typed CSV rows” | Pragmatic.Documents.Csv.Generator with [CsvSerializable<T>] |
What this module does not do
Section titled “What this module does not do”- No reporting designer (no WYSIWYG authoring tool) — templates are text files.
- No form filling — the PDF renderer emits, it does not fill existing PDFs.
- No digital signatures — a PDF signing step is out of scope for this package.
- No email delivery —
Pragmatic.Documents.Emailproduces HTML; send it withPragmatic.Emailor your SMTP library of choice. - No chart rendering — embed pre-rendered images, or generate SVG/PNG separately.
These are deliberate boundaries. The module focuses on being a small, composable, model-driven rendering layer.
Reading order
Section titled “Reading order”- getting-started.md — five concrete scenarios, each ~5 minutes
- pdf-rendering.md — the PDF-specific details (resources, options, quirks)
- docx-rendering.md — DOCX-specific details (TOC auto-update, headers/footers)
- xlsx-rendering.md — XLSX formulas, styling, freeze panes, merges
- csv-io.md — CSV read/write, locale, formula injection
- email-rendering.md — email client compatibility, preheader, sections
- markup-parser.md — PDX-Doc and PDX-Email syntax reference
- templating.md — expressions, pipes, data context, custom pipe registration
- common-mistakes.md / troubleshooting.md