DocxTemplater.Text 3.0.0-beta.4

This is a prerelease version of DocxTemplater.Text.
dotnet add package DocxTemplater.Text --version 3.0.0-beta.4
                    
NuGet\Install-Package DocxTemplater.Text -Version 3.0.0-beta.4
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="DocxTemplater.Text" Version="3.0.0-beta.4" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="DocxTemplater.Text" Version="3.0.0-beta.4" />
                    
Directory.Packages.props
<PackageReference Include="DocxTemplater.Text" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add DocxTemplater.Text --version 3.0.0-beta.4
                    
#r "nuget: DocxTemplater.Text, 3.0.0-beta.4"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package DocxTemplater.Text@3.0.0-beta.4
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=DocxTemplater.Text&version=3.0.0-beta.4&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=DocxTemplater.Text&version=3.0.0-beta.4&prerelease
                    
Install as a Cake Tool

DocxTemplater

DocxTemplater is a library to generate docx documents from a docx template. The template can be bound to multiple datasources and be edited by non-programmers. It supports placeholder replacement, loops, and images.

NuGet MIT CI-Build

Quickstart

Create a docx template with placeholder syntax:

This Text: {{ds.Title}} - will be replaced

Open the template, add a model, and store the result to a file:

var template = DocxTemplate.Open("template.docx");
// To open the file from a stream use the constructor directly 
// var template = new DocxTemplate(stream);
template.BindModel("ds", new { Title = "Some Text" });
template.Save("generated.docx");

The generated word document will contain:

This Text: Some Text - will be replaced

Install DocxTemplater via NuGet

To include DocxTemplater in your project, you can install it directly from NuGet.

Run the following command in the Package Manager Console:

PM> Install-Package DocxTemplater
Additional Extension Packages

Enhance DocxTemplater with these optional extension packages:

Package Description
DocxTemplater.Images Enables embedding images in generated Word documents
DocxTemplater.Markdown Allows use of Markdown syntax for generating parts of Word documents
DocxTemplater.Text Renders plain text and HTML templates (e.g. e-mails) with the same syntax, see Text and HTML Templates
DocxTemplater.Localization.<language> Error messages in the user's language, see Localized Error Messages. Available: de, de-CH, fr, it

Image metadata (size, format, EXIF rotation) is read by a dependency-free built-in reader that supports PNG, JPEG, GIF, BMP and TIFF. If you need another image library for metadata detection, implement IImageMetadataReader and pass it to the formatter:

using DocxTemplater.Images;

settings.RegisterFormatter(new ImageFormatter(new MyImageMetadataReader()));

Migration note: the DocxTemplater.Images.ImageSharp package is discontinued. ImageFormatter() works without it. If you relied on ImageSharp for metadata detection, implement IImageMetadataReader with ImageSharp in your own project.

Table of Contents

Features

  • Variable Replacement
  • Collections - Bind to collections
  • Conditional Blocks
  • Images - Replace placeholder with Image data
  • Chart Data Binding - Bind a chart to a data source
  • Markdown Support - Converts Markdown to OpenXML
  • HTML Snippets - Replace placeholder with HTML Content
  • Dynamic Tables - Columns are defined by the datasource
  • Content Controls - Fill Word content controls from the model, addressed by their tag
  • Template Schema - Statically inspect which variables a template expects, without rendering
  • Syntax Validation - Check a template for syntax errors without rendering it
  • Localized Error Messages - Every error carries a code; messages are available in English, German, Swiss German, French and Italian and can be extended
  • Text and HTML Templates - Render e-mails and other text with the same template syntax, see DocxTemplater.Text

Placeholder Syntax

A placeholder can consist of three parts: {{property:formatter(arguments)}}

  • property: The path to the property in the datasource objects.
  • formatter: Formatter applied to convert the model value to OpenXML (e.g., toupper, tolower, img format).
  • arguments: Formatter arguments - some formatters have arguments.

The syntax is case insensitive. This includes property paths, the model prefixes passed to BindModel and the loop variables - {{customerDetails.Name}} resolves a model bound as BindModel("CustomerDetails", ...). Because prefixes are matched case-insensitively, two models whose prefixes differ only in casing cannot be bound at the same time.

Quick Reference Examples

| Syntax | Description | | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | | {{SomeVar}} | Simple Variable replacement. | | {?{someVar > 5}}...{{:}}...{{/}} | Conditional blocks. | | {{#Items}}...{{Items.Name}} ... {{/Items}} | Text block bound to collection of complex items. | | {{#Items}}...{{.Name}} ... {{/Items}} | Same as above with dot notation - implicit iterator. | | {{#Items}}...{{.}:toUpper} ... {{/Items}} | A list of string all upper case - dot notation. | | {{#Items}}{{.}}{{:s:}},{{/Items}} | A list of strings comma separated - dot notation. | | {{SomeString}:ToUpper()} | Variable with formatter to upper. | | {{SomeDate}:Format('MM/dd/yyyy')} | Date variable with formatting. | | {?{!.IsHw && .Name.Contains('Item')}}...{{}} | Logical Expression with string operation. Careful; Word Replaces ' with ‘ and " with ”. | | {{SomeDate}:F('MM/dd/yyyy')} | Date variable with formatting - short syntax. | | {{(1 + 2)}} | Evaluate simple math expressions. | | {{(ds.Name.ToUpper() + "!")}} | Expressions with variables and string operations. | | {{(ds.Val?.ToString() ?? "N/A")}} | Expressions with null-conditional and coalescing operators. | | {{(ds.Price * 1.19)}:f(c)} | Expressions with calculations and formatters. | | {{(ds.Items[0].Name)}} | Expressions with array / list / dictionary index access. | | {{SomeBytes}:img()} | Image Formatter for image data. | | {{SomeHtmlString}:html()} | Inserts HTML string into the word document. | | {{ds}:template('ds.SubDocument')} | Inserts another docx document (or OpenXML fragment) at the placeholder position. | | {{@i:ItemCount}}...{{i}}...{{/}} | Range loop that repeats its content ItemCount times. | | {{@i:3}}...{{i}}...{{/}} | Range loop with a fixed count; i takes the values 0, 1, 2. | | {{#Items}}{?{Items._Idx % 2 == 0}}{{.}}{{/}}{{/Items}} | Renders every second item in a list. | | {{#switch: SomeVar}}{{#case: 'A'}}...{{/}}{{#default}}...{{/}}{{/}} | Evaluates switch cases and renders the matching block. there is a short syntax too | | {{:ignore}} ... {{/:ignore}} | Ignore DocxTemplater syntax, which is helpful around a Table of Contents. | | {{:break}} | Insert a line break after this keyword block. | | {{:PageBreak}} | Start a new page after this keyword block. | | {{:SectionBreak}} | Start a new "Section Break" on the next page after this keyword block. |

Collections

To repeat document content for each item in a collection, use the loop syntax: {{#<collection>}} ... content ... {{</collection>}}

All document content between the start and end tag is rendered for each element in the collection:

{{#Items}} This text {{Items.Name}} is rendered for each element in the items collection {{/Items}}

This can be used, for example, to bind a collection to a table. In this case, the start and end tag have to be placed in the row of the table: | Name | Position | |--------------|-----------| | {{#Items}} {{Items.Name}} | {{Items.Position}} {{/Items}} |

This template bound to a model:

var template = DocxTemplate.Open("template.docx");
var model = new
{
    Items = new[]
    {
        new { Name = "John", Position = "Developer" },
        new { Name = "Alice", Position = "CEO" }
    }
};
template.BindModel("ds", model);
template.Save("generated.docx");

Will render a table row for each item in the collection: | Name | Position | |-------|-----------| | John | Developer | | Alice | CEO |

Shortcut for Dot Notation - accessing the current item

To access the current item in the collection, use the dot notation {{.}}:

{{#Items}} This text {{.Name}} is rendered for each element in the items collection {{/Items}}

To access the outer item in a nested collection, use the dot notation {{..}} This is useful when you have nested collections and want to access a property from the outer scope:

{{#Items}} This text {{..SomePropertyFromTheOuterScope}} is rendered for each element in the items collection {{/Items}}
Accessing the Index of the Current Item

To access the index of the current item, use the special variable Items._Idx In this example, the collection is called "Items".


Range Loops

To repeat document content a specific number of times based on an integer count or the length of a collection without directly iterating over it, use the range loop syntax: {{@i:count}} ... content ... {{/}}

Here, count can be an integer literal (e.g. {{@i:3}}), a model property holding an integer, a string parseable to an integer, or an IEnumerable (in which case its count is used). The variable i is the zero-based index of the current iteration: {{@i:3}} renders its content three times with i set to 0, 1 and 2. If you omit the index variable name (e.g. {{@count}}), it defaults to Index.


Separator

To render a separator between the items in the collection, use the separator syntax:

{{#Items}} This text {{.Name}} is rendered for each element in the items collection {{:s:}} This is rendered between each element {{/Items}}

Chart Data binding

Charts can be fully styled within the template, and a data source can then be bound to each chart. To bind a chart to a data source, the chart’s title in the template must match the property name in the model. MyChart alt text

To bind the chart correctly, the corresponding model property must be of th ChartData type.

Supported chart types: bar, 3-D bar, pie, 3-D pie and doughnut charts. Pie and 3-D pie show the first series only; doughnut charts show all series. Per-slice colors defined in the pie template are preserved; slices without a defined color are colored automatically.

            using var fileStream = File.OpenRead("MyTemplate.docx");
            var docTemplate = new DocxTemplate(fileStream);
            var model = new
            {
                MyChart = new ChartData()
                {
                    ChartTitle = "Foo 2",
                    Categories = ["Cat1", "Cat2", "Cat3", "Cat4", "Cat5"],
                    Series =
                    [
                        new() {Name = "serie 1", Values = [2200.0, 5500.0, 4600.25, 9560.56],},
                        new() {Name = "serie 2", Values = [1200.0, 2500.0, 8600.25, 4560.56],},
                    ]
                }
            };

            docTemplate.BindModel("ds", model);
            var resultStream = docTemplate.Process();

Conditional Blocks

Show or hide a given section depending on a condition: {?{<condition>}} ... content ... {{/}}

All document content between the start and end tag is rendered only if the condition is met:

{?{Item.Value >= 0}}Only visible if value is >= 0
{{:}}Otherwise this text is shown{{/}}

Switch / Case Blocks

Show or hide a given section depending on a switch variable:

Long Syntax Short Syntax
{{#switch: Item.Value}}<br>  {{#case: 1}}Value is 1{{/}}<br>  {{#case: 'A'}}Value is A{{/}}<br>  {{#default}}Value is unknown{{/}}<br>{{/}} {{#s: Item.Value}}<br>  {{#c: 1}}Value is 1{{/}}<br>  {{#c: 'A'}}Value is A{{/}}<br>  {{#d}}Value is unknown{{/}}<br>{{/}}
Optional Closing Tags: A new Case or Default tag automatically closes any preceding open block. However, the switch itself must always be closed with {{/}}.

Enums: You can also use .ToString() to match enum properties against strings. For example, if Item.Day is DayOfWeek.Monday: {{#s: Item.Day.ToString()}} ... {{#c: 'Monday'}} Match ... {{/}}


C# Expressions

You can evaluate C# expressions directly inside placeholders. This is useful for simple calculations, string manipulations, or handling null values. Expressions must be enclosed in parentheses: {{(<expression>)}}.

Expressions are evaluated using DynamicExpresso and have access to all bound models.

Examples
  • Simple Math: {{(1 + 2)}} → 3
  • String Concatenation: {{(ds.Name.ToUpper() + "!")}} → WORLD! (if Name is "world")
  • Null Handling: {{(ds.Val?.ToString() ?? "N/A")}} → N/A (if Val is null)
  • Complex Logic: {{(ds.Items.Count > 0 ? "Items available" : "Empty")}}
  • Index Access: {{(ds.Items[0])}} → first element of a list, array or dictionary. Also works on method results, e.g. {{(ds.Qa.Split('|')[0])}}, and can be chained: {{(ds.Items[1].Name)}}.

Keep logic out of your templates. Expressions are handy for small calculations, but complex logic embedded in a document is hard to read, hard to test, and easy to break (Word also likes to rewrite quotes and operators). For clean, maintainable templates prefer exposing a property/getter with the logic on your model and bind to that instead:

// Instead of {{(ds.Items[0].FirstName + " " + ds.Items[0].LastName)}}
public string PrimaryContactName => Items.Count > 0
    ? $"{Items[0].FirstName} {Items[0].LastName}"
    : "n/a";

Then the template stays simple: {{ds.PrimaryContactName}}. The logic lives in C#, where it can be unit-tested and reused.

Using Formatters with Expressions

Expressions also support the standard formatter syntax: {{(<expression>)}:<formatter>(<args>)}. This is especially useful when you calculate values that require specific formatting.

Example: {{(ds.Price * 1.19)}:f(c)} → formats the calculated gross price as currency.

Security Note: Expressions are evaluated in a restricted environment. Only bound models and basic .NET types are accessible. Assignment operators are disabled to ensure models cannot be modified from within the template. System namespaces, file system access, or other sensitive operations are not permitted. This prevents malicious code injection through templates.


Formatters

If no formatter is specified, the model value is converted into a text with ToString.

This is not sufficient for all data types. That is why there are formatters that convert text or binary data into the desired representation.

The formatter name is always case insensitive.

String Formatters

  • ToUpper
  • ToLower

Default Value

default (alias ifempty) outputs a fallback text if the value is null, empty or whitespace; any other value is formatted as without a formatter (a DefaultFormatter of the model still applies). The text is literal - it is not evaluated as a template or expression. Unquoted text is split at its commas like any formatter argument and joined again with ", " (default(a,b) is a, b); quote it to keep it as written.

default and ifempty are built in and handled before the registered formatters: an own formatter with one of these names is no longer called.

{{ds.Salutation}:default('Dear Sir or Madam')}  ----> "Dear Sir or Madam" if Salutation is null or ""
{{ds.Phone}:ifempty(-)}

FormatPatterns

Any type that implements IFormattable can be formatted with the standard format strings for this type.

See:

Examples:

{{SomeDate}:format(d)}  ----> "6/15/2009"  (en-US)
{{SomeDouble}:format(f2)}  ----> "1234.42"  (en-US)

Image Formatter

NOTE: For the Image formatter, the NuGet package DocxTemplater.Images is required.

Because the image formatter is not standard, it must be registered in the settings:

var settings = new ProcessSettings();
settings.RegisterFormatter(new ImageFormatter());
var docTemplate = new DocxTemplate(fileStream, settings);

The Image Formatter replaces a placeholder with an image stored as a byte array.

The placeholder can be positioned in a TextBox, allowing end-users to adjust the image size easily within the template. The image will then automatically resize to match the dimensions of the TextBox.

Stretching Behavior

You can configure the image's stretching behavior as follows:

Argument Example Description
KEEPRATIO {{imgData}:img(keepratio)} Scales the image to fit the container while preserving the aspect ratio
STRETCHW {{imgData}:img(STRETCHW)} Scales the image to fit the container’s width
STRETCHH {{imgData}:img(STRETCHH)} Scales the image to fit the container’s height

If the image is not placed in a container, scaling can be applied using the w (width) or h (height) arguments. The r (rotate) argument can be used to rotate the image.

  • When only w or h is specified, the image scales to the specified width or height, maintaining its aspect ratio.
  • The size of the image can be specified in various units: cm, mm, in, px.
Argument Example Description
w {{imgData}:img(w:100mm)} Scales the image to a width of 100 mm, preserving aspect ratio
h {{imgData}:img(h:100in)} Scales the image to a height of 100 inches, preserving aspect ratio
r {{imgData}:img(r:90)} Rotates the image by 90 degrees
w,h {{imgData}:img(w:50px,h:20px)} Stretches the image to 50 x 20 pixels without preserving the aspect ratio

Markdown Formatter

The Markdown Formatter in DocxTemplater allows you to convert Markdown text into OpenXML elements, which can be included in your Word documents. This feature supports placeholder replacement within Markdown text and can handle various Markdown elements including tables, lists, and more.

NOTE: For the Markdown formatter, the NuGet package DocxTemplater.Markdown is required.

Because the markdown formatter is not standard, it must be registered in the settings:

var settings = new ProcessSettings();
settings.RegisterFormatter(new MarkDownFormatter());
var docTemplate = new DocxTemplate(fileStream, settings);
Usage

To use the Markdown formatter, you need to specify the md prefix and pass in the Markdown text as a string. Here is an example:

// Initialize the template
var template = DocxTemplate.Open("template.docx");
var markdown = """
                | Header 1 | Header 2 |
                |----------|----------|
                | Row 1 Col 1 | Row 1 Col 2 |
                | Row 2 Col 1 | Row 2 Col 2 |
                """;
// Bind model with Markdown content
template.BindModel("ds", new { MarkdownContent = markdown });

// Save the generated document
template.Save("generated.docx");

In your template, you would have a placeholder like this:

{{ds.MarkdownContent}:MD}
Template tags in the markdown value

By default the template tags in the rendered markdown are evaluated with the models of the document, so a markdown value can itself hold placeholders. If the value is user content, this is template injection: ACME {{ds.Secret}} typed into a description would print the secret. Insert such values with raw - the markdown is rendered as usual (emphasis, lists, line breaks, tables), its tags stay text:

{{ds.Description}:md(raw)}
{{ds.Description}:md(raw, ts:MyTableStyle)}

The application can set it per field, independent of the template author, with a default formatter on the model:

[ModelProperty(DefaultFormatter = "md(raw)")]
public string Description { get; set; }

Sub-Template Formatter - Inserting Documents

The sub-template formatter :template(...) (short form :T(...)) replaces a placeholder with the content of another docx document or an OpenXML fragment. It is registered by default - no additional setup is required.

The first argument is the path to the model property that holds the template to insert. The following types are supported:

Type Description
byte[] / Stream A complete docx document - its body content is inserted at the placeholder position
string An OpenXML fragment (paragraph, run or text)
OpenXmlCompositeElement An OpenXML element (paragraph, run, table row or table cell)
var template = DocxTemplate.Open("template.docx");
template.BindModel("ds", new
{
    Name = "John",
    SubDocument = File.ReadAllBytes("insert.docx") // byte[] or Stream
});
template.Save("generated.docx");

Placeholder in template.docx:

{{ds}:template('ds.SubDocument')}

The value in front of the formatter (here ds) is bound as ds inside the inserted document - so the inserted document can itself contain placeholders, which are resolved against that value. This also works inside loops, to render a sub-template once per item:

{{#ds.Items}}{{.}:T('ds.ItemTemplate')}{{/ds.Items}}

NOTE: The content is inserted by copying the OpenXML body elements into the host document. Styles, numbering definitions and images stored in the inserted document's own parts are not imported.

Inserting sub-template as Inline Content

When working with small reusable sub-templates, it may be desirable to insert their content directly into the target paragraph instead of generating a new paragraph.

This behavior can be enabled through ProcessSettings:

var docTemplate = new DocxTemplate(memStream, new ProcessSettings()
{
    InlineSubTemplates = true
});
var result = docTemplate.Process();

Inline insertion is particularly useful because it overcomes several limitations of the default sub-template insertion mode:

  • Avoids layout discrepancies caused by additional paragraphs (for example, inconsistent table row heights).
  • Allows sub-templates to be embedded seamlessly within existing text runs.
  • Preserves paragraph-level formatting defined in the parent template (alignment, spacing, indentation, etc.), which is generally what Word users expect.

To prevent formatting inherited from the insertion point from overriding the sub-template's appearance, explicitly define the properties that should remain fixed within the sub-template. This allows the same sub-template to be reused in different contexts while still inheriting the surrounding document (cascading) style where appropriate.

NOTE: When using this mode, DOCX sub-templates consisting of a single paragraph are also merged inline.


Content Controls

A Word content control (a structured document tag, w:sdt) can be filled from the model by putting a placeholder in its tag. Set the control's Tag (Developer tab → Properties → Tag) to a placeholder such as {{ds.Name}}, and DocxTemplater replaces the control's content with the resolved value.

This is opt-in and off by default (existing templates may contain content-control tags never intended as placeholders). Enable it via ProcessSettings.EnableContentControlTagBinding:

var template = DocxTemplate.Open("template.docx",
    new ProcessSettings { EnableContentControlTagBinding = true });
template.BindModel("ds", new { Name = "World", DeliveryDate = new DateTime(2026, 7, 8) });
template.Save("generated.docx");
Content control Tag Result
{{ds.Name}} Filled with the value of ds.Name.
{{ds.Name}:ToUpper()} Formatters work exactly as in text placeholders.
{{ds.DeliveryDate}:F('d MMM yyyy')} Date/number format strings are supported.

The same model lookup and formatters as text placeholders are used, and content controls inside a loop are filled once per iteration (the tag resolves against the loop scope, e.g. {{ds.Items}} or {{.Name}}). A tag whose value resolves to null is treated like an unbound tag (see below).

Only formatters that produce inline text (e.g. ToUpper, ToLower, format, and C# expressions) are supported on a content control tag. Block-producing formatters (html, md, img, template) are not supported inside a content control - use them as normal text placeholders in the document body instead.

Unlike a text placeholder, a content control is a named, persistent region:

  • The control and its tag are never removed and are preserved in the output, so the control stays addressable.
  • A control whose tag cannot be bound is left unchanged (under SkipBindingAndRemoveContent). This makes multi-pass filling safe - a control filled in an earlier pass is never cleared by a later pass that does not bind its tag:
// Pass 1: generate the document, binding what is known now.
var generated = Fill(templateBytes, new { Reference = "R-42" });

// Pass 2 (later): stamp a value into a control that was left empty in pass 1,
// without disturbing anything pass 1 already filled.
var finalDoc = Fill(generated, new { DeliveryDate = "8 Jul 2026" });
  • The control's "showing placeholder" flag is cleared on a successful fill, so Word shows the value as real text rather than grey placeholder text.

A tag that is not a placeholder - or whose placeholder is a block directive such as {{#items}} - is ignored, so existing templates are unaffected. Placeholders in a content control's content (rather than its tag) keep working as normal text placeholders.

Content control tag bindings are not reported by GetTemplateSchema() (the tag is not part of the rendered text). This is the same limitation as sub-template formatters.


Whitespace Trimming Around Directives

To improve template readability without affecting the final output, line breaks before and after template directives (e.g., {{#...}}, {{/}}, {{:}}) can be automatically removed. This behavior can be enabled via the ProcessSettings:

var docTemplate = new DocxTemplate(memStream, new ProcessSettings()
{
    IgnoreLineBreaksAroundTags = true
});
var result = docTemplate.Process();

Error Handling

If a placeholder is not found in the model, an exception is thrown. This can be configured with the ProcessSettings:

var docTemplate = new DocxTemplate(memStream);
docTemplate.Settings.BindingErrorHandling = BindingErrorHandling.SkipBindingAndRemoveContent;
var result = docTemplate.Process();
BindingErrorHandling Behavior
ThrowException (default) Process() throws an OpenXmlTemplateException for the first binding error.
SkipBindingAndRemoveContent Placeholders that cannot be bound are removed, loops and conditions with errors render nothing.
HighlightErrorsInDocument Failed placeholders are highlighted in red and all error messages are listed at the top of the document. A template with syntax errors is not rendered at all: the offending tags are highlighted and the errors are listed, see Template Syntax Validation.

Error Codes

Every error raised by the template engine carries a TemplateErrorCode and the arguments of its message, so an application can react to it - or translate it - without parsing the message text:

try
{
    template.Process();
}
catch (OpenXmlTemplateException e)
{
    // e.g. PlaceholderNotReplaced with Arguments[0] == "{{ds.Name}}"
    Console.WriteLine($"{e.ErrorCode}: {string.Join(", ", e.Arguments)}");
    // the cause, e.g. PropertyNotFoundOnType with Arguments[0] == "Name"
    var cause = e.InnerException as OpenXmlTemplateException;
}

ErrorCode is TemplateErrorCode.None for internal errors and for errors of third-party formatters that use the plain OpenXmlTemplateException(string) constructor. The documentation of each TemplateErrorCode value lists the meaning of its arguments.

A condition or expression that cannot be parsed - also an incomplete one such as {?{ds.Count >}} - is reported as ScriptParseError / ExpressionParseError. Earlier versions threw a plain ArgumentException for some incomplete scripts.

OpenXmlTemplateException derives from TemplateException, the common base of the errors of all template types, which defines ErrorCode, Arguments and GetMessage(culture).

Localized Error Messages

Exception messages, TemplateSyntaxError.Message and the error list written with HighlightErrorsInDocument are formatted in the ProcessSettings.UiCulture - the culture of the user who generates the document. It defaults to CultureInfo.CurrentUICulture and is independent of ProcessSettings.Culture, which only formats the values in the document: a user with an English UI can generate a German invoice and still gets English error messages.

English is built in. Other languages are NuGet packages named DocxTemplater.Localization.<language tag>, the tag being the IETF language tag .NET uses as CultureInfo.Name (de, de-CH, fr, it, ...). Installing a package is all it takes: the first time a message is formatted for a culture, DocxTemplater looks for the assembly DocxTemplater.Localization.<culture> of that culture and its parents and loads the texts it finds (once per culture, the result is cached).

// packages installed: DocxTemplater.Localization.de, DocxTemplater.Localization.fr
var template = new DocxTemplate(stream, new ProcessSettings
{
    Culture = new CultureInfo("de-CH"),   // number and date formats in the document
    UiCulture = new CultureInfo("fr-CH")  // language of the error messages: fr-CH -> fr
});

A culture without texts falls back to its parent culture (de-CH → de, fr-CH → fr) and finally to English, so every code always has a message. A specific culture takes precedence over its parent: with DocxTemplater.Localization.de-CH and .de installed, de-CH users get the Swiss spelling and de-DE / de-AT users the standard one.

Automatic loading uses reflection; in trimmed applications disable it with TemplateErrorMessages.Default.AutoLoadLanguagePackages = false and register the packs explicitly: TemplateErrorMessages.Default.AddLanguage(new GermanErrorMessages()) (namespace DocxTemplater.Localization).

Your own language - or your own wording - is a dictionary from TemplateErrorCode to a format string. It does not have to be complete; missing codes fall back as described above. Adding a language twice merges the dictionaries, so single texts can be overridden. TemplateErrorMessages.English is the reference for the placeholders of each code:

TemplateErrorMessages.Default.AddLanguage(new CultureInfo("es"), new Dictionary<TemplateErrorCode, string>
{
    [TemplateErrorCode.ModelNotFound] = "Modelo {0} no encontrado",
    [TemplateErrorCode.BlockNotClosed] = "'{0}' no está cerrado",
});

To ship a language as a package of its own, create an assembly named DocxTemplater.Localization.<language tag> with a public class implementing ITemplateLanguagePack (Culture + Formats) and a parameterless constructor; it is then loaded automatically like the built-in packages.

TemplateErrorMessages.Default is shared by all documents. To keep languages local to one document, assign a separate instance to ProcessSettings.ErrorMessages.

An error can be re-rendered in another language at any time, nested messages included: e.GetMessage(new CultureInfo("de")), syntaxError.GetMessage(culture) and syntaxError.ToString(culture).


Culture

The culture used to format the model values can be configured with the ProcessSettings:

var docTemplate = new DocxTemplate(memStream, new ProcessSettings()
{
    Culture = new CultureInfo("en-us")
});
var result = docTemplate.Process();

Process Settings

The ProcessSettings hold the options above and the registrations: own formatters (RegisterFormatter) and tag content preprocessors (RegisterTagContentPreprocessor). They also cache the parsed conditions and expressions, so for a batch of documents - letters for many recipients, a print run - create one instance and pass it to every DocxTemplate of the batch; each expression is then parsed once instead of once per document. Templates with the same settings can be processed in parallel; registered formatters are shared and must be thread-safe then.

var settings = new ProcessSettings { Culture = new CultureInfo("de-CH") };
settings.RegisterFormatter(new ImageFormatter());
foreach (var customer in customers)
{
    using var template = DocxTemplate.Open("letter.docx", settings);
    template.BindModel("ds", customer);
    template.Save($"{customer.Id}.docx");
}
  • Copied per template. A DocxTemplate copies the settings when it is created; changing them afterwards only affects templates created later. template.Settings is the copy of that template: changes to it (e.g. template.Settings.RegisterFormatter(...)) apply to this template if made before Process and do not affect any other. All copies share the cache of the original instance.
  • Per batch, not per application. The cache has no size limit; it grows with the number of distinct expressions times the types of their values. Create the settings per batch and do not keep one instance for the lifetime of an application that renders templates edited by users.
  • Extensions (ITemplateProcessorExtension) are still registered per document with DocxTemplate.RegisterExtension.

Advanced Model Binding

Two ways to control how placeholders are resolved against your model: ITemplateModel and TemplateModelWithDisplayNames.

ITemplateModel Interface

For advanced scenarios where a standard object or dictionary is not suitable for your data model, you can implement the ITemplateModel interface. This allows you to control how properties are resolved for template binding.

public interface ITemplateModel
{
    bool TryGetPropertyValue(string propertyName, out ValueWithMetadata value);
}

Implement this interface to provide custom property lookup logic. This is useful for dynamic models, computed properties, or when you want to support custom property resolution strategies.

TemplateModelWithDisplayNames Base Class

TemplateModelWithDisplayNames is an abstract base class that extends ITemplateModel and allows you to bind template placeholders to properties using either their property name or a [DisplayName] attribute. This is especially useful when you want to use user-friendly or localized names in your templates.

using DocxTemplater.Model;
using System.ComponentModel;

public class PersonModel : TemplateModelWithDisplayNames
{
    [DisplayName("Vorname")]
    public string FirstName { get; set; }

    [DisplayName("Nachname")]
    public string LastName { get; set; }
}

In your template, you can then use either {{person.Vorname}} or {{person.FirstName}} to access the property.

Template Schema Inspection

GetTemplateSchema() statically analyzes a template without rendering it and returns the structural schema of the variables, collections, and nested objects the template references. Use it to validate a caller's model against the template's expectations before rendering, or to generate a skeleton model for callers to fill in.

using var template = DocxTemplate.Open("template.docx");

// Template: "Hello {{customer.Name}}" and "{{#items}}{{items.Price}}{{/items}}"
var schema = template.GetTemplateSchema();

// Roots are the top-level models you would pass to BindModel (case-insensitive)
foreach (var root in schema.Roots.Values)
{
    Console.WriteLine($"{root.Name}: {root.Kind}");
}

schema.Roots["customer"].Properties["Name"].Kind;          // Scalar
schema.Roots["items"].Kind;                                // Collection
schema.Roots["items"].ItemSchema.Properties["Price"].Kind; // Scalar

Each TemplateSchemaNode exposes:

Member Description
Name Name as referenced in the template (case-insensitive against the model).
Kind Scalar, Object, or Collection.
Properties Child properties of an Object node (empty for scalars/collections).
ItemSchema Element shape of a Collection node (null if items are never accessed).

The schema is the union over all branches (both if and else, every case, every loop body) - a caller must be prepared to bind anything the template could reach at runtime.

GetTemplateSchema() may be called before Process() on the same instance; the analysis result is cached and reused when rendering, so there is no need to reopen the template:

using var template = DocxTemplate.Open("template.docx");
var schema = template.GetTemplateSchema();   // inspect
template.BindModel("customer", customer);
template.Save("generated.docx");             // render - reuses the cached analysis

Known limitations (these constructs may yield an incomplete schema):

  • Sub-template formatters (:template / :T): the referenced sub-template is itself a runtime template string and not visible to static analysis.
  • Dynamic tables (:dyntable): only the collection itself is reported, not its runtime-defined rows/columns.
  • String-key indexing (props["key"]): the key is not statically known and is not reflected in the schema.

Template Syntax Validation

ValidateTemplateSyntax() checks the template syntax without rendering it and without a model and returns all errors found (an empty list if the syntax is valid). The document is not modified. The same parser runs at the start of Process(): errors with Severity == Error (broken block structure) make Process() throw an OpenXmlTemplateException listing all of them, while warnings (malformed tags, suspicious expressions) only show up here. With BindingErrorHandling.HighlightErrorsInDocument, Process() does not throw for syntax errors: nothing is rendered (a broken block structure cannot be rendered partially), the offending tags are highlighted in red in every part and all errors are listed at the top of the document in the UiCulture, exactly like binding errors. The other modes keep throwing.

using var template = DocxTemplate.Open("template.docx");
foreach (var error in template.ValidateTemplateSyntax())
{
    // e.g. "Body: '{{/Orders}}' does not match '{{#Items}}' (near '...')"
    Console.WriteLine(error);
}

Errors:

  • Blocks that are not closed and closing tags without an opening tag.
  • Else {{:}} outside of a condition or more than once, separator {{:s:}} outside of a collection loop, {{#case}}/{{#default}} outside of a switch, {{#}} / {{#switch}} without a name or expression, unknown inline keywords ({{:Foo}}).

Warnings:

  • Closing tags that do not match their opening tag ({{#Items}}...{{/Orders}}, {?{...}}...{{/Items}}, {{:ignore}}...{{/}}) - rendering closes the current block anyway. The closing tag may omit the implicit model prefix ({{#ds.Items}}...{{/Items}}).
  • Malformed tags that silently remain as text, e.g. {{Name}, {{first name}}, a stray }}.
  • Unbalanced parentheses and unterminated strings in conditions, expressions, switch selectors and case values.

Everything between {{:ignore}} and {{/:ignore}} is treated as plain text and not validated.

Each TemplateSyntaxError exposes the Severity, the Part (Body, Header or Footer), the offending Tag, a Message and the surrounding text as Context. The ErrorCode and Arguments identify the error independent of the message language, see Error Codes; the Message is formatted in the ProcessSettings.UiCulture and can be re-rendered in another language with GetMessage(culture). Binding errors (unknown variables, wrong types, unknown formatters) are not detected, as they depend on the model.

Tag Content Preprocessors

Editors change what is typed into a tag: Word's autocorrect turns ' into ‘’, an HTML editor stores a typed > as &gt; and & as &amp;. An ITagContentPreprocessor repairs the content of a tag before it is parsed. It receives the expressions - conditions, {{(...)}} expressions including their parentheses, switch and case expressions - as TagContentKind.Expression, and the arguments of a formatter as written between the parentheses as TagContentKind.FormatterArguments; the result is split into arguments afterwards, so the preprocessor can also fix the quotes that delimit an argument (formatter arguments are quoted with '):

public sealed class HtmlEditorPreprocessor : ITagContentPreprocessor
{
    public string Process(string content, TagContentKind kind)
    {
        return WebUtility.HtmlDecode(content).Replace('‘', '\'').Replace('’', '\'').Replace('“', '"').Replace('”', '"');
    }
}

processSettings.RegisterTagContentPreprocessor(new HtmlEditorPreprocessor());  // DocxTemplate
textTemplateSettings.RegisterTagContentPreprocessor(new HtmlEditorPreprocessor());  // TextTemplate

With it {?{ds.Count &gt; 1}} is evaluated as ds.Count > 1 and {{ds.Date}:f(‘dd.MM.yyyy’)} as f('dd.MM.yyyy'). Several preprocessors run in the order they are registered. They are registered in the settings before the first template uses them, see Process Settings.

Only tags that are recognized are preprocessed. Text outside of tags and variable names are never passed - a variable name can only hold letters, digits, . and _, so a tag with markup or a character reference in its name ({{ds.<b>First</b>Name}}) is not a tag at all and stays in the output as it is, reported as InvalidTag. Error positions and the Tag of an error refer to the template as written.

Text and HTML Templates

The package DocxTemplater.Text renders plain text and HTML templates - for example the subject and the body of an e-mail - with the same syntax and the same semantics as the Word templates, so an application can use one template language for its documents and its mails. Placeholders with paths and formatters, C# expressions, collections with _Idx and separators, range loops, conditions, switch blocks and {{:ignore}} work exactly as described above; the conformance tests of the repository run the same templates through both renderers.

A TextTemplate renders one text: create it with the template and the settings, bind the models and call Process(). The TextTemplateSettings hold the options, the formatters and the tag content preprocessors:

using DocxTemplater.Text;

var settings = new TextTemplateSettings
{
    Escaping = TextEscaping.Html,          // Html (default) or None for plain text such as a subject
    Culture = new CultureInfo("de-CH"),    // formats the values
    UiCulture = new CultureInfo("fr"),     // language of the error messages
    BindingErrorHandling = BindingErrorHandling.ThrowException,
    IgnoreLineBreaksAroundTags = true
};
settings.RegisterFormatter(new MyFormatter());           // optional

var template = new TextTemplate(
    "<p>Dear {{ds.Name}}</p><ul>{{#ds.Items}}<li>{{.Name}}: {{.Price}:f(c)}</li>{{/ds.Items}}</ul>", settings);
template.BindModel("ds", model);
TextRenderResult result = template.Process();            // the text and the collected errors
string html = result.Text;

IReadOnlyList<TemplateSyntaxError> warnings = template.ValidateTemplateSyntax();   // never throws
TemplateSchema schema = template.GetTemplateSchema();

Errors are TemplateExceptions, the common base class of OpenXmlTemplateException, with the same error codes and localized messages. A template with syntax errors makes Process() and GetTemplateSchema() throw TemplateSyntaxErrors, whose argument is the list of TemplateSyntaxErrors - except with HighlightErrorsInDocument, see below.

What is specific to text:

  • Escaping. With TextEscaping.Html (the default) every value is HTML-encoded and line breaks in a value become <br/>. Only & < > " ' are encoded, umlauts stay as they are (Müller &amp; Co), so the output has to be sent as Unicode (UTF-8); a value that is trusted HTML - a rich text field, a rendered sub-template - is inserted unchanged with {{value}:html()}. With TextEscaping.None values are inserted as they are. The text of the template itself is never encoded. {{:break}} is a <br/> or a line feed respectively.
  • Whitespace trimming. With IgnoreLineBreaksAroundTags a line that holds nothing but block tags ({{#Items}}, {{/}}, {{:}}, {{:s:}}, {{#case: ...}}, ...) and whitespace is removed together with its line break, so a template can be indented and spread over lines without affecting the output. Lines with values are not affected.
  • HTML editors. Register a tag content preprocessor that decodes the character references an HTML editor writes into expressions and formatter arguments.
  • Unrecognized tags. Text that looks like a tag but is not one - {{first name}}, a tag split by markup - stays in the output as it is. It is an error of the template, not of a render: ValidateTemplateSyntax() reports it as warning InvalidTag with its position.
  • Error handling. The three BindingErrorHandling modes behave as in Word documents. With ThrowException every tag that cannot be rendered - value, condition, loop or switch case - throws PlaceholderNotReplaced with the tag and its context and the cause as inner exception. With HighlightErrorsInDocument the failed tags stay in the output - wrapped in a red <span> in HTML - and all errors are listed at the top of the output; a template with syntax errors is not rendered but returned with its offending tags highlighted. The highlighting is markup: a failed tag inside an HTML attribute (<a href="{{url}}">) breaks the attribute, so use this mode for previews, not for output that is sent.
  • Word-only constructs: the Word formatters :img(), :template(), :md() and :dyntable() are not built into text templates. An application can register an ITextFormatter under the same name (e.g. md rendering Markdown to HTML); otherwise they fail like any unknown formatter with UnknownFormatter. The keywords {{:PageBreak}} and {{:SectionBreak}} stay syntax errors (KeywordNotSupported). Charts and content controls do not exist in text.
  • Formatters. format / f, toupper / tolower, html and default / ifempty are built in. Own formatters implement ITextFormatter and return a TextValue; TextValue.Raw(...) marks output that is already HTML and must not be encoded. A formatter that no registered formatter handles is an error (UnknownFormatter) - unlike in Word documents, where the value is inserted as text.

Layouts are left to the application: render the body with one template, then render the layout with the body bound to a value and inserted with {{body}:html()}.

Rendering for Many Recipients

For a mailing - one subject and one body for thousands of recipients - create the settings once per mailing and a new template per recipient. The settings cache the parsed conditions and expressions, so a new template only parses its text (about 20 µs for a 15 KB mail); without shared settings every template compiles its expressions anew, which is about 60 times slower.

var subjectSettings = new TextTemplateSettings { Escaping = TextEscaping.None, Culture = culture };
var bodySettings = new TextTemplateSettings
{
    Culture = culture,
    BindingErrorHandling = BindingErrorHandling.SkipBindingAndRemoveContent // collect the errors instead of throwing
};
bodySettings.RegisterTagContentPreprocessor(new HtmlEditorPreprocessor());

Parallel.ForEach(recipients, recipient =>
{
    var body = new TextTemplate(bodyHtml, bodySettings);
    body.BindModel("recipient", recipient);
    body.BindModel("company", company);
    body.BindModel("unsubscribe", Unsubscribe(recipient));
    TextRenderResult html = body.Process();
    if (html.HasErrors)
    {
        // html.Errors: Tag, Index, Length and the TemplateException (ErrorCode, Message, cause) of each failed tag
        return;
    }
    var subject = new TextTemplate("News for {{(recipient.FirstName ?? recipient.LastName)}}", subjectSettings);
    subject.BindModel("recipient", recipient);
    Send(recipient, subject.Process().Text, html.Text);
});
  • Settings per mailing. A TextTemplate copies the settings when it is created, so changing them afterwards only affects templates created later; all copies share the cache. The cache has no size limit - it grows with the number of distinct expressions times the types of their values - so create the settings per mailing (or per template and language) and do not keep one instance for the lifetime of an application that renders templates edited by users. Culture and escaping belong to the settings; use one instance per language.
  • Thread-safe and isolated. Templates with the same settings can be processed concurrently; one TextTemplate instance is not thread-safe. Each template resolves its own models, so no value of one recipient can appear in the output of another. Registered formatters are shared and must be thread-safe.
  • Collected errors. Process() returns the text and the errors of this template. With SkipBindingAndRemoveContent the failed tags are left out of the text and reported only in TextRenderResult.Errors - also exceptions of the model, of an expression (an index out of range, a division by zero) or of a formatter, so one broken recipient does not stop the mailing. A missing value is not an error: a null property renders as empty text, and in conditions and expressions a missing property is null with SkipBindingAndRemoveContent, so {{(recipient.FirstName ?? "Sir or Madam")}} falls back.

Breaking Changes

Compared to 3.0.0-beta.2:

  • Registrations moved to the settings. DocxTemplate.RegisterFormatter(...) is now ProcessSettings.RegisterFormatter(...) - on the settings passed to the template or on template.Settings, see Process Settings. RegisterExtension stays on the DocxTemplate.
  • Settings are copied. A DocxTemplate works with a copy of the ProcessSettings taken in its constructor: changing the instance passed in after creating the template no longer affects it. Change template.Settings instead.
  • default / ifempty are built in. A custom formatter of that name is no longer reached. Unquoted text is split at commas and joined with ", " (default(a,b) renders a, b); quote it to keep it as written.
  • Incomplete scripts. A condition or expression that cannot be parsed ({?{ds.Count >}}) is reported as ScriptParseError / ExpressionParseError (an OpenXmlTemplateException) instead of an ArgumentException.

Worth knowing when moving e-mails to DocxTemplater.Text: TextEscaping.Html encodes only & < > " ' (umlauts stay, send the output as UTF-8), and HighlightErrorsInDocument wraps a failed tag in a <span>, which breaks the markup of a tag inside an HTML attribute (<a href="{{url}}">) - use it for previews only.

Support This Project

If you find DocxTemplater useful, please consider supporting its development:

Sponsor Buy Me A Coffee

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
3.0.0-beta.4 0 11/9/2026
3.0.0-beta.3 5 11/8/2026