Table of Contents

Class DocxEditor

Namespace
DocToolkit
Assembly
DocToolkit.dll

Creates, reads and edits Word (.docx) documents.

public static class DocxEditor
Inheritance
DocxEditor
Inherited Members

Methods

Create(IEnumerable<DocxBlock>)

Creates a document from blocks.

A DOCX can also be produced by converting HTML with HtmlToDocxConverter. This exists for the case where the content comes from data rather than from markup: there is no HTML to escape, so a value containing < cannot corrupt the document's structure, and the same blocks produce the same CONTENT on every machine — nothing here consults the current culture. Not the same BYTES: the OpenXml SDK mints fresh relationship ids per package, so two calls with identical blocks in the same process differ. Do not build a cache key, a content hash or a golden-file test on the bytes.

An empty sequence is valid and produces a valid empty document.

public static byte[] Create(IEnumerable<DocxBlock> blocks)

Parameters

blocks IEnumerable<DocxBlock>

The content, written in order.

Returns

byte[]

Examples

byte[] docx = DocxEditor.Create(new[]
{
    DocxBlock.Heading("Quarterly Report", 1),
    DocxBlock.Paragraph("Revenue was up 12%."),
    DocxBlock.Table(
        new[] { "Region", "Total" },
        new[] { new object?[] { "North", 1200 } }),
});

Remarks

The document is laid out on A4. Use Create(IEnumerable<DocxBlock>, PageSetup) for anything else.

Exceptions

ArgumentNullException

blocks is null.

ArgumentException

An element of blocks is null.

DocumentConversionException

The document could not be built.

Create(IEnumerable<DocxBlock>, PageSetup)

Builds a document from blocks, laid out on page. See Create(IEnumerable<DocxBlock>) for the block semantics — this overload applies identical logic and differs only in the paper.

public static byte[] Create(IEnumerable<DocxBlock> blocks, PageSetup page)

Parameters

blocks IEnumerable<DocxBlock>

The content, written in order.

page PageSetup

The page size, orientation and margins.

Returns

byte[]

Exceptions

ArgumentNullException

blocks or page is null.

ArgumentException

An element of blocks is null.

DocumentConversionException

The document could not be built.

CreateAsync(IEnumerable<DocxBlock>, PageSetup, Stream, CancellationToken)

Builds a document from blocks, laid out on page, and writes it to destination. See Create(IEnumerable<DocxBlock>, PageSetup) for the semantics.

destination is written, from its current position, and is not disposed, closed or sought — it belongs to the caller, and may be write-only and forward-only, such as an HTTP response body.

public static Task CreateAsync(IEnumerable<DocxBlock> blocks, PageSetup page, Stream destination, CancellationToken ct = default)

Parameters

blocks IEnumerable<DocxBlock>

The content, written in order.

page PageSetup

The page size, orientation and margins.

destination Stream

The stream the document is written to.

ct CancellationToken

Cancels the build and the write to destination.

Returns

Task

Exceptions

ArgumentNullException

Any argument is null.

ArgumentException

An element of blocks is null, or destination is not writable.

OperationCanceledException

ct was cancelled.

DocumentConversionException

The document could not be built or written.

CreateAsync(IEnumerable<DocxBlock>, Stream, CancellationToken)

Builds a document from blocks and writes it to destination. See Create(IEnumerable<DocxBlock>) for the block semantics — this overload applies identical logic, writing to destination instead of returning an array.

destination is written, from its current position, and is not disposed, closed or sought — it belongs to the caller, and may be write-only and forward-only, such as an HTTP response body.

public static Task CreateAsync(IEnumerable<DocxBlock> blocks, Stream destination, CancellationToken ct = default)

Parameters

blocks IEnumerable<DocxBlock>

The content, written in order.

destination Stream

The stream the document is written to.

ct CancellationToken

Cancels the build and the write to destination.

Returns

Task

Remarks

The document is laid out on A4. Use CreateAsync(IEnumerable<DocxBlock>, PageSetup, Stream, CancellationToken) for anything else.

Exceptions

ArgumentNullException

blocks or destination is null.

ArgumentException

An element of blocks is null, or destination is not writable.

OperationCanceledException

ct was cancelled.

DocumentConversionException

The document could not be built or written.

CreateToFileAsync(IEnumerable<DocxBlock>, PageSetup, string, CancellationToken)

Builds a document from blocks, laid out on page, and writes it to outputPath. See Create(IEnumerable<DocxBlock>, PageSetup) for the semantics.

The document is built completely before the output is opened, so a failed build cannot truncate a file that was already there.

public static Task CreateToFileAsync(IEnumerable<DocxBlock> blocks, PageSetup page, string outputPath, CancellationToken ct = default)

Parameters

blocks IEnumerable<DocxBlock>

The content, written in order.

page PageSetup

The page size, orientation and margins.

outputPath string

Where to write the document. Overwritten if it exists.

ct CancellationToken

Cancels the write to outputPath.

Returns

Task

Exceptions

ArgumentNullException

Any argument is null.

ArgumentException

outputPath is blank, or an element of blocks is null.

DirectoryNotFoundException

outputPath's directory does not exist.

OperationCanceledException

ct was cancelled.

DocumentConversionException

The document could not be built.

CreateToFileAsync(IEnumerable<DocxBlock>, string, CancellationToken)

Builds a document from blocks and writes it to outputPath. See Create(IEnumerable<DocxBlock>) for the block semantics.

Named CreateToFileAsync rather than a third CreateAsync overload, matching CreateToFileAsync(string, IEnumerable<IEnumerable<object?>>, string, CancellationToken): the distinct name keeps which kind of destination a call writes to visible at the call site, rather than resting on the argument type alone.

The document is built completely before the output is opened. That ordering is the reason a failed build cannot truncate a file that was already there, and it is pinned by FilePathOverloadTests rather than left as a comment — it survives only as long as nobody rewrites this into a streaming write.

public static Task CreateToFileAsync(IEnumerable<DocxBlock> blocks, string outputPath, CancellationToken ct = default)

Parameters

blocks IEnumerable<DocxBlock>

The content, written in order.

outputPath string

Where to write the document. Overwritten if it exists.

ct CancellationToken

Cancels the write to outputPath.

Returns

Task

Remarks

The document is laid out on A4. Use CreateToFileAsync(IEnumerable<DocxBlock>, PageSetup, string, CancellationToken) for anything else.

Exceptions

ArgumentNullException

blocks or outputPath is null.

ArgumentException

outputPath is blank, or an element of blocks is null.

DirectoryNotFoundException

outputPath's directory does not exist.

OperationCanceledException

ct was cancelled.

DocumentConversionException

The document could not be built.

ExtractText(byte[])

Returns the plain text of the document body. Headers, footers, footnotes and endnotes are not included — call ExtractText(byte[], bool) for those.

public static string ExtractText(byte[] docx)

Parameters

docx byte[]

Returns

string

Exceptions

ArgumentNullException

docx is null.

ArgumentException

docx is empty.

DocumentConversionException

The package could not be opened or read.

ExtractText(byte[], bool)

Returns the plain text of the document. When includeHeadersAndFooters is true the body text is followed by each header part and then each footer part, separated by newlines; footnotes and endnotes are never included.

public static string ExtractText(byte[] docx, bool includeHeadersAndFooters)

Parameters

docx byte[]
includeHeadersAndFooters bool

Returns

string

Exceptions

ArgumentNullException

docx is null.

ArgumentException

docx is empty.

DocumentConversionException

The package could not be opened or read.

ExtractTextAsync(Stream, bool, CancellationToken)

Reads a .docx from source and returns its plain text. When includeHeadersAndFooters is true the body text is followed by each header part and then each footer part; footnotes and endnotes are never included. source is read to its end and is neither disposed, closed nor sought.

public static Task<string> ExtractTextAsync(Stream source, bool includeHeadersAndFooters, CancellationToken ct = default)

Parameters

source Stream
includeHeadersAndFooters bool
ct CancellationToken

Returns

Task<string>

Exceptions

ArgumentNullException

source is null.

ArgumentException

source is not readable or held no bytes.

OperationCanceledException

ct was cancelled.

DocumentConversionException

The package could not be opened or read.

ExtractTextAsync(Stream, CancellationToken)

Reads a .docx from source and returns the plain text of its body. Headers, footers, footnotes and endnotes are not included — call ExtractTextAsync(Stream, bool, CancellationToken) for those. source is read to its end and is neither disposed, closed nor sought.

public static Task<string> ExtractTextAsync(Stream source, CancellationToken ct = default)

Parameters

source Stream
ct CancellationToken

Returns

Task<string>

Exceptions

ArgumentNullException

source is null.

ArgumentException

source is not readable or held no bytes.

OperationCanceledException

ct was cancelled.

DocumentConversionException

The package could not be opened or read.

ExtractTextAsync(string, bool, CancellationToken)

Reads a .docx from path and returns its text, optionally including headers and footers.

public static Task<string> ExtractTextAsync(string path, bool includeHeadersAndFooters, CancellationToken ct = default)

Parameters

path string

The .docx to read.

includeHeadersAndFooters bool

Whether to include header and footer text.

ct CancellationToken

Cancels the read.

Returns

Task<string>

The document's text.

Exceptions

ArgumentNullException

path is null.

ArgumentException

path is blank, or the file it names is empty.

FileNotFoundException

path does not exist.

DirectoryNotFoundException

path's directory does not exist.

OperationCanceledException

ct was cancelled.

DocumentConversionException

The document could not be processed.

ExtractTextAsync(string, CancellationToken)

Reads a .docx from path and returns its body text.

public static Task<string> ExtractTextAsync(string path, CancellationToken ct = default)

Parameters

path string

The .docx to read.

ct CancellationToken

Cancels the read.

Returns

Task<string>

The document's body text.

Exceptions

ArgumentNullException

path is null.

ArgumentException

path is blank, or the file it names is empty.

FileNotFoundException

path does not exist.

DirectoryNotFoundException

path's directory does not exist.

OperationCanceledException

ct was cancelled.

DocumentConversionException

The document could not be processed.

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

Expands a table row once per record, so a template can render a variable-length list such as invoice line items.

A row is a template row when one of its cells contains a placeholder prefixed with collection{{item.Desc}} when collection is item. Each record deep-clones that row, so every clone keeps the template's run formatting, cell shading and borders, and substitution runs through the same splicer ReplaceText(byte[], IReadOnlyDictionary<string, string>) uses — a placeholder split across runs is still replaced, and a hyperlink in a cell is left intact.

Keys are bare field names (Desc), not full placeholders — unlike ReplaceText(byte[], IReadOnlyDictionary<string, string>), whose keys are the placeholder text including braces. collection is already an argument, so repeating it in every key of every record would duplicate it many times over.

A placeholder with no matching key resolves to empty rather than staying visible. Placeholders for other prefixes are untouched, so a second call fills a second table. An empty rows removes the template row, and removes the whole table when that row was its only one — an empty frame left on the page reads worse than rendering nothing.

Compose with ReplaceText(byte[], IReadOnlyDictionary<string, string>) for document-level scalars, expanding rows first.

public static byte[] FillRows(byte[] docx, string collection, IEnumerable<IReadOnlyDictionary<string, string>> rows)

Parameters

docx byte[]
collection string
rows IEnumerable<IReadOnlyDictionary<string, string>>

Returns

byte[]

Exceptions

ArgumentNullException

Any argument is null.

ArgumentException

docx is empty, or collection is blank.

DocumentConversionException

The package could not be opened or edited, or no template row was found for collection — a mismatch between the call and the template is a bug in one of them, not a no-op.

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

Reads a .docx from source, expands the template row once per record, and writes the result to destination. See FillRows(byte[], string, IEnumerable<IReadOnlyDictionary<string, string>>) for what counts as a template row and how formatting survives — this overload applies the identical logic via streams instead of a byte array.

source is read to its end and destination is written; neither is disposed, closed or sought, and neither has to be seekable, so both may be sockets, files or HTTP message bodies.

public static Task FillRowsAsync(Stream source, string collection, IEnumerable<IReadOnlyDictionary<string, string>> rows, Stream destination, CancellationToken ct = default)

Parameters

source Stream

The stream the .docx package is read from.

collection string

The placeholder prefix marking the template row, without braces.

rows IEnumerable<IReadOnlyDictionary<string, string>>

One dictionary per record, keyed by bare field name.

destination Stream

The stream the edited .docx package is written to.

ct CancellationToken

Cancels the read, the edit and the write.

Returns

Task

Exceptions

ArgumentNullException

Any argument is null.

ArgumentException

source is not readable or held no bytes, destination is not writable, or collection is blank.

OperationCanceledException

ct was cancelled.

DocumentConversionException

The package could not be opened or edited, or no template row was found.

FillRowsAsync(string, string, string, IEnumerable<IReadOnlyDictionary<string, string>>, CancellationToken)

Reads a .docx from inputPath, expands one table row per record, and writes the result to outputPath. The two may be the same file: the updated bytes are computed in full before outputPath is opened, so a document that fails to process — cannot be read, or cannot be edited — leaves outputPath untouched. That guarantee does not extend to a failure during the write itself: a full disk, a cancellation, or the process dying mid-write can still leave a partial file, so in-place editing of an irreplaceable document is not crash-safe.

public static Task FillRowsAsync(string inputPath, string outputPath, string collection, IEnumerable<IReadOnlyDictionary<string, string>> rows, CancellationToken ct = default)

Parameters

inputPath string

The .docx to read.

outputPath string

Where to write the result. Overwritten if it exists.

collection string

The collection name used in the row's placeholders.

rows IEnumerable<IReadOnlyDictionary<string, string>>

One dictionary per record.

ct CancellationToken

Cancels the read and the write.

Returns

Task

Exceptions

ArgumentNullException

A path, collection or rows is null.

ArgumentException

A path is blank, or the file at inputPath is empty.

FileNotFoundException

inputPath does not exist.

DirectoryNotFoundException

inputPath's or outputPath's directory does not exist.

OperationCanceledException

ct was cancelled.

DocumentConversionException

The document could not be processed.

ReplaceImage(byte[], string, byte[], double?, double?)

Replaces every occurrence of placeholder with image, inline, across the body, headers, footers, footnotes and endnotes.

Only the matched text goes: text sharing a run with the placeholder keeps its place and its formatting, so Signed: {{sig}} (authorised) becomes Signed: , the image, then (authorised).

placeholder is the literal text including braces, like ReplaceText(byte[], IReadOnlyDictionary<string, string>) — and unlike FillRows(byte[], string, IEnumerable<IReadOnlyDictionary<string, string>>), whose keys are bare field names only because the collection name is already an argument there.

Size is in points. Omit both and the image's intrinsic size is used, read from its own header at 96 DPI. Give one and the other scales to preserve the aspect ratio. Give both and the image is stretched to fit — distortion is the caller's choice, not an error.

PNG and JPEG only, detected from the image's magic bytes rather than any filename.

public static byte[] ReplaceImage(byte[] docx, string placeholder, byte[] image, double? widthPoints = null, double? heightPoints = null)

Parameters

docx byte[]
placeholder string
image byte[]
widthPoints double?
heightPoints double?

Returns

byte[]

Exceptions

ArgumentNullException

Any of the three required arguments is null.

ArgumentException

docx or image is empty, or placeholder is blank.

ArgumentOutOfRangeException

A supplied size is zero or negative, or the resulting size is larger than a drawing extent can hold (2,147,483,647 EMU per side — about 2,348 inches). The upper bound also applies when the side that overflows is the one DERIVED from the aspect ratio rather than the one supplied.

DocumentConversionException

The image is neither PNG nor JPEG, the package could not be edited, or placeholder does not appear anywhere — a call matching nothing is a bug in the call or the template, not a no-op.

ReplaceImageAsync(Stream, string, byte[], Stream, double?, double?, CancellationToken)

Reads a .docx from source, replaces every occurrence of placeholder with image, and writes the result to destination. See ReplaceImage(byte[], string, byte[], double?, double?) for what is matched and how it is sized — this overload applies the identical logic via streams instead of a byte array.

source is read to its end and destination is written; neither is disposed, closed or sought, and neither has to be seekable, so both may be sockets, files or HTTP message bodies.

public static Task ReplaceImageAsync(Stream source, string placeholder, byte[] image, Stream destination, double? widthPoints = null, double? heightPoints = null, CancellationToken ct = default)

Parameters

source Stream

The stream the .docx package is read from.

placeholder string

The literal placeholder text, braces included.

image byte[]

PNG or JPEG bytes, identified by their magic bytes.

destination Stream

The stream the edited .docx package is written to.

widthPoints double?

Width in points, or null to derive it.

heightPoints double?

Height in points, or null to derive it.

ct CancellationToken

Cancels the read, the edit and the write.

Returns

Task

Exceptions

ArgumentNullException

Any argument is null.

ArgumentException

source is not readable or held no bytes, destination is not writable, image is empty, or placeholder is blank.

ArgumentOutOfRangeException

A supplied size is zero or negative, or the resulting size is larger than a drawing extent can hold (2,147,483,647 EMU per side — about 2,348 inches). The upper bound also applies when the side that overflows is the one DERIVED from the aspect ratio rather than the one supplied.

OperationCanceledException

ct was cancelled.

DocumentConversionException

The image is neither PNG nor JPEG, the package could not be edited, or the placeholder was not found.

ReplaceImageAsync(string, string, string, byte[], double?, double?, CancellationToken)

Reads a .docx from inputPath, replaces an image placeholder, and writes the result to outputPath. The two may be the same file: the updated bytes are computed in full before outputPath is opened, so a document that fails to process — cannot be read, or cannot be edited — leaves outputPath untouched. That guarantee does not extend to a failure during the write itself: a full disk, a cancellation, or the process dying mid-write can still leave a partial file, so in-place editing of an irreplaceable document is not crash-safe.

public static Task ReplaceImageAsync(string inputPath, string outputPath, string placeholder, byte[] image, double? widthPoints = null, double? heightPoints = null, CancellationToken ct = default)

Parameters

inputPath string

The .docx to read.

outputPath string

Where to write the result. Overwritten if it exists.

placeholder string

The placeholder text to replace.

image byte[]

PNG or JPEG bytes. The format is decided by the bytes, never a filename.

widthPoints double?

Width in points. Give one dimension and the other scales.

heightPoints double?

Height in points.

ct CancellationToken

Cancels the read and the write.

Returns

Task

Exceptions

ArgumentNullException

A path, placeholder or image is null.

ArgumentException

A path is blank, or the file at inputPath is empty.

FileNotFoundException

inputPath does not exist.

DirectoryNotFoundException

inputPath's or outputPath's directory does not exist.

OperationCanceledException

ct was cancelled.

DocumentConversionException

The document could not be processed.

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

Replaces every key with its value across the document body, its headers and footers, and its footnotes and endnotes.

Word routinely splits a single visible word across several <w:t> runs (spell-check state, formatting changes, a language switch), so a naive per-run replace misses any placeholder that straddles a run boundary. Substitution therefore happens against the concatenated text of each paragraph, but the result is spliced back into only the runs the match actually overlaps: runs outside a match — including the runs inside a <w:hyperlink> — keep their text and their formatting untouched. When a placeholder does straddle runs, the replacement value is written into the run holding its first character and so inherits that run's formatting.

Text boxes (<w:txbxContent>) nest whole paragraphs inside a run of the enclosing paragraph. They are treated as the separate paragraphs they are, so a placeholder inside a text box is replaced and a text box without one is left alone.

Keys are matched in a single left-to-right pass and the longest key wins at any given offset, so a substituted value is never rescanned for further placeholders.

public static byte[] ReplaceText(byte[] docx, IReadOnlyDictionary<string, string> replacements)

Parameters

docx byte[]
replacements IReadOnlyDictionary<string, string>

Returns

byte[]

Examples

byte[] filled = DocxEditor.ReplaceText(template, new Dictionary<string, string>
{
    ["{{customer}}"] = "Acme Ltd",
    ["{{number}}"] = "2026-114",
});

Exceptions

ArgumentNullException

Either argument is null.

ArgumentException

docx is empty.

DocumentConversionException

The package could not be opened or edited.

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

Reads a .docx from source, replaces every key with its value, and writes the result to destination. See ReplaceText(byte[], IReadOnlyDictionary<string, string>) for exactly what counts as a match and how formatting survives it — this overload applies the identical logic via source and destination instead of a byte array.

source is read to its end and destination is written; neither is disposed, closed or sought, and neither has to be seekable, so both may be sockets, files or HTTP message bodies.

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

Parameters

source Stream

The stream the .docx package is read from.

replacements IReadOnlyDictionary<string, string>

Each key is replaced by its value, longest key wins per match.

destination Stream

The stream the edited .docx package is written to.

ct CancellationToken

Cancels the read, the edit and the write.

Returns

Task

Exceptions

ArgumentNullException

Any argument is null.

ArgumentException

source is not readable or held no bytes, or destination is not writable.

OperationCanceledException

ct was cancelled.

DocumentConversionException

The package could not be opened or edited.

ReplaceTextAsync(string, string, IReadOnlyDictionary<string, string>, CancellationToken)

Reads a .docx from inputPath, substitutes placeholders, and writes the result to outputPath. The two may be the same file: the updated bytes are computed in full before outputPath is opened, so a document that fails to process — cannot be read, or cannot be edited — leaves outputPath untouched. That guarantee does not extend to a failure during the write itself: a full disk, a cancellation, or the process dying mid-write can still leave a partial file, so in-place editing of an irreplaceable document is not crash-safe.

public static Task ReplaceTextAsync(string inputPath, string outputPath, IReadOnlyDictionary<string, string> replacements, CancellationToken ct = default)

Parameters

inputPath string

The .docx to read.

outputPath string

Where to write the result. Overwritten if it exists.

replacements IReadOnlyDictionary<string, string>

Placeholder to replacement text.

ct CancellationToken

Cancels the read and the write.

Returns

Task

Exceptions

ArgumentNullException

A path or replacements is null.

ArgumentException

A path is blank, or the file at inputPath is empty.

FileNotFoundException

inputPath does not exist.

DirectoryNotFoundException

inputPath's or outputPath's directory does not exist.

OperationCanceledException

ct was cancelled.

DocumentConversionException

The document could not be processed.