11 min read 2316 words Updated Mar 17, 2026 Created Mar 17, 2026

C# OpenXML Coding Guide

Required Reading: This document contains critical rules to avoid compile errors. Must read before writing C# code.

For template-apply workflow, see guides/template-apply-workflow.md first.


1. Dependency Discipline

  • Keep core workflow dependency-free (Python stdlib + .NET).
  • Treat matplotlib, playwright, Pillow as optional.
  • Use lazy imports for optional libraries.
  • Degrade gracefully when optional features are unavailable.

2. OOXML Order Profiles

spec/ooxml_order.py uses layered constraints:

LayerMeaningTypical Use
MUSTSchema anchor orderAny generation
SHOULDCompatibility orderDefault repair profile
MAYOptional hintscompat/strict
VENDORImplementation-specificstrict diagnostics

Profiles: minimal, repair (default), compat, strict

python3 <skill-path>/docx_engine.py order pPr repair

3. String Encoding Rules (CRITICAL)

Core Principle: Keep text as-is, escape only when necessary

Default behavior: All Chinese, Japanese, Korean (CJK) characters are written directly in strings.

Only these characters require Unicode escaping:

CharacterUnicodeReason
" (Chinese left double quote)\u201cC# compiler treats it as string delimiter → CS1003
" (Chinese right double quote)\u201dSame as above
' (Chinese left single quote)\u2018May conflict with character literals
' (Chinese right single quote)\u2019Same as above

Wrong vs Correct Examples

// ❌ Wrong - Chinese quotes cause CS1003 compile error
new Text("Please click "OK" button")

// ✓ Correct - Only quotes use Unicode escaping, other Chinese stays as-is
new Text("Please click \u201cOK\u201d button")

// ✓ Correct - Book title marks, parentheses, colons used directly
new Text("See《User Manual》Chapter 3(Important):Notes")

Never Use Verbatim Strings @""

\u escaping does not work in @"" verbatim strings:

// ❌ Wrong - @"" doesn't escape \u, outputs literal \u201c
string text = @"She said\u201cHello\u201d";  // Output: She said\u201cHello\u201d

// ✓ Correct - Regular string, \u escapes properly
string text = "She said\u201cHello\u201d";   // Output: She said"Hello"

Long Text: Use + Concatenation

var para = new Text(
    "As urbanization accelerates, smart city construction has become a national priority. " +
    "The \u201cFourteenth Five-Year\u201d National Informatization Plan states: " +
    "\u201cDigital transformation shall drive production method reform.\u201d"
);

Characters That Don't Need Escaping

CategoryCharactersExample
Book title marks《 》"See《Guide》"
Chinese parentheses( )"(Note)"
Chinese punctuation:。,;!?、Use directly

4. Namespace Aliases (MANDATORY)

CRITICAL: DocumentFormat.OpenXml.Drawing and DocumentFormat.OpenXml.Wordprocessing contain identical class names (Paragraph, Run, Text, Table, etc.). Direct using causes CS0104 ambiguity errors.

// ✓ Correct - use aliases
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;  // Main namespace, no alias needed
using DW = DocumentFormat.OpenXml.Drawing.Wordprocessing;  // Anchor, Inline
using A = DocumentFormat.OpenXml.Drawing;                   // Graphic, Blip
using PIC = DocumentFormat.OpenXml.Drawing.Pictures;        // Picture
using C = DocumentFormat.OpenXml.Drawing.Charts;            // Chart

// Usage: DW.Anchor, A.Graphic, PIC.Picture, C.BarChart
// Wordprocessing types (Paragraph, Run, Text) need no prefix

// ❌ Wrong - causes ambiguity
using DocumentFormat.OpenXml.Drawing;           // Conflicts with Wordprocessing!
using DocumentFormat.OpenXml.Wordprocessing;

5. API Quick Reference

Common Wrong vs Correct Names

WrongCorrect
mainPart.StylesmainPart.AddNewPart<StyleDefinitionsPart>()
StyleBasedOnBasedOn
SpacingBefore / SpacingAfterSpacingBetweenLines
Alignment / ParagraphAlignmentJustification
JustificationValues.JustifyJustificationValues.Both (justified)
PageBreaknew Break { Type = BreakValues.Page }
LineSpacingSpacingBetweenLines
Indention (typo)Indentation
new FontSize { Val = 24 } (int)new FontSize { Val = "24" } (string)
StrikeThroughStrike
NumberingFormatValuesNumberFormatValues (no -ing)
new Level(0)new Level() { LevelIndex = 0 }
sectPr.TitlePage = new TitlePage()sectPr.Append(new TitlePage())

Paragraph Properties Quick Reference

PropertyClassExample
Space afterSpacingBetweenLinesnew SpacingBetweenLines { After = "200" }
Space beforeSpacingBetweenLinesnew SpacingBetweenLines { Before = "600" }
Line spacingSpacingBetweenLinesnew SpacingBetweenLines { Line = "360", LineRule = LineSpacingRuleValues.Auto }
First line indentIndentationnew Indentation { FirstLine = "420" }
CenterJustificationnew Justification { Val = JustificationValues.Center }
JustifyJustificationnew Justification { Val = JustificationValues.Both }

Common Code Patterns

// Centered heading
new Paragraph(
    new ParagraphProperties(
        new Justification { Val = JustificationValues.Center },
        new SpacingBetweenLines { After = "400", Before = "600" }
    ),
    new Run(new Text("Title"))
)

// Left-aligned body (first line indent)
new Paragraph(
    new ParagraphProperties(
        new Indentation { FirstLine = "420" },
        new SpacingBetweenLines { After = "200" }
    ),
    new Run(new Text("Body content"))
)

6. RunProperties Element Order (CRITICAL)

OpenXML ordering in RunProperties is profile-sensitive. In strict profile, use the sequence below to avoid validation drift.

Recommended strict-profile order (top to bottom):

#ElementExample
1rStylenew RunStyle { Val = "Heading1Char" }
2rFontsnew RunFonts { Ascii = "Arial", EastAsia = "SimSun" }
3bnew Bold()
4inew Italic()
5strikenew Strike()
6colornew Color { Val = "FF0000" }
7sznew FontSize { Val = "24" }
8szCsnew FontSizeComplexScript { Val = "24" }
9unew Underline { Val = UnderlineValues.Single }
10vertAlignnew VerticalTextAlignment { Val = VerticalPositionValues.Superscript }
// ❌ Wrong order - sz before color
new RunProperties(
    new FontSize { Val = "24" },
    new Color { Val = "666666" }
)

// ✓ Correct order - color before sz
new RunProperties(
    new RunFonts { EastAsia = "SimSun" },
    new Color { Val = "666666" },
    new FontSize { Val = "24" }
)

7. Type Conversions (CRITICAL)

OpenXML properties often require explicit type conversions.

Target TypeConversionExample
UInt32Value(UInt32Value)(uint)valuenew TableRowHeight { Val = (UInt32Value)(uint)400 }
Int32Value(Int32Value)valuenew Indentation { Left = (Int32Value)420 }
StringValuevalue.ToString()new FontSize { Val = "24" }
OnOffValuenew OnOffValue(true)new Bold { Val = new OnOffValue(true) }
EnumValue<T>Direct assignmentnew Justification { Val = JustificationValues.Center }
// ❌ Wrong - int cannot convert to UInt32Value
new TableRowHeight { Val = 400 }

// ✓ Correct
new TableRowHeight { Val = (UInt32Value)(uint)400 }

// ❌ Wrong - int in conditional
new TableRowHeight { Val = row == 0 ? 400 : 300 }

// ✓ Correct
new TableRowHeight { Val = (UInt32Value)(uint)(row == 0 ? 400 : 300) }

8. Value Constraints

PropertyTypeWrongCorrect
FontSize.ValInteger string"17.5""18" ✓ (9pt)
Indentation.FirstLineUInt32 (≥0)"-420""420"
Indentation.LeftUInt32 (≥0)"-420""420"

Negative indent solution: Use Hanging property:

// ❌ Wrong
new Indentation { FirstLine = "-420" }

// ✓ Correct
new Indentation { Hanging = "420", Left = "420" }

Unit Conversions

ConversionFormula
1 inch= 72 pt = 1440 Twips = 914400 EMU
1 pt= 20 Twips = 12700 EMU
1 cm≈ 567 Twips
1 Twip= 635 EMU
FontSize Val= pt × 2 (half-points)

Paper Sizes (Twips)

SizePortrait (W×H)Landscape
A316838 × 23811Swap + Orient
A411906 × 16838Swap + Orient
A58391 × 11906Swap + Orient
Letter12240 × 15840Swap + Orient

Landscape: PageSize { Width=H, Height=W, Orient=PageOrientationValues.Landscape }


9. Common Error Troubleshooting

Compile Errors

ErrorCauseSolution
CS1003 Chinese quotes"" treated as delimiterUse \u201c\u201d
CS0246 SpacingBeforeClass doesn't existUse SpacingBetweenLines
CS0246 AlignmentClass doesn't existUse Justification
CS0117 JustificationValues.JustifyEnum value doesn't existUse .Both
CS0246 LineSpacingClass doesn't existUse SpacingBetweenLines
CS0246 StrikeThroughWrong class nameUse Strike

Schema Validation Errors

ErrorCauseSolution
'br' invalid childBreak not inside Runnew Run(new Break { Type = BreakValues.Page })
bookmarkStart invalid child of pPrWrong Bookmark locationPlace directly in Paragraph, not in pPr
docPr id duplicatesHardcoded duplicate IDsUse global counter docPrId++

Table Errors

ErrorCauseSolution
Column width stretched by contentMissing TableCellWidthSet TableCellWidth { Type = Dxa } for every cell
Table skewedGridColumn doesn't match TableCellWidthEnsure values match
Exceeds pageTotal column width too largeKeep total under 9350 twips

⚠️ Table Width Matching Rules (CRITICAL)

GridColumn.Width and TableCellWidth.Width MUST use the same value and unit type.

// ❌ Wrong - GridColumn uses Pct, TableCellWidth uses Dxa
new TableGrid(
    new GridColumn { Width = "2500" },  // 50% in Pct
    new GridColumn { Width = "2500" }
);
new TableCellWidth { Width = "4680", Type = TableWidthUnitValues.Dxa }  // Mismatch!

// ✓ Correct - Both use Dxa with matching values
new TableGrid(
    new GridColumn { Width = "4680" },
    new GridColumn { Width = "4680" }
);
new TableCellWidth { Width = "4680", Type = TableWidthUnitValues.Dxa }

// ✓ Correct - Both use Pct (percentage × 50)
new TableGrid(
    new GridColumn { Width = "2500" },  // 50%
    new GridColumn { Width = "2500" }
);
new TableCellWidth { Width = "2500", Type = TableWidthUnitValues.Pct }

Complete table example:

var table = new Table();
int[] colWidths = { 2000, 3680, 3680 };  // Total = 9360 twips

// 1. TableProperties
table.Append(new TableProperties(
    new TableWidth { Width = "0", Type = TableWidthUnitValues.Auto },
    new TableLayout { Type = TableLayoutValues.Fixed }
));

// 2. TableGrid - define column widths
table.Append(new TableGrid(
    colWidths.Select(w => new GridColumn { Width = w.ToString() }).ToArray()
));

// 3. TableRow with matching TableCellWidth
var row = new TableRow();
foreach (var w in colWidths) {
    row.Append(new TableCell(
        new TableCellProperties(
            new TableCellWidth { Width = w.ToString(), Type = TableWidthUnitValues.Dxa }
        ),
        new Paragraph(new Run(new Text("Cell")))
    ));
}
table.Append(row);

10. Critical Code Snippets

Correct Bookmark Placement

// ❌ Wrong - Bookmark inside pPr
new Paragraph(
    new ParagraphProperties(
        new BookmarkStart { Id = "420", Name = "ChartAnchor_Q1" }  // Wrong!
    ),
    new Run(new Text("Q1 trend chart")),
    new BookmarkEnd { Id = "420" }
)

// ✓ Correct - Directly in Paragraph
new Paragraph(
    new ParagraphProperties(new ParagraphStyleId { Val = "FigureCaption" }),
    new BookmarkStart { Id = "420", Name = "ChartAnchor_Q1" },
    new Run(new Text("Chart A: Q1 Trend Overview")),
    new BookmarkEnd { Id = "420" }
)

In stream assembly terms, BookmarkStart/BookmarkEnd are content anchors and must stay in the paragraph payload stream, not inside paragraph property metadata (pPr).

docPr ID Uniqueness

// Class-level counter
private static uint _docPrId = 1;

// Increment on use
new DW.DocProperties { Id = _docPrId++, Name = "Image1" }

Dynamic Image Dimensions

// ❌ Never hardcode
long chartWidth = 6000000;
long chartHeight = 3375000;

// ✓ Read from PNG header
private static (int width, int height) GetPngDimensions(string path)
{
    using var fs = new FileStream(path, FileMode.Open, FileAccess.Read);
    fs.Seek(16, SeekOrigin.Begin);
    var buffer = new byte[8];
    fs.Read(buffer, 0, 8);
    int width = (buffer[0] << 24) | (buffer[1] << 16) | (buffer[2] << 8) | buffer[3];
    int height = (buffer[4] << 24) | (buffer[5] << 16) | (buffer[6] << 8) | buffer[7];
    return (width, height);
}

var (w, h) = GetPngDimensions(imagePath);
long displayWidth = 5000000L;
long displayHeight = displayWidth * h / w;

11. Golden Rule: Never Improvise API Calls

Use current source modules as API references. Prefer src/Core/*.cs and src/Templates/*.cs for class names, property names, and constructor patterns. When writing code:

✓ Correct approach:

  1. Find the corresponding API pattern in src/Core or src/Templates
  2. Reference its API call structure (class names, property assignments, element ordering)
  3. Adapt content and document structure to match user requirements — examples are API cookbooks, not mandatory templates

❌ Wrong approach:

  • Recall API names from memory and write directly
  • Infer property names from "common sense"
  • Use properties not found in any code examples

Document structure is flexible. Each helper in src/Core and each assembly segment in src/Templates is a reusable building block. Select and combine them based on the document's actual needs.

Table Creation Checklist

  • Based on Layout.Matrix(...) + Layout.ThreeLineTable(...) pattern?
  • Has TableGrid defining column widths?
  • Every cell has TableCellWidth?
  • TableCellWidth matches GridColumn width?
  • No properties not found in examples?

12. Extended Reference

Use current files as canonical references:

  • src/Core/Metrics.cs: pt/Twips/EMU/cm conversions
  • src/Core/Layout.cs: section/page/table layout helpers
  • src/Core/Fields.cs: TOC, cross-reference, bookmark, update-on-open field helpers
  • src/Core/Primitives.cs: text/paragraph primitives and style fragments
  • src/Templates/AcademicPaper.cs: long-form report assembly pattern
  • src/Templates/TechManual.cs: technical manual/table-heavy assembly pattern