# Mail Merge Performance

> What MailMerge costs per invoice and merge block, templates that merge faster, WPF merges in the background with progress and cancel, multi-process batches.

- **Product:** TX Text Control .NET for WPF
- **Section:** Documentation
- **Kind:** Topic
- **Path:** TX Text Control .NET for WPF > Performance > Mail Merge Performance
- **2684 words**
- **Web URL:** https://docs.textcontrol.com/tx-text-control-dotnet-wpf/performance/mail-merge/
- **LLMs URL:** https://docs.textcontrol.com/tx-text-control-dotnet-wpf/performance/mail-merge/llms.txt
- **LLMs-Full URL:** https://docs.textcontrol.com/tx-text-control-dotnet-wpf/performance/mail-merge/llms-full.txt

---

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](https://www.textcontrol.com/blog/2023/04/21/mailmerge-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](https://docs.textcontrol.com/tx-text-control-dotnet-wpf/mail-merge/blocks/llms-full.txt) 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 `append` set to `false`, each record becomes its own document, delivered by the `DataRowMerged` event, as [Create One Document or One per Record](https://docs.textcontrol.com/tx-text-control-dotnet-wpf/mail-merge/overview/#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](https://docs.textcontrol.com/tx-text-control-dotnet-wpf/reference/txtextcontrol-documentserver/mailmerge/datarowmerged-event/llms-full.txt) 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`:

```csharp
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 `LoadSettings` import the MS Word fields and merge blocks of the DOCX template, and [MergeJsonData](https://docs.textcontrol.com/tx-text-control-dotnet-wpf/reference/txtextcontrol-documentserver/mailmerge/mergejsondata-method/llms-full.txt) merges all records of the JSON array into one document.
- The `DataRowMerged` handler 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](https://docs.textcontrol.com/tx-text-control-dotnet-wpf/reference/txtextcontrol-documentserver/mailmerge-datarowmergedeventargs/cancel-property/llms-full.txt) to `true` stops the merge, which ends with a [MergeCanceledException](https://docs.textcontrol.com/tx-text-control-dotnet-wpf/reference/txtextcontrol-documentserver/mergecanceledexception/llms-full.txt). `await` throws it again on the UI thread.
- The merged document comes back as a byte array, and [WPF.TextControl.Load](https://docs.textcontrol.com/tx-text-control-dotnet-wpf/reference/txtextcontrol-wpf/textcontrol/load-method/llms-full.txt) shows it in the editor.

Set [MailMerge.MergeCulture](https://docs.textcontrol.com/tx-text-control-dotnet-wpf/reference/txtextcontrol-documentserver/mailmerge/mergeculture-property/llms-full.txt) in merges that run in the background: it defaults to the culture of the current thread, as [Background Operations](https://docs.textcontrol.com/tx-text-control-dotnet-wpf/performance/background-operations/#how-far-threads-take-you) 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](https://docs.textcontrol.com/tx-text-control-dotnet-wpf/performance/background-operations/#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](https://www.textcontrol.com/blog/2019/08/16/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](https://docs.textcontrol.com/tx-text-control-dotnet-server/performance/multiple-processes/llms-full.txt) in the documentation of TX Text Control .NET Server describes it with its options and measurements.

### Next Steps

- [Overview](https://docs.textcontrol.com/tx-text-control-dotnet-wpf/mail-merge/overview/llms-full.txt) — where a merge runs and what it creates.
- [Merge Events](https://docs.textcontrol.com/tx-text-control-dotnet-wpf/mail-merge/events/llms-full.txt) — every event of `MailMerge`.
- [Background Operations](https://docs.textcontrol.com/tx-text-control-dotnet-wpf/performance/background-operations/llms-full.txt) — threads, progress and cancellation in WPF.
- [Performance Best Practices](https://docs.textcontrol.com/tx-text-control-dotnet-wpf/performance/best-practices/llms-full.txt) — all measures at a glance.

---

## Nearby

- **Previous:** [Document Loading and Conversion](https://docs.textcontrol.com/tx-text-control-dotnet-wpf/performance/loading-conversion/llms-full.txt)
- **Next:** [Performance Best Practices](https://docs.textcontrol.com/tx-text-control-dotnet-wpf/performance/best-practices/llms-full.txt)
