Skip to content

Troubleshooting

Runtime errors and their fixes. Organised by symptom.


”Unable to load DLL ‘Pragmatic.Pdf.Native.dll’” at startup

Section titled “”Unable to load DLL ‘Pragmatic.Pdf.Native.dll’” at startup”

The PDF renderer is native-backed. The native binary ships in the NuGet under runtimes/{rid}/native/. If it isn’t resolved:

  1. Verify the RID is supported. Currently win-x64, linux-x64, osx-arm64. For other platforms, open an issue.
  2. Check your publish output. For self-contained deployments the native binary must be next to the app (dotnet publish -r win-x64 --self-contained).
  3. Check Docker images. Pragmatic.Documents.Pdf needs a base image with glibc on Linux — alpine (musl) is not supported today.
  • Did you call .Page(p => ...)? A DocumentModel with zero pages produces a 0-page PDF.
  • Did every page builder add at least one node? An empty .Page(p => { }) emits an empty page, which some viewers elide.

Fonts fall back to a default (Helvetica, Arial)

Section titled “Fonts fall back to a default (Helvetica, Arial)”

The renderer couldn’t find the requested font family. Options:

  • Use the default fonts (Helvetica/Times/Courier) which are embedded in every PDF viewer
  • Register a custom font via PdfResources.AddFont(name, bytes) and reference it in NodeStyle.FontFamily
  • Embedded images are stored as-is. Compress them before AddImage if they’re larger than necessary.
  • Embedded fonts add size — every font is shipped inside the PDF. Use system fonts when portability isn’t a concern.

The TOC is a field. Press F9 in Word, or pass UpdateFieldsOnOpen = true in DocxRenderOptions so Word refreshes automatically:

var options = new DocxRenderOptions { UpdateFieldsOnOpen = true };
new DocxRenderer().RenderTo(fs, doc, options: options);

DOCX heading levels map to the document’s Heading1..Heading9 paragraph styles. If the target Word template has different styles for those names (or no styles at all), appearance will vary.

Force visual styles by combining a heading + a styled TextNode:

.Heading("Title", level: 1) // sets the outline level (for TOC)
.Paragraph(new TextNode { // sets the visual appearance
Content = "Title",
Style = new NodeStyle { FontSize = 28, FontWeight = FontWeight.Bold, Color = "#1F497D" }
})

Or pass a DocxTheme to customise the built-in style definitions.

The default hyperlink style in DOCX is defined by the template (styles.xml). The renderer emits the hyperlink correctly — if it shows up as black text, the consuming template has overridden the Hyperlink style.


Formulas show as text (=SUM(...) instead of the computed value)

Section titled “Formulas show as text (=SUM(...) instead of the computed value)”
  • You used .Row("=SUM(A2:A10)") instead of .FormulaRow(...). Plain .Row(value) writes the string literally.
  • Use .FormulaRow("=SUM(A2:A10)") or build a Cell { Formula = "..." }.
  • Freeze(rows, columns) must be called before any rows — the freeze configuration is per-sheet metadata, not per-row.
  • Merge("A1", "C1") coordinates must be valid cell references; malformed references throw an ArgumentException at render time.

Opened XLSX has lost formatting applied in Excel

Section titled “Opened XLSX has lost formatting applied in Excel”

If you read → modify → write a file that used features the model doesn’t represent (pivot tables, conditional formatting, charts), those are dropped on read. Keep the original file and apply modifications via a dedicated XLSX tool if you need to preserve them.


The writer quotes cells that contain the delimiter, ", or newline. If you’re seeing split cells in the output, inspect the actual bytes:

"hello, world","regular" ✅ quoted correctly
hello, world,regular ❌ malformed — shouldn't happen from CsvWriter

If CsvWriter produced unquoted output, file an issue with the input.

Excel’s CSV auto-detection picks the delimiter based on the system locale. An Italian Excel expects ; and treats ,-separated CSV as “all one column”.

Export with the locale-appropriate delimiter:

var italian = new CsvOptions { Delimiter = ';', DecimalSeparator = ',' };
CsvWriter.Write(fs, book, italian);

UTF-8 without BOM is the default. Some Excel versions misinterpret UTF-8 without BOM. Force a BOM for broader compatibility:

var options = new CsvOptions { WriteByteOrderMark = true };

Outlook (desktop) renders the email with the wrong width

Section titled “Outlook (desktop) renders the email with the wrong width”
  • Ensure Width is set (new EmailBuilder().Width(600)); Outlook respects the MSO-conditional wrapper table the renderer emits.
  • Don’t replace the output’s wrapper table by post-processing the HTML — it’s load-bearing for Outlook.

The preheader is hidden text at the top of the body; some clients with unusual CSS overrides may still show it. This is a known email-client quirk across the industry. You can reduce the preheader text to 1-2 words as a workaround.

Gmail, Outlook, and most mobile clients block remote images until the user clicks “show images”. Always set alt text and make sure the email reads without images. The renderer emits alt from Image(src, alt, ...).


MarkupParseException: Root element must be <document>

Section titled “MarkupParseException: Root element must be <document>”

The PDX-Doc parser expects <document> as the root. If you have a different root, or no XML declaration, re-author with the right wrapper:

<document title="...">
<page>
<!-- ... -->
</page>
</document>

PDX uses well-formed XML. Stray < characters, unclosed elements, unescaped & all break parsing. Escape entity characters inside text:

<text>Q&amp;A session</text> <!-- not "Q&A session" -->

Expressions render literally as {{ foo.bar }}

Section titled “Expressions render literally as {{ foo.bar }}”

You parsed the markup into a DocumentTemplate but didn’t resolve it with data. Templates must go through a resolver:

var template = PdxDocParser.Parse(markup);
var resolver = new DocumentTemplateResolver(PipeRegistry.Default.WithI18N());
var model = await resolver.ResolveAsync(template, data); // ← this step
PdfRenderer.RenderTo(fs, model);

Intentional: missing properties render as null. Use | default:"..." for fallbacks, and inspect resolver.Warnings to see which expressions couldn’t resolve.

PipeRegistry.Default has 5 core pipes (upper, lower, trim, default, number). Currency, date, percent, translate need I18N:

var resolver = new DocumentTemplateResolver(PipeRegistry.Default.WithI18N());

For custom pipes, register explicitly:

var pipes = PipeRegistry.Default.WithI18N().With(new TruncatePipe());

Pipes honour the data context’s culture. If you forgot to set one, they use CultureInfo.CurrentCulture — which in a server process may be en-US.

var data = new TemplateDataContext(CultureInfo.GetCultureInfo("it-IT"))
.AddSource("invoice", invoice);

You probably installed Pragmatic.Documents.Model + a renderer but also took a transitive reference via another Pragmatic package. Run dotnet list package --include-transitive and ensure only one version of each model package is in the graph. Central Package Management (Directory.Packages.props) eliminates this category of issue.

The renderers are deterministic given the same input. If byte-for-byte comparison fails, check:

  • Embedded creation date metadata (set a fixed DateTime via the builder’s metadata API when snapshot-testing)
  • Image compression — if you load an image from disk, ensure the bytes haven’t changed between runs
  • Font-resource enumeration order — register fonts in a deterministic order

  • Check the corresponding samples/ project — it’s a minimal, runnable demonstration
  • Inspect the generated obj/Generated/ folder if your issue might be source-generator related
  • Open an issue with a reproducer — include the model (serialise with DocumentSerializer.Serialize) and the exact renderer / options used