How to Mail Merge Word Documents in C#
IronWord performs mail merge in C# by populating MERGEFIELD placeholders in a Word template with your data, drawn from dictionaries, DataTable/DataRow/DataSet objects, or repeating-region tables, all without Microsoft Office Interop. Merge fields created in Microsoft Word, or any compatible tool, are detected automatically.
Mail merge is the standard way to generate personalized documents at scale: form letters, invoices, certificates, contracts, and reports that share one template but differ per recipient. Instead of editing each document by hand, you author a single .docx template with merge fields and let IronWord fill them from your data source. Every operation is reached through the WordDocument.MailMerge entry point.
Quickstart: Mail Merge a Word Document
Load a template that contains MERGEFIELD placeholders, pass a dictionary of field names and values to Execute, and save the result.
-
Install IronWord with NuGet Package Manager
-
Copy and run this code snippet.
WordDocument doc = new WordDocument("template.docx"); doc.MailMerge.Execute(new Dictionary<string, string> { { "FirstName", "Jane" }, { "Company", "Acme Corp" } }); doc.SaveAs("output.docx"); -
Deploy to test on your live environment
Start using IronWord in your project today with a free trial
Minimal Workflow (5 steps)
- Download the C# library for Word mail merge from NuGet
- Author a Word template with
MERGEFIELDplaceholders - Load the template with the
WordDocumentconstructor - Call
MailMerge.ExecuteorMailMerge.ExecuteWithRegionswith your data source - Save the populated document
How Do I Mail Merge from a Dictionary?
The simplest data source is an IDictionary<string, string> keyed by merge-field name. Execute replaces every field whose name matches a key.
:path=/static-assets/word/content-code-examples/how-to/mail-merge-dictionary.cs
using IronWord;
using System.Collections.Generic;
// Load a template containing MERGEFIELD placeholders
WordDocument doc = new WordDocument("template.docx");
// Replace each field whose name matches a dictionary key
doc.MailMerge.Execute(new Dictionary<string, string>
{
{ "FirstName", "Jane" },
{ "LastName", "Smith" },
{ "Company", "Acme Corp" }
});
doc.SaveAs("output.docx");
Imports IronWord
Imports System.Collections.Generic
' Load a template containing MERGEFIELD placeholders
Dim doc As New WordDocument("template.docx")
' Replace each field whose name matches a dictionary key
doc.MailMerge.Execute(New Dictionary(Of String, String) From {
{"FirstName", "Jane"},
{"LastName", "Smith"},
{"Company", "Acme Corp"}
})
doc.SaveAs("output.docx")
MailMerge.Options.CaseInsensitiveFieldNames = false to require an exact-case match.You can also supply two parallel sequences of field names and values:
:path=/static-assets/word/content-code-examples/how-to/mail-merge-parallel-sequences.cs
using IronWord;
WordDocument doc = new WordDocument("template.docx");
// Merge from two parallel sequences of field names and values
doc.MailMerge.Execute(
new[] { "FirstName", "LastName" },
new[] { "Jane", "Smith" });
doc.SaveAs("output.docx");
Imports IronWord
Dim doc As New WordDocument("template.docx")
' Merge from two parallel sequences of field names and values
doc.MailMerge.Execute(
New String() { "FirstName", "LastName" },
New String() { "Jane", "Smith" })
doc.SaveAs("output.docx")
Execute(fieldNames, values) throws an ArgumentException when the two sequences have different lengths.How Do I Mail Merge from a DataTable or DataRow?
When data comes from a database or an existing DataSet, pass a DataTable or DataRow straight to Execute. Merge-field names are matched to column names. Execute(DataTable) uses the values from the table's first row, while Execute(DataRow) uses the column names of the row's parent table.
:path=/static-assets/word/content-code-examples/how-to/mail-merge-datatable.cs
using IronWord;
using System.Data;
// Build a DataTable, for example from a database query
DataTable table = new DataTable();
table.Columns.Add("FirstName");
table.Columns.Add("LastName");
DataRow row = table.NewRow();
row["FirstName"] = "Jane";
row["LastName"] = "Smith";
table.Rows.Add(row);
WordDocument doc = new WordDocument("template.docx");
// Merge-field names are matched to the row's column names
doc.MailMerge.Execute(row);
doc.SaveAs("output.docx");
Imports IronWord
Imports System.Data
' Build a DataTable, for example from a database query
Dim table As New DataTable()
table.Columns.Add("FirstName")
table.Columns.Add("LastName")
Dim row As DataRow = table.NewRow()
row("FirstName") = "Jane"
row("LastName") = "Smith"
table.Rows.Add(row)
Dim doc As New WordDocument("template.docx")
' Merge-field names are matched to the row's column names
doc.MailMerge.Execute(row)
doc.SaveAs("output.docx")
How Do I Fill Repeating Table Regions?
Repeating regions turn one template row into many, which is ideal for invoice line items or order lists. In the template, wrap the repeating content between two marker fields named TableStart:RegionName and TableEnd:RegionName. Then call ExecuteWithRegions, which repeats the region's content once per row of the matching table.
ExecuteWithRegions accepts a full DataSet (expanding every region whose name matches a table), a single DataTable (expanding the region whose name matches dataTable.TableName), or a region name together with a DataTable.
:path=/static-assets/word/content-code-examples/how-to/mail-merge-regions.cs
using IronWord;
using System.Collections.Generic;
using System.Data;
// One DataTable per repeating region, named to match the TableStart/TableEnd markers
DataTable orders = new DataTable("Orders");
orders.Columns.Add("Item");
orders.Columns.Add("Qty");
orders.Rows.Add("Widget A", "3");
orders.Rows.Add("Widget B", "1");
DataSet ds = new DataSet();
ds.Tables.Add(orders);
WordDocument doc = new WordDocument("invoice-template.docx");
// Expand the repeating region once per row, then fill standard fields
doc.MailMerge.ExecuteWithRegions(ds);
doc.MailMerge.Execute(new Dictionary<string, string> { { "CustomerName", "Jane Smith" } });
doc.SaveAs("invoice.docx");
Imports IronWord
Imports System.Collections.Generic
Imports System.Data
' One DataTable per repeating region, named to match the TableStart/TableEnd markers
Dim orders As New DataTable("Orders")
orders.Columns.Add("Item")
orders.Columns.Add("Qty")
orders.Rows.Add("Widget A", "3")
orders.Rows.Add("Widget B", "1")
Dim ds As New DataSet()
ds.Tables.Add(orders)
Dim doc As New WordDocument("invoice-template.docx")
' Expand the repeating region once per row, then fill standard fields
doc.MailMerge.ExecuteWithRegions(ds)
doc.MailMerge.Execute(New Dictionary(Of String, String) From {{"CustomerName", "Jane Smith"}})
doc.SaveAs("invoice.docx")
ExecuteWithRegions only populates repeating regions. To also fill standard merge fields outside the regions, call Execute(...) after expanding the regions, as shown above.How Do I Inspect a Template's Merge Fields?
Before merging, you can discover what a template contains, which is useful for validating templates or building the data source dynamically.
GetFieldNames()returns the names of all value-style merge fields, in document order and deduplicated.GetRegionNames()returns the names of allTableStartregions declared in the document.GetFields()returns every field, includingTableStart/TableEndmarkers, asMergeFieldobjects.
:path=/static-assets/word/content-code-examples/how-to/mail-merge-inspect-fields.cs
using IronWord;
using System.Collections.Generic;
WordDocument doc = new WordDocument("template.docx");
// Discover the merge fields and regions before merging
IReadOnlyList<string> fieldNames = doc.MailMerge.GetFieldNames();
IReadOnlyList<string> regionNames = doc.MailMerge.GetRegionNames();
// fieldNames: ["FirstName", "LastName", "Company"]
// regionNames: ["Orders", "LineItems"]
Imports IronWord
Imports System.Collections.Generic
Dim doc As New WordDocument("template.docx")
' Discover the merge fields and regions before merging
Dim fieldNames As IReadOnlyList(Of String) = doc.MailMerge.GetFieldNames()
Dim regionNames As IReadOnlyList(Of String) = doc.MailMerge.GetRegionNames()
' fieldNames: ["FirstName", "LastName", "Company"]
' regionNames: ["Orders", "LineItems"]
Each MergeField exposes its Name, the full Instruction text as stored in the document (for example MERGEFIELD FirstName \* MERGEFORMAT), a RegionName that is set only for region markers (for example "Orders" from "TableStart:Orders"), and a Kind that classifies the field as Value (text replaced with a data value), TableStart or TableEnd (the bounds of a repeating region), or NextRecord (a NEXT field that advances to the next data record within the same template body).
How Do I Control Unmatched Fields and Null Values?
Merge behavior is configured through MailMerge.Options:
| Option | Default | Behavior |
|---|---|---|
RemoveUnusedFields |
true |
Removes merge fields that have no matching key in the data source. Set to false to leave them in place. |
RemoveUnusedRegions |
true |
Removes TableStart/TableEnd regions with no matching table in the data source. |
NullValueReplacement |
"" |
Text substituted when the data source supplies a null value for a field. |
CaseInsensitiveFieldNames |
true |
Ignores case when matching field names, matching Microsoft Word behavior. |
:path=/static-assets/word/content-code-examples/how-to/mail-merge-options.cs
using IronWord;
using System.Collections.Generic;
WordDocument doc = new WordDocument("template.docx");
// Keep unmatched fields in the output instead of removing them
doc.MailMerge.Options.RemoveUnusedFields = false;
doc.MailMerge.Execute(new Dictionary<string, string> { { "FirstName", "Jane" } });
doc.SaveAs("partial-output.docx");
Imports IronWord
Imports System.Collections.Generic
Dim doc As New WordDocument("template.docx")
' Keep unmatched fields in the output instead of removing them
doc.MailMerge.Options.RemoveUnusedFields = False
doc.MailMerge.Execute(New Dictionary(Of String, String) From {{"FirstName", "Jane"}})
doc.SaveAs("partial-output.docx")
For straightforward value substitution without a data source, see how to replace text in a Word document.
Frequently Asked Questions
How do I mail merge a Word document in C#?
Load a Word template containing MERGEFIELD placeholders with the WordDocument constructor, then call doc.MailMerge.Execute with an IDictionary, DataTable, or DataRow to replace each field whose name matches a key or column, and save the result with SaveAs. Merge fields created in Microsoft Word are detected automatically, and no Microsoft Office installation is required.
Which data sources can IronWord mail merge use?
MailMerge.Execute accepts an IDictionary of field names and values, two parallel sequences of field names and values, a DataTable (using the first row), or a DataRow (using its parent table's columns). MailMerge.ExecuteWithRegions accepts a DataSet or DataTable to expand repeating table regions.
How do I fill repeating table regions in a Word mail merge?
Wrap the repeating content in the template between TableStart:RegionName and TableEnd:RegionName merge fields, then call MailMerge.ExecuteWithRegions with a DataSet or DataTable. The region's content repeats once per row of the matching table. Because ExecuteWithRegions only populates regions, call Execute afterward to fill any standard merge fields.
Can I inspect a template's merge fields before merging?
Yes. MailMerge.GetFieldNames returns the value-style field names in document order, GetRegionNames returns the TableStart region names, and GetFields returns MergeField objects exposing each field's Name, Instruction, Kind (Value, TableStart, TableEnd, or NextRecord), and RegionName.
What happens to merge fields with no matching data?
By default MailMergeOptions.RemoveUnusedFields is true, so fields with no matching key in the data source are removed from the output. Set it to false to leave them in place. Unmatched TableStart/TableEnd regions are removed when RemoveUnusedRegions is true, and null values are replaced with the NullValueReplacement string.
Is mail merge field matching case-sensitive?
No. Field-name lookups are case-insensitive by default, matching Microsoft Word behavior. Set MailMerge.Options.CaseInsensitiveFieldNames to false if you need an exact-case match.

