Skip to content

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.


.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.

Pragmatic.Documents is a family of packages organised around three intentional concepts:

  1. Models — serialisable data structures that describe a document, an email, or a spreadsheet
  2. Renderers — pure functions that take a model and produce a byte stream
  3. 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)│
└──────────────┘

Pragmatic.Documents keeps three model families separate because their layout constraints are fundamentally different.

FamilyCore packageNatural outputsLayout unit
DocumentPragmatic.Documents.ModelPDF, DOCXPage (with margins, headers, footers)
EmailPragmatic.Email.ModelHTML emailSection (table-based, inline CSS)
SpreadsheetPragmatic.Documents.SpreadsheetXLSX, CSVSheet (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.

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();

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();

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();

PackageTypePurpose
Pragmatic.Documents.ModelDocumentModel, DocumentBuilder, node typesPage-oriented document data
Pragmatic.Email.ModelEmailModel, EmailBuilder, section typesHTML email data
Pragmatic.Documents.SpreadsheetSpreadsheetModel, SpreadsheetBuilder, Sheet, Row, CellTabular 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.

PackageInputOutputImplementation
Pragmatic.Documents.PdfDocumentModelPDF byte streamnative-backed
Pragmatic.Documents.DocxDocumentModelDOCX byte streammanaged OOXML
Pragmatic.Documents.XlsxSpreadsheetModelXLSX byte streammanaged OOXML
Pragmatic.Documents.CsvSpreadsheetModel or raw rowsCSV byte streammanaged
Pragmatic.Documents.EmailEmailModelHTML stringmanaged

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).

PackageCapability
Pragmatic.Documents.Xlsx.XlsxReaderRead(Stream) → SpreadsheetModel
Pragmatic.Documents.Csv.CsvReaderReadAsModel(Stream, sheetName) → SpreadsheetModel

Renderers and readers share the same model, so roundtrips are idempotent for the model features the format supports.

PackagePurpose
Pragmatic.Documents.TemplatesDocumentTemplate — a DocumentModel with placeholders
Pragmatic.Email.TemplatesEmailTemplate — an EmailModel with placeholders
Pragmatic.Documents.TemplatingExpression engine, pipe registry, data context resolver
Pragmatic.Documents.Templating.I18NDate / currency / percent pipes + translation integration
Pragmatic.Documents.Templating.SpreadsheetData-source catalogue for spreadsheet workflows
Pragmatic.Documents.MarkupPdxDocParser (PDX-Doc) and PdxEmailParser (PDX-Email)
PackagePurpose
Pragmatic.Documents.OoxmlShared OOXML writer used by Docx and Xlsx (zip + content-types + relationships)
Pragmatic.Documents.Csv.GeneratorAOT-safe typed CSV source generator ([CsvSerializable<T>])

Every output follows the same three-stage pipeline:

Build or parse → Model → Render
  • 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 / .pdxemail file into a template. Best when authors (designers, content teams) own the layout.

All three paths land on a fully-typed, immutable model. You can:

  • serialise it with DocumentSerializer / EmailSerializer for caching or wire transfer
  • inspect/modify it before rendering
  • attach resources (fonts, embedded images) via renderer-specific resources objects

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 stream
using 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 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.


SituationStart 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>]

  • 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 deliveryPragmatic.Documents.Email produces HTML; send it with Pragmatic.Email or 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.


  1. getting-started.md — five concrete scenarios, each ~5 minutes
  2. pdf-rendering.md — the PDF-specific details (resources, options, quirks)
  3. docx-rendering.md — DOCX-specific details (TOC auto-update, headers/footers)
  4. xlsx-rendering.md — XLSX formulas, styling, freeze panes, merges
  5. csv-io.md — CSV read/write, locale, formula injection
  6. email-rendering.md — email client compatibility, preheader, sections
  7. markup-parser.md — PDX-Doc and PDX-Email syntax reference
  8. templating.md — expressions, pipes, data context, custom pipe registration
  9. common-mistakes.md / troubleshooting.md