Table of Contents

Class DocxMailMerge

Namespace
DocToolkit
Assembly
DocToolkit.Docx.dll

Fills a Word mail-merge template — a document carrying MERGEFIELD instructions — from a set of named values.

public static class DocxMailMerge
Inheritance
DocxMailMerge
Inherited Members

Remarks

This is not FillRows(byte[], string, IEnumerable<IReadOnlyDictionary<string, string>>) under another name, and the difference is who authored the template.

marker · authored by
DocxEditor {{placeholder}} — plain text, typed by anyone in any editor. A convention this library invented.
this class a real Word field, produced by Insert → Merge Field, showing as «FirstName» with field shading. This library reads what Word already writes.

Neither substitutes for the other: a caller holding an existing Word mail-merge template has not one {{ in it, and a caller who does not own Word cannot author merge fields. Behind one name, the same call would do nothing at all depending on how the template happened to be authored.

Both on-disk field encodings are handled. Word writes the complex form — fldChar begin, instrText, separate, result, end — while most generators and hand-built documents emit the simple w:fldSimple. Measured: both merge.

Field names match case-insensitively, so a template field FirstName is filled by a key spelled firstname. Measured, and it is the engine's own matching rather than a property of the dictionary handed in.

A null value is refused rather than merged. Measured: the engine treats null as an empty string, writes nothing, and reports the field merged and the document complete — so a database NULL becomes a letter reading "Your balance is " that nothing flags. A caller who means "leave it blank" writes string.Empty and says so; the one who did not decide gets told. An empty string is accepted and merges. This holds for every method here that takes values, including the per-record and per-row collections the repeating and table-row methods take, where the refusal names which record carried the null.

Produced documents are flattened. The merged fields become ordinary text rather than live fields, so re-opening the result in Word cannot re-merge it and shows no field shading. Measured: the text is identical either way, so this is invisible to anything that reads a document back — which is why it is asserted structurally instead.

Methods

InspectTemplate(byte[])

Reads what docx asks for, without merging anything.

public static DocxMailMergeTemplate InspectTemplate(byte[] docx)

Parameters

docx byte[]

The template to read.

Returns

DocxMailMergeTemplate

Remarks

Use this to learn a template's field names, and to tell a sound template apart from one whose fields are malformed. A document carrying no merge fields reports none and is valid — which is how a caller catches having passed the wrong document, since merging one succeeds, changes nothing, and reports itself complete.

Exceptions

ArgumentNullException

docx is null.

ArgumentException

docx is empty.

DocumentConversionException

The document could not be opened or read.

InspectTemplateAsync(Stream, CancellationToken)

Reads what the template in source asks for, without merging anything. source is read to its end and is neither disposed, closed nor sought.

public static Task<DocxMailMergeTemplate> InspectTemplateAsync(Stream source, CancellationToken ct = default)

Parameters

source Stream

The template to read.

ct CancellationToken

Cancels before the document is read.

Returns

Task<DocxMailMergeTemplate>

Remarks

Use this to learn a template's field names, and to tell a sound template apart from one whose fields are malformed. A document carrying no merge fields reports none and is valid — which is how a caller catches having passed the wrong document, since merging one succeeds, changes nothing, and reports itself complete.

Exceptions

ArgumentNullException

source is null.

ArgumentException

source is not readable or held no bytes.

OperationCanceledException

ct was cancelled.

DocumentConversionException

The document could not be opened or read.

Merge(byte[], IReadOnlyDictionary<string, string>)

A copy of docx with every merge field filled from values.

public static byte[] Merge(byte[] docx, IReadOnlyDictionary<string, string> values)

Parameters

docx byte[]

The template to fill.

values IReadOnlyDictionary<string, string>

The value for each merge field, matched case-insensitively.

Returns

byte[]

Remarks

This refuses to produce a document that still has an unfilled field. That is the whole difference between this and MergeWithReport(byte[], IReadOnlyDictionary<string, string>), and it is deliberate: measured, an unfilled field survives as a live field and the document reads Your balance is «Balance» — valid, opening cleanly, and looking finished. Nothing about it says otherwise except a report, and a report only helps a caller who reads one.

Values naming fields the template does not have are ignored. A mistyped key is still caught, because the field it should have filled then goes unfilled and this throws.

Exceptions

ArgumentNullException

docx or values is null.

ArgumentException

docx is empty, or a value is null.

DocumentConversionException

A merge field received no value, or the document could not be read or written.

MergeAsync(Stream, Stream, IReadOnlyDictionary<string, string>, CancellationToken)

Writes a copy of the template in source to destination with every merge field filled from values. source is read to its end and destination is written; neither is disposed, closed nor sought.

public static Task MergeAsync(Stream source, Stream destination, IReadOnlyDictionary<string, string> values, CancellationToken ct = default)

Parameters

source Stream

The template to fill.

destination Stream

Receives the filled document.

values IReadOnlyDictionary<string, string>

The value for each merge field, matched case-insensitively.

ct CancellationToken

Cancels before the document is read, and while it is written.

Returns

Task

Remarks

This refuses to produce a document that still has an unfilled field. That is the whole difference between this and MergeWithReport(byte[], IReadOnlyDictionary<string, string>), and it is deliberate: measured, an unfilled field survives as a live field and the document reads Your balance is «Balance» — valid, opening cleanly, and looking finished. Nothing about it says otherwise except a report, and a report only helps a caller who reads one.

Values naming fields the template does not have are ignored. A mistyped key is still caught, because the field it should have filled then goes unfilled and this throws.

Exceptions

ArgumentNullException

A stream or values is null.

ArgumentException

A stream is unusable, source held no bytes, or a value is null.

OperationCanceledException

ct was cancelled.

DocumentConversionException

A merge field received no value, or the document could not be read or written.

MergeBatch(byte[], IEnumerable<IReadOnlyDictionary<string, string>>)

Fills docx once per entry in records, yielding each filled document in order.

public static IEnumerable<byte[]> MergeBatch(byte[] docx, IEnumerable<IReadOnlyDictionary<string, string>> records)

Parameters

docx byte[]

The template to fill, once per record.

records IEnumerable<IReadOnlyDictionary<string, string>>

One dictionary of values per output document, matched case-insensitively — see Merge(byte[], IReadOnlyDictionary<string, string>) for the matching rules, which apply here unchanged. An empty sequence yields no documents.

Returns

IEnumerable<byte[]>

Remarks

Strict, the same way Merge(byte[], IReadOnlyDictionary<string, string>) is strict — this refuses the moment a record is incomplete, mid-sequence. Everything already yielded before that point is unaffected; nothing after it runs. See MergeBatchWithReport(byte[], IEnumerable<IReadOnlyDictionary<string, string>>) for the lenient form, which never throws for an incomplete record.

This is lazy. Memory stays proportional to one document in flight, not the whole batch — records is walked one entry at a time as the caller enumerates the result, and each document's bytes are only held until the caller moves on to the next one.

Exceptions

ArgumentNullException

docx or records is null, or an individual record in records is null.

ArgumentException

docx is empty, or a record's value is null.

DocumentConversionException

The package could not be read or written, or a record is missing a value for a field the template requires — the message names the record's position (0-based) and the missing field(s).

MergeBatchAsync(byte[], IEnumerable<IReadOnlyDictionary<string, string>>, CancellationToken)

The async form of MergeBatch(byte[], IEnumerable<IReadOnlyDictionary<string, string>>) — see its documentation for exactly what is matched and how strictness works.

public static IAsyncEnumerable<byte[]> MergeBatchAsync(byte[] docx, IEnumerable<IReadOnlyDictionary<string, string>> records, CancellationToken ct = default)

Parameters

docx byte[]

The template to fill, once per record.

records IEnumerable<IReadOnlyDictionary<string, string>>

One dictionary of values per output document, matched case-insensitively — see Merge(byte[], IReadOnlyDictionary<string, string>) for the matching rules, which apply here unchanged. An empty sequence yields no documents.

ct CancellationToken

Cancels before the next record's merge runs.

Returns

IAsyncEnumerable<byte[]>

Remarks

Strict, the same way Merge(byte[], IReadOnlyDictionary<string, string>) is strict — this refuses the moment a record is incomplete, mid-sequence. Everything already yielded before that point is unaffected; nothing after it runs. See MergeBatchWithReport(byte[], IEnumerable<IReadOnlyDictionary<string, string>>) for the lenient form, which never throws for an incomplete record.

This is lazy. Memory stays proportional to one document in flight, not the whole batch — records is walked one entry at a time as the caller enumerates the result, and each document's bytes are only held until the caller moves on to the next one.

Exceptions

ArgumentNullException

docx or records is null, or an individual record in records is null. Unlike the synchronous MergeBatch(byte[], IEnumerable<IReadOnlyDictionary<string, string>>), this is not thrown until the caller starts enumerating the result — inherent to how an IAsyncEnumerable<T> iterator method defers its whole body, argument validation included, not a gap specific to this method.

ArgumentException

docx is empty, or a record's value is null. Unlike the synchronous MergeBatch(byte[], IEnumerable<IReadOnlyDictionary<string, string>>), this is not thrown until the caller starts enumerating the result — the same IAsyncEnumerable<T> deferral as the ArgumentNullException case above, not a gap specific to this method.

OperationCanceledException

ct was cancelled.

DocumentConversionException

The package could not be read or written, or a record is missing a value for a field the template requires — the message names the record's position (0-based) and the missing field(s).

MergeBatchToFiles(string, IEnumerable<IReadOnlyDictionary<string, string>>, Func<int, IReadOnlyDictionary<string, string>, string>)

Reads a template from templatePath, fills it once per entry in records, and writes each result to the path outputPathFactory returns for it.

public static IReadOnlyList<string> MergeBatchToFiles(string templatePath, IEnumerable<IReadOnlyDictionary<string, string>> records, Func<int, IReadOnlyDictionary<string, string>, string> outputPathFactory)

Parameters

templatePath string

The template to fill, once per record.

records IEnumerable<IReadOnlyDictionary<string, string>>

One dictionary of values per output document, matched case-insensitively — see Merge(byte[], IReadOnlyDictionary<string, string>) for the matching rules. An empty sequence writes nothing.

outputPathFactory Func<int, IReadOnlyDictionary<string, string>, string>

Given a record's 0-based index and its own values, returns the path its document is written to. Called once per record before any document is merged.

Returns

IReadOnlyList<string>

Remarks

Strict, the same way MergeBatch(byte[], IEnumerable<IReadOnlyDictionary<string, string>>) is strict — refuses the moment a record is incomplete, and nothing after that record is written. See MergeBatchToFilesWithReport(string, IEnumerable<IReadOnlyDictionary<string, string>>, Func<int, IReadOnlyDictionary<string, string>, string>) for the lenient form.

Every output path is checked for a collision against every other, before anything is written. Two records producing the same path is refused outright, naming both record indices — measured against the underlying engine's own batch writer, a collision silently overwrites one record's document with another's, with no exception and no warning. This refuses rather than risk it. Paths are compared as exact strings, not resolved or normalized — two different spellings of the same file (a relative path and its absolute equivalent, or two different cases on a case-insensitive filesystem) are not detected as a collision.

templatePath itself is not one of the paths this check compares against. outputPathFactory returning the template's own path is not treated as a collision — the template is already fully read into memory before any record is merged, so nothing about the write itself fails, but the result is that the template file on disk is silently overwritten with a merged record's output, with no exception and no warning.

Exceptions

ArgumentNullException

templatePath, records or outputPathFactory is null, or an individual record is null.

ArgumentException

templatePath is blank, a record's value is null, or outputPathFactory produced a null/blank path, or the same path for two different records.

FileNotFoundException

templatePath does not exist.

DocumentConversionException

The package could not be read or written, or a record is missing a value for a field the template requires — the message names the record's position (0-based) and the missing field(s).

MergeBatchToFilesAsync(string, IEnumerable<IReadOnlyDictionary<string, string>>, Func<int, IReadOnlyDictionary<string, string>, string>, CancellationToken)

The async form of MergeBatchToFiles(string, IEnumerable<IReadOnlyDictionary<string, string>>, Func<int, IReadOnlyDictionary<string, string>, string>) — see its documentation for exactly what is matched, how strictness works, and how the path-collision guard works.

public static Task<IReadOnlyList<string>> MergeBatchToFilesAsync(string templatePath, IEnumerable<IReadOnlyDictionary<string, string>> records, Func<int, IReadOnlyDictionary<string, string>, string> outputPathFactory, CancellationToken ct = default)

Parameters

templatePath string

The template to fill, once per record.

records IEnumerable<IReadOnlyDictionary<string, string>>

One dictionary of values per output document, matched case-insensitively — see Merge(byte[], IReadOnlyDictionary<string, string>) for the matching rules. An empty sequence writes nothing.

outputPathFactory Func<int, IReadOnlyDictionary<string, string>, string>

Given a record's 0-based index and its own values, returns the path its document is written to. Called once per record before any document is merged.

ct CancellationToken

Cancels before the template is read, and again before each record's merge.

Returns

Task<IReadOnlyList<string>>

Remarks

Strict, the same way MergeBatch(byte[], IEnumerable<IReadOnlyDictionary<string, string>>) is strict — refuses the moment a record is incomplete, and nothing after that record is written. See MergeBatchToFilesWithReport(string, IEnumerable<IReadOnlyDictionary<string, string>>, Func<int, IReadOnlyDictionary<string, string>, string>) for the lenient form.

Every output path is checked for a collision against every other, before anything is written. Two records producing the same path is refused outright, naming both record indices — measured against the underlying engine's own batch writer, a collision silently overwrites one record's document with another's, with no exception and no warning. This refuses rather than risk it. Paths are compared as exact strings, not resolved or normalized — two different spellings of the same file (a relative path and its absolute equivalent, or two different cases on a case-insensitive filesystem) are not detected as a collision.

templatePath itself is not one of the paths this check compares against. outputPathFactory returning the template's own path is not treated as a collision — the template is already fully read into memory before any record is merged, so nothing about the write itself fails, but the result is that the template file on disk is silently overwritten with a merged record's output, with no exception and no warning.

Exceptions

ArgumentNullException

templatePath, records or outputPathFactory is null, or an individual record is null.

ArgumentException

templatePath is blank, a record's value is null, or outputPathFactory produced a null/blank path, or the same path for two different records.

FileNotFoundException

templatePath does not exist.

OperationCanceledException

ct was cancelled before the template finished reading.

DocumentConversionException

The package could not be read or written, or a record is missing a value for a field the template requires — the message names the record's position (0-based) and the missing field(s).

MergeBatchToFilesWithReport(string, IEnumerable<IReadOnlyDictionary<string, string>>, Func<int, IReadOnlyDictionary<string, string>, string>)

Reads a template from templatePath, fills it once per entry in records, and writes each result to the path outputPathFactory returns for it — together with what happened to every field in it.

public static IReadOnlyList<DocxMailMergeFileBatchItem> MergeBatchToFilesWithReport(string templatePath, IEnumerable<IReadOnlyDictionary<string, string>> records, Func<int, IReadOnlyDictionary<string, string>, string> outputPathFactory)

Parameters

templatePath string

The template to fill, once per record.

records IEnumerable<IReadOnlyDictionary<string, string>>

One dictionary of values per output document, matched case-insensitively. An empty sequence writes nothing.

outputPathFactory Func<int, IReadOnlyDictionary<string, string>, string>

Given a record's 0-based index and its own values, returns the path its document is written to. Called once per record before any document is merged.

Returns

IReadOnlyList<DocxMailMergeFileBatchItem>

Remarks

The lenient half of the pair. This always writes a file for every record, complete or not, and never throws for an incomplete one — see MergeBatchToFiles(string, IEnumerable<IReadOnlyDictionary<string, string>>, Func<int, IReadOnlyDictionary<string, string>, string>) for the strict form. The path-collision refusal is unconditional and applies here too — see that method's remarks.

Exceptions

ArgumentNullException

templatePath, records or outputPathFactory is null, or an individual record is null.

ArgumentException

templatePath is blank, a record's value is null, outputPathFactory produced a null/blank path, or outputPathFactory produced the same path for two different records.

FileNotFoundException

templatePath does not exist.

DocumentConversionException

The package could not be read or written.

MergeBatchToFilesWithReportAsync(string, IEnumerable<IReadOnlyDictionary<string, string>>, Func<int, IReadOnlyDictionary<string, string>, string>, CancellationToken)

The async form of MergeBatchToFilesWithReport(string, IEnumerable<IReadOnlyDictionary<string, string>>, Func<int, IReadOnlyDictionary<string, string>, string>) — see its documentation for exactly what is matched, how strictness works, and how the path-collision guard works.

public static Task<IReadOnlyList<DocxMailMergeFileBatchItem>> MergeBatchToFilesWithReportAsync(string templatePath, IEnumerable<IReadOnlyDictionary<string, string>> records, Func<int, IReadOnlyDictionary<string, string>, string> outputPathFactory, CancellationToken ct = default)

Parameters

templatePath string

The template to fill, once per record.

records IEnumerable<IReadOnlyDictionary<string, string>>

One dictionary of values per output document, matched case-insensitively. An empty sequence writes nothing.

outputPathFactory Func<int, IReadOnlyDictionary<string, string>, string>

Given a record's 0-based index and its own values, returns the path its document is written to. Called once per record before any document is merged.

ct CancellationToken

Cancels before the template is read, and again before each record's merge.

Returns

Task<IReadOnlyList<DocxMailMergeFileBatchItem>>

Remarks

The lenient half of the pair. This always writes a file for every record, complete or not, and never throws for an incomplete one — see MergeBatchToFiles(string, IEnumerable<IReadOnlyDictionary<string, string>>, Func<int, IReadOnlyDictionary<string, string>, string>) for the strict form. The path-collision refusal is unconditional and applies here too — see that method's remarks.

Exceptions

ArgumentNullException

templatePath, records or outputPathFactory is null, or an individual record is null.

ArgumentException

templatePath is blank, a record's value is null, or outputPathFactory produced a null/blank path, or the same path for two different records.

FileNotFoundException

templatePath does not exist.

OperationCanceledException

ct was cancelled before the template finished reading.

DocumentConversionException

The package could not be read or written.

MergeBatchWithReport(byte[], IEnumerable<IReadOnlyDictionary<string, string>>)

Fills docx once per entry in records, yielding each record's document together with what happened to every field in it.

public static IEnumerable<DocxMailMergeBatchItem> MergeBatchWithReport(byte[] docx, IEnumerable<IReadOnlyDictionary<string, string>> records)

Parameters

docx byte[]

The template to fill, once per record.

records IEnumerable<IReadOnlyDictionary<string, string>>

One dictionary of values per output document, matched case-insensitively. An empty sequence yields no items.

Returns

IEnumerable<DocxMailMergeBatchItem>

Remarks

The lenient half of the pair. This always produces a document for every record, complete or not, and never throws for an incomplete one — see MergeBatch(byte[], IEnumerable<IReadOnlyDictionary<string, string>>) for the strict form.

This is lazy. Memory stays proportional to one item in flight, not the whole batch — records is walked one entry at a time as the caller enumerates the result, and each item is only held until the caller moves on to the next one.

Exceptions

ArgumentNullException

docx or records is null, or an individual record in records is null.

ArgumentException

docx is empty, or a record's value is null.

DocumentConversionException

The package could not be read or written.

MergeBatchWithReportAsync(byte[], IEnumerable<IReadOnlyDictionary<string, string>>, CancellationToken)

The async form of MergeBatchWithReport(byte[], IEnumerable<IReadOnlyDictionary<string, string>>) — see its documentation for exactly what is matched and how lenience works.

public static IAsyncEnumerable<DocxMailMergeBatchItem> MergeBatchWithReportAsync(byte[] docx, IEnumerable<IReadOnlyDictionary<string, string>> records, CancellationToken ct = default)

Parameters

docx byte[]

The template to fill, once per record.

records IEnumerable<IReadOnlyDictionary<string, string>>

One dictionary of values per output document, matched case-insensitively. An empty sequence yields no items.

ct CancellationToken

Cancels before the next record's merge runs.

Returns

IAsyncEnumerable<DocxMailMergeBatchItem>

Remarks

The lenient half of the pair. This always produces a document for every record, complete or not, and never throws for an incomplete one — see MergeBatch(byte[], IEnumerable<IReadOnlyDictionary<string, string>>) for the strict form.

This is lazy. Memory stays proportional to one item in flight, not the whole batch — records is walked one entry at a time as the caller enumerates the result, and each item is only held until the caller moves on to the next one.

Exceptions

ArgumentNullException

docx or records is null, or an individual record in records is null. Unlike the synchronous MergeBatchWithReport(byte[], IEnumerable<IReadOnlyDictionary<string, string>>), this is not thrown until the caller starts enumerating the result — inherent to how an IAsyncEnumerable<T> iterator method defers its whole body, argument validation included, not a gap specific to this method.

ArgumentException

docx is empty, or a record's value is null. Unlike the synchronous MergeBatchWithReport(byte[], IEnumerable<IReadOnlyDictionary<string, string>>), this is not thrown until the caller starts enumerating the result — the same IAsyncEnumerable<T> deferral as the ArgumentNullException case above, not a gap specific to this method.

OperationCanceledException

ct was cancelled.

DocumentConversionException

The package could not be read or written.

MergeConditional(byte[], IReadOnlyDictionary<string, bool>)

A copy of docx with every conditional block ({{#Name}}{{/Name}}) resolved — included and its markers removed when its condition is true, removed entirely (markers and content) when false.

public static byte[] MergeConditional(byte[] docx, IReadOnlyDictionary<string, bool> conditions)

Parameters

docx byte[]

The template to resolve.

conditions IReadOnlyDictionary<string, bool>

Whether to include each named block.

Returns

byte[]

Remarks

Refuses to produce a document with a condition the template asks for but conditions did not supply — measured: the underlying engine throws immediately for an unsupplied name, so this preflights via InspectTemplate(byte[]) and refuses before the document is ever touched, naming every missing condition at once. Use MergeConditionalWithReport(byte[], IReadOnlyDictionary<string, bool>) when you want the document anyway.

Run this before Merge(byte[], IReadOnlyDictionary<string, string>) when a template mixes conditional blocks with ordinary merge fields — a field inside a block that ends up excluded is removed along with the block, so running the field-level merge first would fill a field that this call is about to delete.

A marker paragraph must contain only the marker — trailing text on the same paragraph is not recognised as a marker at all, which is left as literal text in the output. This is inherent to the marker convention and cannot be detected here.

Exceptions

ArgumentNullException

An argument is null.

ArgumentException

docx is empty.

DocumentConversionException

A condition the template asks for was not supplied, the marker structure is unbalanced, or the document could not be read or written.

MergeConditionalAsync(Stream, Stream, IReadOnlyDictionary<string, bool>, CancellationToken)

Writes a copy of the template in source to destination with every conditional block resolved. source is read to its end and destination is written; neither is disposed, closed nor sought.

public static Task MergeConditionalAsync(Stream source, Stream destination, IReadOnlyDictionary<string, bool> conditions, CancellationToken ct = default)

Parameters

source Stream

The template to resolve.

destination Stream

Receives the resolved document.

conditions IReadOnlyDictionary<string, bool>

Whether to include each named block.

ct CancellationToken

Cancels before the document is read, and while it is written.

Returns

Task

Remarks

Refuses to produce a document with a condition the template asks for but conditions did not supply — measured: the underlying engine throws immediately for an unsupplied name, so this preflights via InspectTemplate(byte[]) and refuses before the document is ever touched, naming every missing condition at once. Use MergeConditionalWithReport(byte[], IReadOnlyDictionary<string, bool>) when you want the document anyway.

Run this before Merge(byte[], IReadOnlyDictionary<string, string>) when a template mixes conditional blocks with ordinary merge fields — a field inside a block that ends up excluded is removed along with the block, so running the field-level merge first would fill a field that this call is about to delete.

A marker paragraph must contain only the marker — trailing text on the same paragraph is not recognised as a marker at all, which is left as literal text in the output. This is inherent to the marker convention and cannot be detected here.

Exceptions

ArgumentNullException

A stream or conditions is null.

ArgumentException

A stream is unusable, or source held no bytes.

OperationCanceledException

ct was cancelled.

DocumentConversionException

A condition the template asks for was not supplied, the marker structure is unbalanced, or the document could not be read or written.

MergeConditionalWithReport(byte[], IReadOnlyDictionary<string, bool>)

A copy of docx with every conditional block resolved, together with which condition names the template asked for that conditions did not supply. Always produces a document, except when the marker structure is genuinely unbalanced.

public static DocxMailMergeBlockResult MergeConditionalWithReport(byte[] docx, IReadOnlyDictionary<string, bool> conditions)

Parameters

docx byte[]

The template to resolve.

conditions IReadOnlyDictionary<string, bool>

Whether to include each named block.

Returns

DocxMailMergeBlockResult

Remarks

Refuses to produce a document with a condition the template asks for but conditions did not supply — measured: the underlying engine throws immediately for an unsupplied name, so this preflights via InspectTemplate(byte[]) and refuses before the document is ever touched, naming every missing condition at once. Use MergeConditionalWithReport(byte[], IReadOnlyDictionary<string, bool>) when you want the document anyway.

Run this before Merge(byte[], IReadOnlyDictionary<string, string>) when a template mixes conditional blocks with ordinary merge fields — a field inside a block that ends up excluded is removed along with the block, so running the field-level merge first would fill a field that this call is about to delete.

A marker paragraph must contain only the marker — trailing text on the same paragraph is not recognised as a marker at all, which is left as literal text in the output. This is inherent to the marker convention and cannot be detected here.

An unsupplied condition is defaulted to false — the block is removed, exactly as if the caller had explicitly said not to show it — and named in MissingNames. That is the one difference from the strict overload: this never refuses for a missing name. It still refuses for a genuinely unbalanced marker structure — an unmatched or mismatched start/end pair makes the underlying engine throw regardless of what conditions contains, which is not something any dictionary content can work around.

Exceptions

ArgumentNullException

An argument is null.

ArgumentException

docx is empty.

DocumentConversionException

The marker structure is unbalanced, or the document could not be read or written.

MergeConditionalWithReportAsync(Stream, Stream, IReadOnlyDictionary<string, bool>, CancellationToken)

Writes a copy of the template in source to destination with every conditional block resolved, and returns which condition names were not supplied. source is read to its end and destination is written; neither is disposed, closed nor sought.

public static Task<DocxMailMergeBlockReport> MergeConditionalWithReportAsync(Stream source, Stream destination, IReadOnlyDictionary<string, bool> conditions, CancellationToken ct = default)

Parameters

source Stream

The template to resolve.

destination Stream

Receives the resolved document.

conditions IReadOnlyDictionary<string, bool>

Whether to include each named block.

ct CancellationToken

Cancels before the document is read, and while it is written.

Returns

Task<DocxMailMergeBlockReport>

Remarks

Refuses to produce a document with a condition the template asks for but conditions did not supply — measured: the underlying engine throws immediately for an unsupplied name, so this preflights via InspectTemplate(byte[]) and refuses before the document is ever touched, naming every missing condition at once. Use MergeConditionalWithReport(byte[], IReadOnlyDictionary<string, bool>) when you want the document anyway.

Run this before Merge(byte[], IReadOnlyDictionary<string, string>) when a template mixes conditional blocks with ordinary merge fields — a field inside a block that ends up excluded is removed along with the block, so running the field-level merge first would fill a field that this call is about to delete.

A marker paragraph must contain only the marker — trailing text on the same paragraph is not recognised as a marker at all, which is left as literal text in the output. This is inherent to the marker convention and cannot be detected here.

An unsupplied condition is defaulted to false — the block is removed, exactly as if the caller had explicitly said not to show it — and named in MissingNames. That is the one difference from the strict overload: this never refuses for a missing name. It still refuses for a genuinely unbalanced marker structure — an unmatched or mismatched start/end pair makes the underlying engine throw regardless of what conditions contains, which is not something any dictionary content can work around.

This returns a DocxMailMergeBlockReport rather than a DocxMailMergeBlockResult because the document went to destination.

Exceptions

ArgumentNullException

A stream or conditions is null.

ArgumentException

A stream is unusable, or source held no bytes.

OperationCanceledException

ct was cancelled.

DocumentConversionException

The marker structure is unbalanced, or the document could not be read or written.

MergeRepeating(byte[], IReadOnlyDictionary<string, IEnumerable<IReadOnlyDictionary<string, string>>>)

A copy of docx with every repeating block ({{#each Name}}{{/each Name}}) expanded once per entry in its region, merge fields inside each expansion filled from that entry.

public static byte[] MergeRepeating(byte[] docx, IReadOnlyDictionary<string, IEnumerable<IReadOnlyDictionary<string, string>>> regions)

Parameters

docx byte[]

The template to expand.

regions IReadOnlyDictionary<string, IEnumerable<IReadOnlyDictionary<string, string>>>

One sequence of value sets per named repeating region.

Returns

byte[]

Remarks

Refuses to produce a document with a region the template asks for but regions did not supply — the same reasoning as MergeConditional(byte[], IReadOnlyDictionary<string, bool>): the underlying engine throws immediately for an unsupplied name, so this preflights and refuses before the document is touched. Use MergeRepeatingWithReport(byte[], IReadOnlyDictionary<string, IEnumerable<IReadOnlyDictionary<string, string>>>) when you want the document anyway.

An empty sequence for a region removes the whole marked region — markers and content both — measured.

Run this before MergeConditional(byte[], IReadOnlyDictionary<string, bool>) and before Merge(byte[], IReadOnlyDictionary<string, string>), for the same reason: a conditional block or a merge field nested inside a repeating region only exists once this call has expanded it.

A missing field inside one record's expansion is not caught here — it leaves that field's raw placeholder in the generated row, silently, because ExecuteRepeatingBlocks has no report of its own. A follow-up call to Merge(byte[], IReadOnlyDictionary<string, string>) or MergeWithReport(byte[], IReadOnlyDictionary<string, string>) against the result — even with an empty values dictionary — finds and reports it, because it scans the whole document for remaining MERGEFIELDs. Measured.

Exceptions

ArgumentNullException

An argument is null, or an individual record in it is null.

ArgumentException

docx is empty, or a record's value is null.

DocumentConversionException

A region the template asks for was not supplied, the marker structure is unbalanced, or the document could not be read or written.

MergeRepeatingAsync(Stream, Stream, IReadOnlyDictionary<string, IEnumerable<IReadOnlyDictionary<string, string>>>, CancellationToken)

Writes a copy of the template in source to destination with every repeating block expanded. source is read to its end and destination is written; neither is disposed, closed nor sought.

public static Task MergeRepeatingAsync(Stream source, Stream destination, IReadOnlyDictionary<string, IEnumerable<IReadOnlyDictionary<string, string>>> regions, CancellationToken ct = default)

Parameters

source Stream

The template to expand.

destination Stream

Receives the expanded document.

regions IReadOnlyDictionary<string, IEnumerable<IReadOnlyDictionary<string, string>>>

One sequence of value sets per named repeating region.

ct CancellationToken

Cancels before the document is read, and while it is written.

Returns

Task

Remarks

Refuses to produce a document with a region the template asks for but regions did not supply — the same reasoning as MergeConditional(byte[], IReadOnlyDictionary<string, bool>): the underlying engine throws immediately for an unsupplied name, so this preflights and refuses before the document is touched. Use MergeRepeatingWithReport(byte[], IReadOnlyDictionary<string, IEnumerable<IReadOnlyDictionary<string, string>>>) when you want the document anyway.

An empty sequence for a region removes the whole marked region — markers and content both — measured.

Run this before MergeConditional(byte[], IReadOnlyDictionary<string, bool>) and before Merge(byte[], IReadOnlyDictionary<string, string>), for the same reason: a conditional block or a merge field nested inside a repeating region only exists once this call has expanded it.

A missing field inside one record's expansion is not caught here — it leaves that field's raw placeholder in the generated row, silently, because ExecuteRepeatingBlocks has no report of its own. A follow-up call to Merge(byte[], IReadOnlyDictionary<string, string>) or MergeWithReport(byte[], IReadOnlyDictionary<string, string>) against the result — even with an empty values dictionary — finds and reports it, because it scans the whole document for remaining MERGEFIELDs. Measured.

Exceptions

ArgumentNullException

A stream or regions is null, or an individual record in it is null.

ArgumentException

A stream is unusable, or source held no bytes, or a record's value is null.

OperationCanceledException

ct was cancelled.

DocumentConversionException

A region the template asks for was not supplied, the marker structure is unbalanced, or the document could not be read or written.

MergeRepeatingRegions(byte[], IReadOnlyDictionary<string, IEnumerable<DocxMailMergeBlockData>>)

A copy of docx with every repeating block expanded once per entry in its region, the same as MergeRepeating(byte[], IReadOnlyDictionary<string, IEnumerable<IReadOnlyDictionary<string, string>>>) — except each entry may itself carry further nested regions, for a template whose repeating blocks are nested inside one another.

public static byte[] MergeRepeatingRegions(byte[] docx, IReadOnlyDictionary<string, IEnumerable<DocxMailMergeBlockData>> regions)

Parameters

docx byte[]

The template to expand.

regions IReadOnlyDictionary<string, IEnumerable<DocxMailMergeBlockData>>

One sequence of block rows per named top-level repeating region.

Returns

byte[]

Remarks

Refuses to produce a document with a region the template asks for but regions did not supply — the same reasoning as MergeConditional(byte[], IReadOnlyDictionary<string, bool>): the underlying engine throws immediately for an unsupplied name, so this preflights and refuses before the document is touched. Use MergeRepeatingWithReport(byte[], IReadOnlyDictionary<string, IEnumerable<IReadOnlyDictionary<string, string>>>) when you want the document anyway.

An empty sequence for a region removes the whole marked region — markers and content both — measured.

Run this before MergeConditional(byte[], IReadOnlyDictionary<string, bool>) and before Merge(byte[], IReadOnlyDictionary<string, string>), for the same reason: a conditional block or a merge field nested inside a repeating region only exists once this call has expanded it.

A missing field inside one record's expansion is not caught here — it leaves that field's raw placeholder in the generated row, silently, because ExecuteRepeatingBlocks has no report of its own. A follow-up call to Merge(byte[], IReadOnlyDictionary<string, string>) or MergeWithReport(byte[], IReadOnlyDictionary<string, string>) against the result — even with an empty values dictionary — finds and reports it, because it scans the whole document for remaining MERGEFIELDs. Measured.

"Missing" is checked at every nesting level, not just the top one. RepeatingBlockNames is flat regardless of nesting depth — a nested marker's name appears in that list exactly like a top-level one, measured — so this walks regions and every Regions inside it recursively before comparing against what the template asks for.

Exceptions

ArgumentNullException

An argument is null, or an individual block row in it is null.

ArgumentException

docx is empty, or a block row's value is null.

DocumentConversionException

A region the template asks for was not supplied at any nesting level, or was supplied by one block row and omitted by a sibling — the preflight catches the first, the underlying engine throws for the second, which is a NAME-level report's blind spot and is why MergeRepeatingRegionsWithReport(byte[], IReadOnlyDictionary<string, IEnumerable<DocxMailMergeBlockData>>) reports nothing missing for it. Also when the marker structure is unbalanced, or the document could not be read or written.

MergeRepeatingRegionsAsync(Stream, Stream, IReadOnlyDictionary<string, IEnumerable<DocxMailMergeBlockData>>, CancellationToken)

Writes a copy of the template in source to destination with every nested repeating region expanded. source is read to its end and destination is written; neither is disposed, closed nor sought.

public static Task MergeRepeatingRegionsAsync(Stream source, Stream destination, IReadOnlyDictionary<string, IEnumerable<DocxMailMergeBlockData>> regions, CancellationToken ct = default)

Parameters

source Stream

The template to expand.

destination Stream

Receives the expanded document.

regions IReadOnlyDictionary<string, IEnumerable<DocxMailMergeBlockData>>

One sequence of block rows per named top-level repeating region.

ct CancellationToken

Cancels before the document is read, and while it is written.

Returns

Task

Remarks

Refuses to produce a document with a region the template asks for but regions did not supply — the same reasoning as MergeConditional(byte[], IReadOnlyDictionary<string, bool>): the underlying engine throws immediately for an unsupplied name, so this preflights and refuses before the document is touched. Use MergeRepeatingWithReport(byte[], IReadOnlyDictionary<string, IEnumerable<IReadOnlyDictionary<string, string>>>) when you want the document anyway.

An empty sequence for a region removes the whole marked region — markers and content both — measured.

Run this before MergeConditional(byte[], IReadOnlyDictionary<string, bool>) and before Merge(byte[], IReadOnlyDictionary<string, string>), for the same reason: a conditional block or a merge field nested inside a repeating region only exists once this call has expanded it.

A missing field inside one record's expansion is not caught here — it leaves that field's raw placeholder in the generated row, silently, because ExecuteRepeatingBlocks has no report of its own. A follow-up call to Merge(byte[], IReadOnlyDictionary<string, string>) or MergeWithReport(byte[], IReadOnlyDictionary<string, string>) against the result — even with an empty values dictionary — finds and reports it, because it scans the whole document for remaining MERGEFIELDs. Measured.

"Missing" is checked at every nesting level, not just the top one. RepeatingBlockNames is flat regardless of nesting depth — a nested marker's name appears in that list exactly like a top-level one, measured — so this walks regions and every Regions inside it recursively before comparing against what the template asks for.

Exceptions

ArgumentNullException

A stream or regions is null, or an individual block row in it is null.

ArgumentException

A stream is unusable, or source held no bytes, or a block row's value is null.

OperationCanceledException

ct was cancelled.

DocumentConversionException

A region the template asks for was not supplied at any nesting level, or was supplied by one block row and omitted by a sibling, the marker structure is unbalanced, or the document could not be read or written.

MergeRepeatingRegionsWithReport(byte[], IReadOnlyDictionary<string, IEnumerable<DocxMailMergeBlockData>>)

A copy of docx with every nested repeating region expanded, together with which region names the template asked for — at any nesting level — that regions did not supply. Always produces a document, except when the marker structure is genuinely unbalanced.

public static DocxMailMergeBlockResult MergeRepeatingRegionsWithReport(byte[] docx, IReadOnlyDictionary<string, IEnumerable<DocxMailMergeBlockData>> regions)

Parameters

docx byte[]

The template to expand.

regions IReadOnlyDictionary<string, IEnumerable<DocxMailMergeBlockData>>

One sequence of block rows per named top-level repeating region.

Returns

DocxMailMergeBlockResult

Remarks

Refuses to produce a document with a region the template asks for but regions did not supply — the same reasoning as MergeConditional(byte[], IReadOnlyDictionary<string, bool>): the underlying engine throws immediately for an unsupplied name, so this preflights and refuses before the document is touched. Use MergeRepeatingWithReport(byte[], IReadOnlyDictionary<string, IEnumerable<IReadOnlyDictionary<string, string>>>) when you want the document anyway.

An empty sequence for a region removes the whole marked region — markers and content both — measured.

Run this before MergeConditional(byte[], IReadOnlyDictionary<string, bool>) and before Merge(byte[], IReadOnlyDictionary<string, string>), for the same reason: a conditional block or a merge field nested inside a repeating region only exists once this call has expanded it.

A missing field inside one record's expansion is not caught here — it leaves that field's raw placeholder in the generated row, silently, because ExecuteRepeatingBlocks has no report of its own. A follow-up call to Merge(byte[], IReadOnlyDictionary<string, string>) or MergeWithReport(byte[], IReadOnlyDictionary<string, string>) against the result — even with an empty values dictionary — finds and reports it, because it scans the whole document for remaining MERGEFIELDs. Measured.

"Missing" is checked at every nesting level, not just the top one. RepeatingBlockNames is flat regardless of nesting depth — a nested marker's name appears in that list exactly like a top-level one, measured — so this walks regions and every Regions inside it recursively before comparing against what the template asks for.

An unsupplied region, at any nesting level, is defaulted to zero rows — the whole marked region is removed — and, when the name is unsupplied everywhere, it is also named in MissingNames. It still refuses for a genuinely unbalanced marker structure.

MissingNames answers about NAMES, not about individual block rows, and the difference shows up only under nesting. A name supplied by any row reads as supplied overall, so a template with Orders containing Lines, called with one order that carries Lines and a second that does not, reports nothing missing — the second order's Lines region is still defaulted to zero rows and removed, silently. Measured: without that default the underlying engine throws for that second row, exactly as it does for a name missing everywhere.

Unlike a missing individual MERGEFIELD, which a follow-up Merge(byte[], IReadOnlyDictionary<string, string>) or MergeWithReport pass can still detect as a raw leftover placeholder, a per-row missing nested region leaves no detectable artifact in the output — the result is byte-indistinguishable from a row that genuinely had no nested rows, so nothing downstream can recover this information.

Exceptions

ArgumentNullException

An argument is null, or an individual block row in it is null.

ArgumentException

docx is empty, or a block row's value is null.

DocumentConversionException

The marker structure is unbalanced, or the document could not be read or written.

MergeRepeatingRegionsWithReportAsync(Stream, Stream, IReadOnlyDictionary<string, IEnumerable<DocxMailMergeBlockData>>, CancellationToken)

Writes a copy of the template in source to destination with every nested repeating region expanded, and returns which region names were not supplied at any nesting level. source is read to its end and destination is written; neither is disposed, closed nor sought.

public static Task<DocxMailMergeBlockReport> MergeRepeatingRegionsWithReportAsync(Stream source, Stream destination, IReadOnlyDictionary<string, IEnumerable<DocxMailMergeBlockData>> regions, CancellationToken ct = default)

Parameters

source Stream

The template to expand.

destination Stream

Receives the expanded document.

regions IReadOnlyDictionary<string, IEnumerable<DocxMailMergeBlockData>>

One sequence of block rows per named top-level repeating region.

ct CancellationToken

Cancels before the document is read, and while it is written.

Returns

Task<DocxMailMergeBlockReport>

Remarks

Refuses to produce a document with a region the template asks for but regions did not supply — the same reasoning as MergeConditional(byte[], IReadOnlyDictionary<string, bool>): the underlying engine throws immediately for an unsupplied name, so this preflights and refuses before the document is touched. Use MergeRepeatingWithReport(byte[], IReadOnlyDictionary<string, IEnumerable<IReadOnlyDictionary<string, string>>>) when you want the document anyway.

An empty sequence for a region removes the whole marked region — markers and content both — measured.

Run this before MergeConditional(byte[], IReadOnlyDictionary<string, bool>) and before Merge(byte[], IReadOnlyDictionary<string, string>), for the same reason: a conditional block or a merge field nested inside a repeating region only exists once this call has expanded it.

A missing field inside one record's expansion is not caught here — it leaves that field's raw placeholder in the generated row, silently, because ExecuteRepeatingBlocks has no report of its own. A follow-up call to Merge(byte[], IReadOnlyDictionary<string, string>) or MergeWithReport(byte[], IReadOnlyDictionary<string, string>) against the result — even with an empty values dictionary — finds and reports it, because it scans the whole document for remaining MERGEFIELDs. Measured.

"Missing" is checked at every nesting level, not just the top one. RepeatingBlockNames is flat regardless of nesting depth — a nested marker's name appears in that list exactly like a top-level one, measured — so this walks regions and every Regions inside it recursively before comparing against what the template asks for.

An unsupplied region, at any nesting level, is defaulted to zero rows — the whole marked region is removed — and, when the name is unsupplied everywhere, it is also named in MissingNames. It still refuses for a genuinely unbalanced marker structure.

MissingNames answers about NAMES, not about individual block rows, and the difference shows up only under nesting. A name supplied by any row reads as supplied overall, so a template with Orders containing Lines, called with one order that carries Lines and a second that does not, reports nothing missing — the second order's Lines region is still defaulted to zero rows and removed, silently. Measured: without that default the underlying engine throws for that second row, exactly as it does for a name missing everywhere.

Unlike a missing individual MERGEFIELD, which a follow-up Merge(byte[], IReadOnlyDictionary<string, string>) or MergeWithReport pass can still detect as a raw leftover placeholder, a per-row missing nested region leaves no detectable artifact in the output — the result is byte-indistinguishable from a row that genuinely had no nested rows, so nothing downstream can recover this information.

This returns a DocxMailMergeBlockReport rather than a DocxMailMergeBlockResult because the document went to destination.

Exceptions

ArgumentNullException

A stream or regions is null, or an individual block row in it is null.

ArgumentException

A stream is unusable, or source held no bytes, or a block row's value is null.

OperationCanceledException

ct was cancelled.

DocumentConversionException

The marker structure is unbalanced, or the document could not be read or written.

MergeRepeatingWithReport(byte[], IReadOnlyDictionary<string, IEnumerable<IReadOnlyDictionary<string, string>>>)

A copy of docx with every repeating block expanded, together with which region names the template asked for that regions did not supply. Always produces a document, except when the marker structure is genuinely unbalanced.

public static DocxMailMergeBlockResult MergeRepeatingWithReport(byte[] docx, IReadOnlyDictionary<string, IEnumerable<IReadOnlyDictionary<string, string>>> regions)

Parameters

docx byte[]

The template to expand.

regions IReadOnlyDictionary<string, IEnumerable<IReadOnlyDictionary<string, string>>>

One sequence of value sets per named repeating region.

Returns

DocxMailMergeBlockResult

Remarks

Refuses to produce a document with a region the template asks for but regions did not supply — the same reasoning as MergeConditional(byte[], IReadOnlyDictionary<string, bool>): the underlying engine throws immediately for an unsupplied name, so this preflights and refuses before the document is touched. Use MergeRepeatingWithReport(byte[], IReadOnlyDictionary<string, IEnumerable<IReadOnlyDictionary<string, string>>>) when you want the document anyway.

An empty sequence for a region removes the whole marked region — markers and content both — measured.

Run this before MergeConditional(byte[], IReadOnlyDictionary<string, bool>) and before Merge(byte[], IReadOnlyDictionary<string, string>), for the same reason: a conditional block or a merge field nested inside a repeating region only exists once this call has expanded it.

A missing field inside one record's expansion is not caught here — it leaves that field's raw placeholder in the generated row, silently, because ExecuteRepeatingBlocks has no report of its own. A follow-up call to Merge(byte[], IReadOnlyDictionary<string, string>) or MergeWithReport(byte[], IReadOnlyDictionary<string, string>) against the result — even with an empty values dictionary — finds and reports it, because it scans the whole document for remaining MERGEFIELDs. Measured.

An unsupplied region is defaulted to zero rows — the whole marked region is removed, exactly as an explicitly empty sequence would be — and named in MissingNames. It still refuses for a genuinely unbalanced marker structure, which no dictionary content can work around.

Exceptions

ArgumentNullException

An argument is null, or an individual record in it is null.

ArgumentException

docx is empty, or a record's value is null.

DocumentConversionException

The marker structure is unbalanced, or the document could not be read or written.

MergeRepeatingWithReportAsync(Stream, Stream, IReadOnlyDictionary<string, IEnumerable<IReadOnlyDictionary<string, string>>>, CancellationToken)

Writes a copy of the template in source to destination with every repeating block expanded, and returns which region names were not supplied. source is read to its end and destination is written; neither is disposed, closed nor sought.

public static Task<DocxMailMergeBlockReport> MergeRepeatingWithReportAsync(Stream source, Stream destination, IReadOnlyDictionary<string, IEnumerable<IReadOnlyDictionary<string, string>>> regions, CancellationToken ct = default)

Parameters

source Stream

The template to expand.

destination Stream

Receives the expanded document.

regions IReadOnlyDictionary<string, IEnumerable<IReadOnlyDictionary<string, string>>>

One sequence of value sets per named repeating region.

ct CancellationToken

Cancels before the document is read, and while it is written.

Returns

Task<DocxMailMergeBlockReport>

Remarks

Refuses to produce a document with a region the template asks for but regions did not supply — the same reasoning as MergeConditional(byte[], IReadOnlyDictionary<string, bool>): the underlying engine throws immediately for an unsupplied name, so this preflights and refuses before the document is touched. Use MergeRepeatingWithReport(byte[], IReadOnlyDictionary<string, IEnumerable<IReadOnlyDictionary<string, string>>>) when you want the document anyway.

An empty sequence for a region removes the whole marked region — markers and content both — measured.

Run this before MergeConditional(byte[], IReadOnlyDictionary<string, bool>) and before Merge(byte[], IReadOnlyDictionary<string, string>), for the same reason: a conditional block or a merge field nested inside a repeating region only exists once this call has expanded it.

A missing field inside one record's expansion is not caught here — it leaves that field's raw placeholder in the generated row, silently, because ExecuteRepeatingBlocks has no report of its own. A follow-up call to Merge(byte[], IReadOnlyDictionary<string, string>) or MergeWithReport(byte[], IReadOnlyDictionary<string, string>) against the result — even with an empty values dictionary — finds and reports it, because it scans the whole document for remaining MERGEFIELDs. Measured.

An unsupplied region is defaulted to zero rows — the whole marked region is removed, exactly as an explicitly empty sequence would be — and named in MissingNames. It still refuses for a genuinely unbalanced marker structure, which no dictionary content can work around.

This returns a DocxMailMergeBlockReport rather than a DocxMailMergeBlockResult because the document went to destination.

Exceptions

ArgumentNullException

A stream or regions is null, or an individual record in it is null.

ArgumentException

A stream is unusable, or source held no bytes, or a record's value is null.

OperationCanceledException

ct was cancelled.

DocumentConversionException

The marker structure is unbalanced, or the document could not be read or written.

MergeTableRowGroups(byte[], int, int, int, IEnumerable<DocxMailMergeTableRowGroup>)

A copy of docx with a group/header row and its detail row template, both in the table at tableIndex, repeated once per group in groups.

public static byte[] MergeTableRowGroups(byte[] docx, int tableIndex, int groupTemplateRowIndex, int detailTemplateRowIndex, IEnumerable<DocxMailMergeTableRowGroup> groups)

Parameters

docx byte[]

The template to expand.

tableIndex int

Zero-based index of the table, in document order.

groupTemplateRowIndex int

Zero-based row index of the group/header row template.

detailTemplateRowIndex int

Zero-based row index of the detail row template.

groups IEnumerable<DocxMailMergeTableRowGroup>

One group/header value set, with its detail rows, per generated group.

Returns

byte[]

Remarks

Index-based, not marker-based, exactly as MergeTableRows(byte[], int, int, IEnumerable<IReadOnlyDictionary<string, string>>) is — and tableIndex counts the same set of tables, which is not the set ReadTable(byte[], int) indexes. See that method's remarks for the measured difference: a table wrapped in a content control is one of ReadTable's and none of this one's.

No strict/lenient split, for the same reason: a caller supplies groups directly rather than the template asking for a name that might go unsupplied, so there is nothing to preflight.

An empty groups removes both template rows. A group or detail record missing a field the corresponding row asks for leaves that field's raw placeholder in the generated row, silently, and is caught the same way — a follow-up call to Merge(byte[], IReadOnlyDictionary<string, string>) or MergeWithReport(byte[], IReadOnlyDictionary<string, string>) finds it.

Run this before MergeConditional(byte[], IReadOnlyDictionary<string, bool>) and before Merge(byte[], IReadOnlyDictionary<string, string>) — a conditional pass removes content, so running one first can change which table tableIndex lands on. Position is not a name.

Exceptions

ArgumentNullException

An argument is null, or an individual group or detail row in it is null.

ArgumentException

docx is empty, or a group or detail row's value is null.

DocumentConversionException

tableIndex, groupTemplateRowIndex or detailTemplateRowIndex is out of range, or the document could not be read or written.

MergeTableRowGroupsAsync(Stream, Stream, int, int, int, IEnumerable<DocxMailMergeTableRowGroup>, CancellationToken)

Writes a copy of the template in source to destination with the group and detail rows expanded. source is read to its end and destination is written; neither is disposed, closed nor sought.

public static Task MergeTableRowGroupsAsync(Stream source, Stream destination, int tableIndex, int groupTemplateRowIndex, int detailTemplateRowIndex, IEnumerable<DocxMailMergeTableRowGroup> groups, CancellationToken ct = default)

Parameters

source Stream

The template to expand.

destination Stream

Receives the expanded document.

tableIndex int

Zero-based index of the table, in document order.

groupTemplateRowIndex int

Zero-based row index of the group/header row template.

detailTemplateRowIndex int

Zero-based row index of the detail row template.

groups IEnumerable<DocxMailMergeTableRowGroup>

One group/header value set, with its detail rows, per generated group.

ct CancellationToken

Cancels before the document is read, and while it is written.

Returns

Task

Remarks

Index-based, not marker-based, exactly as MergeTableRows(byte[], int, int, IEnumerable<IReadOnlyDictionary<string, string>>) is — and tableIndex counts the same set of tables, which is not the set ReadTable(byte[], int) indexes. See that method's remarks for the measured difference: a table wrapped in a content control is one of ReadTable's and none of this one's.

No strict/lenient split, for the same reason: a caller supplies groups directly rather than the template asking for a name that might go unsupplied, so there is nothing to preflight.

An empty groups removes both template rows. A group or detail record missing a field the corresponding row asks for leaves that field's raw placeholder in the generated row, silently, and is caught the same way — a follow-up call to Merge(byte[], IReadOnlyDictionary<string, string>) or MergeWithReport(byte[], IReadOnlyDictionary<string, string>) finds it.

Run this before MergeConditional(byte[], IReadOnlyDictionary<string, bool>) and before Merge(byte[], IReadOnlyDictionary<string, string>) — a conditional pass removes content, so running one first can change which table tableIndex lands on. Position is not a name.

Exceptions

ArgumentNullException

A stream or groups is null, or an individual group or detail row in it is null.

ArgumentException

A stream is unusable, or source held no bytes, or a group or detail row's value is null.

OperationCanceledException

ct was cancelled.

DocumentConversionException

tableIndex, groupTemplateRowIndex or detailTemplateRowIndex is out of range, or the document could not be read or written.

MergeTableRows(byte[], int, int, IEnumerable<IReadOnlyDictionary<string, string>>)

A copy of docx with the row at templateRowIndex in the table at tableIndex repeated once per entry in rows, merge fields inside each generated row filled from that entry.

public static byte[] MergeTableRows(byte[] docx, int tableIndex, int templateRowIndex, IEnumerable<IReadOnlyDictionary<string, string>> rows)

Parameters

docx byte[]

The template to expand.

tableIndex int

Zero-based index of the table, in document order.

templateRowIndex int

Zero-based row index within that table to clone and bind.

rows IEnumerable<IReadOnlyDictionary<string, string>>

One value set per generated row.

Returns

byte[]

Remarks

Index-based, not marker-based — unlike MergeRepeating(byte[], IReadOnlyDictionary<string, IEnumerable<IReadOnlyDictionary<string, string>>>), there is no {{...}} convention for a table row; the underlying engine selects both the table and the row by position.

tableIndex counts the tables the underlying engine sees, which is NOT the same set ReadTable(byte[], int) indexes. Both skip a table nested inside another table's cell. Only ReadTable descends into a content control (w:sdt), so a document holding a control-wrapped table followed by an ordinary one gives ReadTable two tables and this method one — index 0 is the wrapped table to ReadTable and the ordinary table here, and index 1 is out of range here while ReadTable answers it. Measured; do not read one index off the other. Content controls are the only known divergence, so a template with none can use either count.

No strict/lenient split. A caller supplies rows directly rather than the template asking for a name that might go unsupplied, so there is nothing to preflight — matching FillRows(byte[], string, IEnumerable<IReadOnlyDictionary<string, string>>)'s own single-form shape exactly.

An empty rows removes the template row. A record missing a field the row asks for leaves that field's raw placeholder in the generated row, silently — the same as MergeRepeating(byte[], IReadOnlyDictionary<string, IEnumerable<IReadOnlyDictionary<string, string>>>)'s per-record behavior, and caught the same way: a follow-up call to Merge(byte[], IReadOnlyDictionary<string, string>) or MergeWithReport(byte[], IReadOnlyDictionary<string, string>) finds it.

Run this before MergeConditional(byte[], IReadOnlyDictionary<string, bool>) and before Merge(byte[], IReadOnlyDictionary<string, string>), and here the order is sharper than it is for the marker-based methods: a conditional pass removes content, so running one first can change which table tableIndex lands on. Position is not a name.

Exceptions

ArgumentNullException

An argument is null, or an individual row in it is null.

ArgumentException

docx is empty, or a row's value is null.

DocumentConversionException

tableIndex or templateRowIndex is out of range, or the document could not be read or written.

MergeTableRowsAsync(Stream, Stream, int, int, IEnumerable<IReadOnlyDictionary<string, string>>, CancellationToken)

Writes a copy of the template in source to destination with the table row expanded. source is read to its end and destination is written; neither is disposed, closed nor sought.

public static Task MergeTableRowsAsync(Stream source, Stream destination, int tableIndex, int templateRowIndex, IEnumerable<IReadOnlyDictionary<string, string>> rows, CancellationToken ct = default)

Parameters

source Stream

The template to expand.

destination Stream

Receives the expanded document.

tableIndex int

Zero-based index of the table, in document order.

templateRowIndex int

Zero-based row index within that table to clone and bind.

rows IEnumerable<IReadOnlyDictionary<string, string>>

One value set per generated row.

ct CancellationToken

Cancels before the document is read, and while it is written.

Returns

Task

Remarks

Index-based, not marker-based — unlike MergeRepeating(byte[], IReadOnlyDictionary<string, IEnumerable<IReadOnlyDictionary<string, string>>>), there is no {{...}} convention for a table row; the underlying engine selects both the table and the row by position.

tableIndex counts the tables the underlying engine sees, which is NOT the same set ReadTable(byte[], int) indexes. Both skip a table nested inside another table's cell. Only ReadTable descends into a content control (w:sdt), so a document holding a control-wrapped table followed by an ordinary one gives ReadTable two tables and this method one — index 0 is the wrapped table to ReadTable and the ordinary table here, and index 1 is out of range here while ReadTable answers it. Measured; do not read one index off the other. Content controls are the only known divergence, so a template with none can use either count.

No strict/lenient split. A caller supplies rows directly rather than the template asking for a name that might go unsupplied, so there is nothing to preflight — matching FillRows(byte[], string, IEnumerable<IReadOnlyDictionary<string, string>>)'s own single-form shape exactly.

An empty rows removes the template row. A record missing a field the row asks for leaves that field's raw placeholder in the generated row, silently — the same as MergeRepeating(byte[], IReadOnlyDictionary<string, IEnumerable<IReadOnlyDictionary<string, string>>>)'s per-record behavior, and caught the same way: a follow-up call to Merge(byte[], IReadOnlyDictionary<string, string>) or MergeWithReport(byte[], IReadOnlyDictionary<string, string>) finds it.

Run this before MergeConditional(byte[], IReadOnlyDictionary<string, bool>) and before Merge(byte[], IReadOnlyDictionary<string, string>), and here the order is sharper than it is for the marker-based methods: a conditional pass removes content, so running one first can change which table tableIndex lands on. Position is not a name.

Exceptions

ArgumentNullException

A stream or rows is null, or an individual row in it is null.

ArgumentException

A stream is unusable, or source held no bytes, or a row's value is null.

OperationCanceledException

ct was cancelled.

DocumentConversionException

tableIndex or templateRowIndex is out of range, or the document could not be read or written.

MergeWithReport(byte[], IReadOnlyDictionary<string, string>)

A copy of docx with every merge field filled from values, together with what happened to each one.

public static DocxMailMergeResult MergeWithReport(byte[] docx, IReadOnlyDictionary<string, string> values)

Parameters

docx byte[]

The template to fill.

values IReadOnlyDictionary<string, string>

The value for each merge field, matched case-insensitively.

Returns

DocxMailMergeResult

Remarks

The lenient half of the pair. This always produces a document, complete or not, and the report is how a caller learns which is which — see Document, which is worth reading before shipping one.

Exceptions

ArgumentNullException

docx or values is null.

ArgumentException

docx is empty, or a value is null.

DocumentConversionException

The document could not be read or written.

MergeWithReportAsync(Stream, Stream, IReadOnlyDictionary<string, string>, CancellationToken)

Writes a copy of the template in source to destination with every merge field filled, and returns what happened to each one. source is read to its end and destination is written; neither is disposed, closed nor sought.

public static Task<DocxMailMergeReport> MergeWithReportAsync(Stream source, Stream destination, IReadOnlyDictionary<string, string> values, CancellationToken ct = default)

Parameters

source Stream

The template to fill.

destination Stream

Receives the filled document.

values IReadOnlyDictionary<string, string>

The value for each merge field, matched case-insensitively.

ct CancellationToken

Cancels before the document is read, and while it is written.

Returns

Task<DocxMailMergeReport>

Remarks

The lenient half of the pair. This always produces a document, complete or not, and the report is how a caller learns which is which — see Document, which is worth reading before shipping one.

This returns a DocxMailMergeReport rather than a DocxMailMergeResult because the document went to destination. Handing it back a second time would buffer a whole document nobody asked for, which is the opposite of what a Stream overload is for.

Exceptions

ArgumentNullException

A stream or values is null.

ArgumentException

A stream is unusable, source held no bytes, or a value is null.

OperationCanceledException

ct was cancelled.

DocumentConversionException

The document could not be read or written.