Replace a phrase in a DOCX, even when its formatting changes halfway through.
dotnet add package RecursiveAI.OneDocument.DocumentsRequires .NET 10 (net10.0); .NET 9 and earlier aren’t supported.
using RecursiveAI.OneDocument.Documents; using var doc = Document.Open("invoice.docx"); doc.Replace("Northwind", "Contoso"); doc.SaveAs("updated.docx");
Writes updated.docx with “Contoso” in place of “Northwind”; the snippet prints nothing. The input file is in the zip.
Unzip the example, then dotnet run.
Full program
A Word run is a piece of text that shares the same formatting, such as a bold word inside a paragraph. Any .docx works: change the input file name and the phrase to find and replace.
invoice.docx is included in the example. Its paragraph is Invoice for Northwind: 120 EUR.; only “North” is bold.
using RecursiveAI.OneDocument.Documents; using var doc = Document.Open("invoice.docx"); var report = doc.Replace("Northwind", "Contoso"); doc.SaveAs("updated.docx"); Console.WriteLine($"Replaced: {report.Count}"); foreach (var issue in report.Issues) Console.WriteLine($"{issue.Code}: {issue.Message}"); Console.WriteLine(doc.ExtractText().Trim());
dotnet run prints:
Replaced: 1 Invoice for Contoso: 120 EUR.
Inspect report.Issues for matches left unchanged; an invalid regular expression raises ArgumentException.
Variations
Case-sensitive whole words
Replace the var report = doc.Replace(...) line in the full program with:
var report = doc.Replace("Northwind", "Contoso", new DocumentReplaceOptions { Search = new() { MatchCase = true, WholeWord = true } });
Output:
Replaced: 1 Invoice for Contoso: 120 EUR.
Related recipes
- Convert PowerPoint to PDF in C#
- Fill a Word template in C#
- Mail merge DOCX files in C#
- Extract text from DOCX, XLSX and PPTX in C#
The short answer
Use Document.Open, call doc.Replace, then save the changed document with SaveAs. OneDocument searches the text of each paragraph, joining its runs before matching. You don’t need to guess how Word split a name across formatting changes. The example deliberately splits “Northwind” between a bold run and an ordinary run, yet finds the complete name.
The return value is a ReplaceReport, not just a count. It lists each replacement and its location, plus each match left unchanged and its reason. The complete program prints the count and any issues so that a successful save isn’t mistaken for a successful replacement. See the find-and-replace documentation for the matching and editing contract.
How do I find and replace text in a Word document?
For a fixed phrase, use literal matching, as in the snippet at the top. Literal matching is case-insensitive in the invariant culture by default. Replacement text is used exactly as written, including dollar signs. If letter case matters, set MatchCase; if only an entire word should match, set WholeWord. The variation above combines both options. Word boundaries mean the neighboring characters are not letters, digits or underscores.
The default document search includes the body, tables, headers, footers, footnotes, endnotes, comments and text boxes. This is useful when a company name also appears in the footer. A replacement in an unexpected story is still a real replacement: inspect the report’s locations when you need to review the scope of a batch edit.
New text takes the properties of the first matched character’s run. This is a concrete formatting rule, not a promise that every original run boundary survives. In this example, the replacement starts where the bold part of the original name started. Surrounding text stays in its original runs. Parts with no match are not modified.
Save to a different path if you want the source file available for comparison. The call edits the open model; saving is the separate step that writes the result. For edits that reviewers must accept or reject, DocumentReplaceOptions.TrackedAuthor records replacements as tracked deletions and insertions.
Why isn’t Find and Replace working in Word?
Check the search boundaries first. A match can cross runs, hyperlinks and content controls inside a paragraph, but never crosses paragraphs. Paragraph marks cannot be used to join or split paragraphs through this API. Two words that look adjacent on a page may therefore belong to different searchable paragraphs.
Next, distinguish visible text from field instructions. Cached field results are searched as text. Field codes are searched only when IncludeFieldCodes is enabled, and each instruction is searched on its own. Replacing a cached result does not recompute the field or re-run a content control’s data binding. Text inside tracked deletions is not part of the current paragraph text.
Finally, distinguish literal patterns from regular expressions. UseRegex enables .NET regex syntax and replacement groups; it is not enabled by default. An invalid pattern throws rather than quietly reporting no matches. Regex execution is bounded by the configured timeout, and a timeout raises OfficeLimitException. Empty matches are skipped and matches don’t overlap. These rules explain common “found less than expected” results without resorting to editing the DOCX archive as raw XML.