Mail Merge Performance
A merge of one invoice takes a fraction of a second; a merge of thousands of records, or of a template with a very long table, takes long enough for a user to notice. Text Control's benchmark of MailMerge shows where the time goes, and from it follow three habits for WPF applications: templates designed with the costs in mind, merges that run behind the window with progress and a cancel button, and separate processes for batches too large for threads.
Measured Merge Times
In 2023, Text Control measured MailMerge with an invoice template: a header with an SVG logo, an address block and a merge block for the line items, saved as PDF. The test was a .NET console application using ServerTextControl and MailMerge of TX Text Control .NET Server 31.0 SP2 on one thread, on an AMD Ryzen 7 2700X eight-core processor with 3.70 GHz and 16 GB of memory.
| Scenario | Result |
|---|---|
| One-page invoice with 5 rows in the merge block | 0.35 seconds per invoice, growing linearly: 80 invoices in about 16 seconds |
| Six-page invoice with 100 rows in the merge block | 3.2 seconds per invoice, growing linearly with the number of invoices |
| One merge block with more and more rows | 0.23 seconds for one row; the time grew faster than the rows, up to 1,280 rows on 59 pages |
| The same rows in two merge blocks | Faster than in one block |
| Two formulas in the merge block | Acceptable for short tables, calculated again for each row |
| PDF encryption, digital signatures and embedded files | No significant extra time; embedded files depend on their size |
The measurements come from the server product and its test machine. Use them for proportions, and measure your own templates for absolute times. The blog article MailMerge Class Performance Benchmark shows the charts and the test code.
Design Faster Templates
- Split long tables. One merge block slowed down disproportionately as its rows grew, while the same rows in two blocks merged faster. Group long lists, for example by category, into several blocks; Merge Blocks explains how blocks are defined.
- Calculate in the data. A formula in a merge block runs for every row. For long tables, pass line totals and sums in the data.
- Split the result when you need single documents. With
appendset tofalse, each record becomes its own document, delivered by theDataRowMergedevent, as Create One Document or One per Record shows.
Merge in the Background With Progress and Cancel
A MailMerge that merges into the WPF TextControl works on the UI thread, and the window waits for it. Connected to a ServerTextControl, the merge runs on a thread of the thread pool, and its DataRowMerged event, which occurs after each record, reports progress and lets the user cancel.
The following handlers in MainWindow.xaml.cs merge the invoices of invoice.json into the template invoice.docx. They need a ProgressBar named mergeProgress and the buttons mergeButton and cancelMergeButton in MainWindow.xaml, whose Click events call Merge_Click and CancelMerge_Click:
using System;
using System.IO;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using TXTextControl;
using TXTextControl.DocumentServer;
private CancellationTokenSource mergeCancellation;
private async void Merge_Click(object sender, RoutedEventArgs e)
{
string json = File.ReadAllText("invoice.json");
// The number of records, for the progress bar
using (JsonDocument data = JsonDocument.Parse(json))
{
mergeProgress.Maximum = data.RootElement.GetArrayLength();
}
mergeProgress.Value = 0;
mergeButton.IsEnabled = false;
IProgress<int> progress = new Progress<int>(count => mergeProgress.Value = count);
mergeCancellation = new CancellationTokenSource();
CancellationToken token = mergeCancellation.Token;
try
{
byte[] result = await Task.Run(() =>
{
using (ServerTextControl tx = new ServerTextControl())
{
tx.Create();
tx.Load("invoice.docx", StreamType.WordprocessingML, new LoadSettings
{
ApplicationFieldFormat = ApplicationFieldFormat.MSWord,
LoadSubTextParts = true
});
using (MailMerge mailMerge = new MailMerge { TextComponent = tx })
{
int merged = 0;
// Raised on this background thread after each record
mailMerge.DataRowMerged += (s, args) =>
{
progress.Report(++merged);
args.Cancel = token.IsCancellationRequested;
};
mailMerge.MergeJsonData(json);
}
tx.Save(out byte[] document, BinaryStreamType.InternalUnicodeFormat);
return document;
}
});
// Back on the UI thread: show the merged invoices
textControl1.Load(result, BinaryStreamType.InternalUnicodeFormat);
}
catch (MergeCanceledException)
{
MessageBox.Show(this, "The merge was canceled.");
}
finally
{
mergeButton.IsEnabled = true;
mergeCancellation.Dispose();
mergeCancellation = null;
}
}
private void CancelMerge_Click(object sender, RoutedEventArgs e)
{
mergeCancellation?.Cancel();
}
The code works as follows:
- The
LoadSettingsimport the MS Word fields and merge blocks of the DOCX template, and MergeJsonData merges all records of the JSON array into one document. - The
DataRowMergedhandler runs on the thread that merges. It doesn't touch any WPF element;Progress<int>, created on the UI thread, hands the count to the dispatcher. - Setting Cancel to
truestops the merge, which ends with a MergeCanceledException.awaitthrows it again on the UI thread. - The merged document comes back as a byte array, and WPF.TextControl.Load shows it in the editor.
Set MailMerge.MergeCulture in merges that run in the background: it defaults to the culture of the current thread, as Background Operations explains.
Merge Large Batches in Separate Processes
Threads that merge at the same time wait for each other inside TX Text Control, as How Far Threads Take You explains, so for merges of hundreds or thousands of documents Text Control recommends several processes instead. Its sample of 2019 starts a new process of a processing application for each template, passes the template and its data through anonymous pipes and gets the PDF file back; 100 templates took about 45 seconds with normal processing and 14 seconds with parallel processes on a CPU with 16 cores. The sample is written for Windows Forms, and Text Control states that the concept applies to WPF applications as well. The blog article True Parallel Processing of Documents using MailMerge describes the host and the processing application.
The NuGet package TXTextControl.DocumentServer.ParallelMailMerge provides such worker processes ready-made. Text Control names console, Windows Forms and ASP.NET applications and other .NET project types as places to use it. It targets .NET 10 and depends on TXTextControl.TextControl.Core.SDK, the package of TX Text Control .NET Server, not on TXTextControl.TextControl.WPF.SDK. Multiple Processes in the documentation of TX Text Control .NET Server describes it with its options and measurements.
Next Steps
- Overview — where a merge runs and what it creates.
- Merge Events — every event of
MailMerge. - Background Operations — threads, progress and cancellation in WPF.
- Performance Best Practices — all measures at a glance.