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
docxbyte[]The template to read.
Returns
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
docxis null.- ArgumentException
docxis 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
sourceStreamThe template to read.
ctCancellationTokenCancels before the document is read.
Returns
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
sourceis null.- ArgumentException
sourceis not readable or held no bytes.- OperationCanceledException
ctwas 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
docxbyte[]The template to fill.
valuesIReadOnlyDictionary<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
docxorvaluesis null.- ArgumentException
docxis 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
sourceStreamThe template to fill.
destinationStreamReceives the filled document.
valuesIReadOnlyDictionary<string, string>The value for each merge field, matched case-insensitively.
ctCancellationTokenCancels before the document is read, and while it is written.
Returns
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
valuesis null.- ArgumentException
A stream is unusable,
sourceheld no bytes, or a value is null.- OperationCanceledException
ctwas 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
docxbyte[]The template to fill, once per record.
recordsIEnumerable<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
docxorrecordsis null, or an individual record inrecordsis null.- ArgumentException
docxis 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
docxbyte[]The template to fill, once per record.
recordsIEnumerable<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.
ctCancellationTokenCancels before the next record's merge runs.
Returns
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
docxorrecordsis null, or an individual record inrecordsis 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
docxis 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
ctwas 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
templatePathstringThe template to fill, once per record.
recordsIEnumerable<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.
outputPathFactoryFunc<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
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,recordsoroutputPathFactoryis null, or an individual record is null.- ArgumentException
templatePathis blank, a record's value is null, oroutputPathFactoryproduced a null/blank path, or the same path for two different records.- FileNotFoundException
templatePathdoes 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
templatePathstringThe template to fill, once per record.
recordsIEnumerable<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.
outputPathFactoryFunc<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.
ctCancellationTokenCancels before the template is read, and again before each record's merge.
Returns
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,recordsoroutputPathFactoryis null, or an individual record is null.- ArgumentException
templatePathis blank, a record's value is null, oroutputPathFactoryproduced a null/blank path, or the same path for two different records.- FileNotFoundException
templatePathdoes not exist.- OperationCanceledException
ctwas 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
templatePathstringThe template to fill, once per record.
recordsIEnumerable<IReadOnlyDictionary<string, string>>One dictionary of values per output document, matched case-insensitively. An empty sequence writes nothing.
outputPathFactoryFunc<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
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,recordsoroutputPathFactoryis null, or an individual record is null.- ArgumentException
templatePathis blank, a record's value is null,outputPathFactoryproduced a null/blank path, oroutputPathFactoryproduced the same path for two different records.- FileNotFoundException
templatePathdoes 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
templatePathstringThe template to fill, once per record.
recordsIEnumerable<IReadOnlyDictionary<string, string>>One dictionary of values per output document, matched case-insensitively. An empty sequence writes nothing.
outputPathFactoryFunc<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.
ctCancellationTokenCancels before the template is read, and again before each record's merge.
Returns
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,recordsoroutputPathFactoryis null, or an individual record is null.- ArgumentException
templatePathis blank, a record's value is null, oroutputPathFactoryproduced a null/blank path, or the same path for two different records.- FileNotFoundException
templatePathdoes not exist.- OperationCanceledException
ctwas 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
docxbyte[]The template to fill, once per record.
recordsIEnumerable<IReadOnlyDictionary<string, string>>One dictionary of values per output document, matched case-insensitively. An empty sequence yields no items.
Returns
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
docxorrecordsis null, or an individual record inrecordsis null.- ArgumentException
docxis 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
docxbyte[]The template to fill, once per record.
recordsIEnumerable<IReadOnlyDictionary<string, string>>One dictionary of values per output document, matched case-insensitively. An empty sequence yields no items.
ctCancellationTokenCancels before the next record's merge runs.
Returns
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
docxorrecordsis null, or an individual record inrecordsis 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
docxis 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
ctwas 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
docxbyte[]The template to resolve.
conditionsIReadOnlyDictionary<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
docxis 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
sourceStreamThe template to resolve.
destinationStreamReceives the resolved document.
conditionsIReadOnlyDictionary<string, bool>Whether to include each named block.
ctCancellationTokenCancels before the document is read, and while it is written.
Returns
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
conditionsis null.- ArgumentException
A stream is unusable, or
sourceheld no bytes.- OperationCanceledException
ctwas 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
docxbyte[]The template to resolve.
conditionsIReadOnlyDictionary<string, bool>Whether to include each named block.
Returns
Remarks
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
docxis 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
sourceStreamThe template to resolve.
destinationStreamReceives the resolved document.
conditionsIReadOnlyDictionary<string, bool>Whether to include each named block.
ctCancellationTokenCancels before the document is read, and while it is written.
Returns
Remarks
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
conditionsis null.- ArgumentException
A stream is unusable, or
sourceheld no bytes.- OperationCanceledException
ctwas 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
docxbyte[]The template to expand.
regionsIReadOnlyDictionary<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
docxis 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
sourceStreamThe template to expand.
destinationStreamReceives the expanded document.
regionsIReadOnlyDictionary<string, IEnumerable<IReadOnlyDictionary<string, string>>>One sequence of value sets per named repeating region.
ctCancellationTokenCancels before the document is read, and while it is written.
Returns
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
regionsis null, or an individual record in it is null.- ArgumentException
A stream is unusable, or
sourceheld no bytes, or a record's value is null.- OperationCanceledException
ctwas 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
docxbyte[]The template to expand.
regionsIReadOnlyDictionary<string, IEnumerable<DocxMailMergeBlockData>>One sequence of block rows per named top-level repeating region.
Returns
- byte[]
Remarks
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
docxis 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
sourceStreamThe template to expand.
destinationStreamReceives the expanded document.
regionsIReadOnlyDictionary<string, IEnumerable<DocxMailMergeBlockData>>One sequence of block rows per named top-level repeating region.
ctCancellationTokenCancels before the document is read, and while it is written.
Returns
Remarks
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
regionsis null, or an individual block row in it is null.- ArgumentException
A stream is unusable, or
sourceheld no bytes, or a block row's value is null.- OperationCanceledException
ctwas 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
docxbyte[]The template to expand.
regionsIReadOnlyDictionary<string, IEnumerable<DocxMailMergeBlockData>>One sequence of block rows per named top-level repeating region.
Returns
Remarks
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
docxis 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
sourceStreamThe template to expand.
destinationStreamReceives the expanded document.
regionsIReadOnlyDictionary<string, IEnumerable<DocxMailMergeBlockData>>One sequence of block rows per named top-level repeating region.
ctCancellationTokenCancels before the document is read, and while it is written.
Returns
Remarks
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
regionsis null, or an individual block row in it is null.- ArgumentException
A stream is unusable, or
sourceheld no bytes, or a block row's value is null.- OperationCanceledException
ctwas 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
docxbyte[]The template to expand.
regionsIReadOnlyDictionary<string, IEnumerable<IReadOnlyDictionary<string, string>>>One sequence of value sets per named repeating region.
Returns
Remarks
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
docxis 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
sourceStreamThe template to expand.
destinationStreamReceives the expanded document.
regionsIReadOnlyDictionary<string, IEnumerable<IReadOnlyDictionary<string, string>>>One sequence of value sets per named repeating region.
ctCancellationTokenCancels before the document is read, and while it is written.
Returns
Remarks
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
regionsis null, or an individual record in it is null.- ArgumentException
A stream is unusable, or
sourceheld no bytes, or a record's value is null.- OperationCanceledException
ctwas 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
docxbyte[]The template to expand.
tableIndexintZero-based index of the table, in document order.
groupTemplateRowIndexintZero-based row index of the group/header row template.
detailTemplateRowIndexintZero-based row index of the detail row template.
groupsIEnumerable<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
docxis empty, or a group or detail row's value is null.- DocumentConversionException
tableIndex,groupTemplateRowIndexordetailTemplateRowIndexis 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
sourceStreamThe template to expand.
destinationStreamReceives the expanded document.
tableIndexintZero-based index of the table, in document order.
groupTemplateRowIndexintZero-based row index of the group/header row template.
detailTemplateRowIndexintZero-based row index of the detail row template.
groupsIEnumerable<DocxMailMergeTableRowGroup>One group/header value set, with its detail rows, per generated group.
ctCancellationTokenCancels before the document is read, and while it is written.
Returns
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
groupsis null, or an individual group or detail row in it is null.- ArgumentException
A stream is unusable, or
sourceheld no bytes, or a group or detail row's value is null.- OperationCanceledException
ctwas cancelled.- DocumentConversionException
tableIndex,groupTemplateRowIndexordetailTemplateRowIndexis 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
docxbyte[]The template to expand.
tableIndexintZero-based index of the table, in document order.
templateRowIndexintZero-based row index within that table to clone and bind.
rowsIEnumerable<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
docxis empty, or a row's value is null.- DocumentConversionException
tableIndexortemplateRowIndexis 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
sourceStreamThe template to expand.
destinationStreamReceives the expanded document.
tableIndexintZero-based index of the table, in document order.
templateRowIndexintZero-based row index within that table to clone and bind.
rowsIEnumerable<IReadOnlyDictionary<string, string>>One value set per generated row.
ctCancellationTokenCancels before the document is read, and while it is written.
Returns
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
rowsis null, or an individual row in it is null.- ArgumentException
A stream is unusable, or
sourceheld no bytes, or a row's value is null.- OperationCanceledException
ctwas cancelled.- DocumentConversionException
tableIndexortemplateRowIndexis 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
docxbyte[]The template to fill.
valuesIReadOnlyDictionary<string, string>The value for each merge field, matched case-insensitively.
Returns
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
docxorvaluesis null.- ArgumentException
docxis 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
sourceStreamThe template to fill.
destinationStreamReceives the filled document.
valuesIReadOnlyDictionary<string, string>The value for each merge field, matched case-insensitively.
ctCancellationTokenCancels before the document is read, and while it is written.
Returns
Remarks
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
valuesis null.- ArgumentException
A stream is unusable,
sourceheld no bytes, or a value is null.- OperationCanceledException
ctwas cancelled.- DocumentConversionException
The document could not be read or written.