I’m trying to merge multiple Word (.docx) documents using OpenXML in C#. The merge itself works, but I’m having a persistent issue with numbered and bulleted lists.
Problem
When I copy paragraphs and their <w:numPr> references from the source document into the target, the numbering from different documents does not stay separate:
- If the first document has bullets and the second has numbers → all lists in the merged file become numbers.
- If the first document has numbers and the second has bullets → all lists in the merged file become bullets.
- If both have numbers, instead of restarting at 1, the numbering continues (e.g., doc1 ends at 4, doc2 starts at 5).
It seems that whichever list type is last added to numbering.xml in the target document overwrites the previous ones.
What I Tried
- Creating new <w:abstractNum> and <w:num> entries for each source document.
- Generating new nsid and name attributes per list to avoid collisions.
- Tracking mappings with dictionaries for numId and abstractNumId.
- Ensuring startOverride is set to 1 for each level.
In essence, this is the code I am calling and all the magic is happening in NumberHelper.MyCodeHelper(). I would like to avoid posting all that code and just give my XML output for each source and the numbering.xml of the target document and maybe someone will be able to tell me where I am going wrong because as I understand it, the XML doesn't look to have any glaring ID issues.
var targetDoc = targetWml
NumberHelper.ResetGlobalMaps();
foreach (var sourceDoc in sourceDocs)
{
var result = NumberHelper.MyCodeHelper(WordprocessingDocument sourceDoc, WmlDocument targetDoc);
targetDoc = result.UpdatedDoc;
updatedParagraphs = result.UpdatedParagraphs;
//I then have some code here that I am using to insert the result.UpdatedPargraphs into the targetDoc. This code is not having any issues. It is a numbering.xml issue, I am almost certain.
}
Source1 XML that I am inserting and Image of its appearance in document.
<w:body xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:p xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:pPr>
<w:numPr>
<w:ilvl w:val="0" />
<w:numId w:val="1" />
</w:numPr>
<w:spacing w:before="0" w:after="0" w:afterAutospacing="0" w:line="240" />
<w:rPr />
</w:pPr>
<w:r>
<w:rPr>
<w:rFonts w:ascii="Times New Roman" />
<w:sz w:val="24" />
</w:rPr>
<w:t xml:space="preserve">This is atest</w:t>
</w:r>
</w:p>
<w:p xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:pPr>
<w:numPr>
<w:ilvl w:val="0" />
<w:numId w:val="1" />
</w:numPr>
<w:spacing w:before="0" w:after="0" w:afterAutospacing="0" w:line="240" />
<w:rPr />
<w:ind w:right="0" />
</w:pPr>
<w:r>
<w:rPr>
<w:rFonts w:ascii="Times New Roman" />
<w:sz w:val="24" />
</w:rPr>
<w:t xml:space="preserve">asdfads</w:t>
</w:r>
</w:p>
<w:p xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:pPr>
<w:numPr>
<w:ilvl w:val="0" />
<w:numId w:val="1" />
</w:numPr>
<w:spacing w:before="0" w:after="0" w:afterAutospacing="0" w:line="240" />
<w:rPr />
<w:ind w:right="0" />
</w:pPr>
<w:r>
<w:rPr>
<w:rFonts w:ascii="Times New Roman" />
<w:sz w:val="24" />
</w:rPr>
<w:t xml:space="preserve">fasdfasdfad</w:t>
</w:r>
</w:p>
<w:p xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:pPr>
<w:numPr>
<w:ilvl w:val="0" />
<w:numId w:val="1" />
</w:numPr>
<w:spacing w:before="0" w:after="0" w:afterAutospacing="0" w:line="240" />
<w:rPr />
<w:ind w:right="0" />
</w:pPr>
<w:r>
<w:rPr>
<w:rFonts w:ascii="Times New Roman" />
<w:sz w:val="24" />
</w:rPr>
<w:t xml:space="preserve">fasdfads</w:t>
</w:r>
</w:p>
<w:p xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:pPr>
<w:numPr>
<w:ilvl w:val="1" />
<w:numId w:val="1" />
</w:numPr>
<w:spacing w:before="0" w:after="0" w:afterAutospacing="0" w:line="240" />
<w:rPr />
<w:ind w:right="0" />
</w:pPr>
<w:r>
<w:rPr>
<w:rFonts w:ascii="Times New Roman" />
<w:sz w:val="24" />
</w:rPr>
<w:t xml:space="preserve">asdfasdf</w:t>
</w:r>
</w:p>
<w:p xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:pPr>
<w:numPr>
<w:ilvl w:val="1" />
<w:numId w:val="1" />
</w:numPr>
<w:spacing w:before="0" w:after="0" w:afterAutospacing="0" w:line="240" />
<w:rPr />
<w:ind w:right="0" />
</w:pPr>
<w:r>
<w:rPr>
<w:rFonts w:ascii="Times New Roman" />
<w:sz w:val="24" />
</w:rPr>
<w:t xml:space="preserve">asdfasdfsdf</w:t>
</w:r>
</w:p>
<w:p xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:pPr>
<w:numPr>
<w:ilvl w:val="1" />
<w:numId w:val="1" />
</w:numPr>
<w:spacing w:before="0" w:after="0" w:afterAutospacing="0" w:line="240" />
<w:rPr />
<w:ind w:right="0" />
</w:pPr>
<w:r>
<w:rPr>
<w:rFonts w:ascii="Times New Roman" />
<w:sz w:val="24" />
</w:rPr>
<w:t xml:space="preserve">asdfdsaf</w:t>
</w:r>
</w:p>
<w:p xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:pPr>
<w:numPr>
<w:ilvl w:val="2" />
<w:numId w:val="1" />
</w:numPr>
<w:spacing w:before="0" w:after="0" w:afterAutospacing="0" w:line="240" />
<w:rPr />
<w:ind w:right="0" />
</w:pPr>
<w:r>
<w:rPr>
<w:rFonts w:ascii="Times New Roman" />
<w:sz w:val="24" />
</w:rPr>
<w:t xml:space="preserve">adsfasdfasdf</w:t>
</w:r>
</w:p>
<w:p xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:pPr>
<w:numPr>
<w:ilvl w:val="1" />
<w:numId w:val="1" />
</w:numPr>
<w:spacing w:before="0" w:after="0" w:afterAutospacing="0" w:line="240" />
<w:rPr />
<w:ind w:right="0" />
</w:pPr>
<w:r>
<w:rPr>
<w:rFonts w:ascii="Times New Roman" />
<w:sz w:val="24" />
</w:rPr>
<w:t xml:space="preserve">asdfasdfasdf</w:t>
</w:r>
</w:p>
<w:p xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:pPr>
<w:numPr>
<w:ilvl w:val="0" />
<w:numId w:val="1" />
</w:numPr>
<w:spacing w:before="0" w:after="0" w:afterAutospacing="0" w:line="240" />
<w:rPr />
<w:ind w:right="0" />
</w:pPr>
<w:r>
<w:rPr>
<w:rFonts w:ascii="Times New Roman" />
<w:sz w:val="24" />
</w:rPr>
<w:t xml:space="preserve">asdfadsfasdfdsafasdf</w:t>
</w:r>
</w:p></w:body>
Source2 XML that I am inserting and Image of its appearance in document.
<w:body xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:p xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:pPr>
<w:numPr>
<w:ilvl w:val="0" />
<w:numId w:val="2" />
</w:numPr>
<w:spacing w:before="0" w:after="0" w:afterAutospacing="0" w:line="240" />
<w:rPr />
</w:pPr>
<w:r>
<w:rPr>
<w:rFonts w:ascii="Times New Roman" />
<w:sz w:val="24" />
</w:rPr>
<w:t xml:space="preserve">This iadfasdf adf</w:t>
</w:r>
</w:p>
<w:p xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:pPr>
<w:numPr>
<w:ilvl w:val="0" />
<w:numId w:val="2" />
</w:numPr>
<w:spacing w:before="0" w:after="0" w:afterAutospacing="0" w:line="240" />
<w:rPr />
<w:ind w:right="0" />
</w:pPr>
<w:r>
<w:rPr>
<w:rFonts w:ascii="Times New Roman" />
<w:sz w:val="24" />
</w:rPr>
<w:t xml:space="preserve">asdfasd</w:t>
</w:r>
</w:p>
<w:p xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:pPr>
<w:numPr>
<w:ilvl w:val="0" />
<w:numId w:val="2" />
</w:numPr>
<w:spacing w:before="0" w:after="0" w:afterAutospacing="0" w:line="240" />
<w:rPr />
<w:ind w:right="0" />
</w:pPr>
<w:r>
<w:rPr>
<w:rFonts w:ascii="Times New Roman" />
<w:sz w:val="24" />
</w:rPr>
<w:t xml:space="preserve">fasdfasdf</w:t>
</w:r>
</w:p>
<w:p xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:pPr>
<w:numPr>
<w:ilvl w:val="1" />
<w:numId w:val="2" />
</w:numPr>
<w:spacing w:before="0" w:after="0" w:afterAutospacing="0" w:line="240" />
<w:rPr />
<w:ind w:right="0" />
</w:pPr>
<w:r>
<w:rPr>
<w:rFonts w:ascii="Times New Roman" />
<w:sz w:val="24" />
</w:rPr>
<w:t xml:space="preserve">asdfasdfasd</w:t>
</w:r>
</w:p>
<w:p xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:pPr>
<w:numPr>
<w:ilvl w:val="1" />
<w:numId w:val="2" />
</w:numPr>
<w:spacing w:before="0" w:after="0" w:afterAutospacing="0" w:line="240" />
<w:rPr />
<w:ind w:right="0" />
</w:pPr>
<w:r>
<w:rPr>
<w:rFonts w:ascii="Times New Roman" />
<w:sz w:val="24" />
</w:rPr>
<w:t xml:space="preserve">fasdfasd</w:t>
</w:r>
</w:p>
<w:p xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:pPr>
<w:numPr>
<w:ilvl w:val="1" />
<w:numId w:val="2" />
</w:numPr>
<w:spacing w:before="0" w:after="0" w:afterAutospacing="0" w:line="240" />
<w:rPr />
<w:ind w:right="0" />
</w:pPr>
<w:r>
<w:rPr>
<w:rFonts w:ascii="Times New Roman" />
<w:sz w:val="24" />
</w:rPr>
<w:t xml:space="preserve">fadsafs</w:t>
</w:r>
</w:p>
<w:p xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:pPr>
<w:numPr>
<w:ilvl w:val="2" />
<w:numId w:val="2" />
</w:numPr>
<w:spacing w:before="0" w:after="0" w:afterAutospacing="0" w:line="240" />
<w:rPr />
<w:ind w:right="0" />
</w:pPr>
<w:r>
<w:rPr>
<w:rFonts w:ascii="Times New Roman" />
<w:sz w:val="24" />
</w:rPr>
<w:t xml:space="preserve">adsfasdfasd</w:t>
</w:r>
</w:p>
<w:p xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:pPr>
<w:numPr>
<w:ilvl w:val="0" />
<w:numId w:val="2" />
</w:numPr>
<w:spacing w:before="0" w:after="0" w:afterAutospacing="0" w:line="240" />
<w:rPr />
<w:ind w:right="0" />
</w:pPr>
<w:r>
<w:rPr>
<w:rFonts w:ascii="Times New Roman" />
<w:sz w:val="24" />
</w:rPr>
<w:t xml:space="preserve">asdfa</w:t>
</w:r>
</w:p>
<w:p xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:pPr>
<w:numPr>
<w:ilvl w:val="0" />
<w:numId w:val="2" />
</w:numPr>
<w:spacing w:before="0" w:after="0" w:afterAutospacing="0" w:line="240" />
<w:rPr />
<w:ind w:right="0" />
</w:pPr>
<w:r>
<w:rPr>
<w:rFonts w:ascii="Times New Roman" />
<w:sz w:val="24" />
</w:rPr>
<w:t xml:space="preserve">sdfasdfadsf</w:t>
</w:r>
</w:p>
<w:p xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:pPr>
<w:numPr>
<w:ilvl w:val="0" />
<w:numId w:val="2" />
</w:numPr>
<w:spacing w:before="0" w:after="0" w:afterAutospacing="0" w:line="240" />
<w:rPr />
<w:ind w:right="0" />
</w:pPr>
<w:r>
<w:rPr>
<w:rFonts w:ascii="Times New Roman" />
<w:sz w:val="24" />
</w:rPr>
<w:t xml:space="preserve">asdfsdf</w:t>
</w:r>
</w:p></w:body>
Results - How it appears in the final document. EDIT: Removed final doc XML to post Code below:
EDIT:
Here is my Helper code that can be ran if you pass in the parameters you see. I have to remove the XML output that I am getting in order to post this.
public static class NumberingHelper
{
// Maps (docId, srcAbsId, signature) → mapped abstractNumId
private static readonly Dictionary<((string docId, int srcAbsId), string absSignature), int> GlobalAbstractNumIdMap
= new Dictionary<((string, int), string), int>();
// Maps (docId, srcNumId, signature) → mapped numId
private static readonly Dictionary<(string docId, int srcNumId, string absSignature), int> GlobalNumIdMap
= new Dictionary<(string, int, string), int>();
// Maps srcNumId → absSignature (per document, temporary)
private static readonly Dictionary<int, string> SrcNumToAbsSignature
= new Dictionary<int, string>();
private static int maxAbsNumId = 0;
private static int maxNumId = 0;
public static void ResetGlobalMaps()
{
GlobalAbstractNumIdMap.Clear();
GlobalNumIdMap.Clear();
SrcNumToAbsSignature.Clear();
}
public static NumberingResult CopyNumberingAndParagraphs(
WordprocessingDocument sourceDoc,
WmlDocument targetWml)
{
XNamespace w = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
//XNamespace w = "";
byte[] targetBytes = targetWml.DocumentByteArray;
// 1) Load source numbering
XDocument sourceNumbering = null;
if (sourceDoc?.MainDocumentPart?.NumberingDefinitionsPart != null)
{
using (var reader = new StreamReader(sourceDoc.MainDocumentPart.NumberingDefinitionsPart.GetStream()))
{
sourceNumbering = XDocument.Load(reader);
}
}
if (sourceNumbering == null)
return new NumberingResult { UpdatedDoc = targetWml, UpdatedParagraphs = new List<XElement>() };
// 2) Load target numbering (or create new)
XDocument targetNumbering;
using (var msLoad = new MemoryStream(targetBytes))
using (var wDocLoad = WordprocessingDocument.Open(msLoad, false))
{
var mainPart = wDocLoad.MainDocumentPart;
var numberingPart = mainPart.NumberingDefinitionsPart;
if (numberingPart != null)
{
using (var reader = new StreamReader(numberingPart.GetStream()))
targetNumbering = XDocument.Load(reader);
}
else
{
targetNumbering = new XDocument(
new XElement(w + "numbering",
new XAttribute(XNamespace.Xmlns + "w", w.NamespaceName)));
}
}
// 3) Determine max IDs in target numbering
int maxAbsNumId = targetNumbering.Descendants(w + "abstractNum")
.Select(a => (int?)a.Attribute(w + "abstractNumId"))
.DefaultIfEmpty(0)
.Max() ?? 0;
int maxNumId = targetNumbering.Descendants(w + "num")
.Select(n => (int?)n.Attribute(w + "numId"))
.DefaultIfEmpty(0)
.Max() ?? 0;
// Unique prefix for this source doc instance
string docGuid = (sourceDoc?.PackageProperties?.Identifier) ?? Guid.NewGuid().ToString("N");
var srcNumToAbsSignature = new Dictionary<int, string>();
// 4) Copy <num> elements from source
foreach (var srcNum in sourceNumbering.Descendants(w + "num"))
{
var numAttr = srcNum.Attribute(w + "numId");
if (numAttr == null || !int.TryParse(numAttr.Value, out int srcNumId))
continue;
var absElem = srcNum.Element(w + "abstractNumId");
if (absElem == null)
continue;
var absAttr = absElem.Attribute(w + "val");
if (absAttr == null || !int.TryParse(absAttr.Value, out int srcAbsId))
continue;
var srcAbs = sourceNumbering.Descendants(w + "abstractNum")
.FirstOrDefault(a =>
{
var aAttr = a.Attribute(w + "abstractNumId");
return aAttr != null &&
int.TryParse(aAttr.Value, out int v) &&
v == srcAbsId;
});
if (srcAbs == null)
continue;
// Build unique signature
string absSignature = string.Join("|",
new[] {
(string)srcAbs.Element(w + "multiLevelType")?.Attribute(w + "val") ?? "",
(string)srcAbs.Element(w + "nsid")?.Attribute(w + "val") ?? ""
}
.Concat(
srcAbs.Elements(w + "lvl").Select(l =>
$"{(int?)l.Attribute(w + "ilvl") ?? -1}-" +
$"{(string)l.Element(w + "numFmt")?.Attribute(w + "val") ?? ""}-" +
$"{(string)l.Element(w + "lvlText")?.Attribute(w + "val") ?? ""}-" +
$"{(string)l.Element(w + "lvlJc")?.Attribute(w + "val") ?? ""}-" +
$"{(string)l.Element(w + "isLgl")?.Attribute(w + "val") ?? ""}"
)
)
);
var absKey = ((docGuid, srcAbsId), absSignature + "|" + docGuid);
if (!GlobalAbstractNumIdMap.TryGetValue(absKey, out int mappedAbsId))
{
var clonedAbs = new XElement(srcAbs);
mappedAbsId = ++maxAbsNumId;
clonedAbs.SetAttributeValue(w + "abstractNumId", mappedAbsId);
// generate fresh nsid
var newNsid = Guid.NewGuid().ToString("N").Substring(0, 8).ToUpperInvariant();
var nsidElem = clonedAbs.Element(w + "nsid");
if (nsidElem != null) nsidElem.SetAttributeValue(w + "val", newNsid);
else clonedAbs.Add(new XElement(w + "nsid", new XAttribute(w + "val", newNsid)));
// give it its own name
var nameElem = clonedAbs.Element(w + "name");
if (nameElem != null) nameElem.SetAttributeValue(w + "val", $"List_{newNsid}");
else clonedAbs.Add(new XElement(w + "name", new XAttribute(w + "val", $"List_{newNsid}")));
// ensure multiLevelType is preserved
var multiElem = srcAbs.Element(w + "multiLevelType");
if (multiElem != null)
clonedAbs.Add(new XElement(w + "multiLevelType",
new XAttribute(w + "val", multiElem.Attribute(w + "val")?.Value)));
targetNumbering.Root.Add(clonedAbs);
GlobalAbstractNumIdMap[absKey] = mappedAbsId;
}
int newNumForThis = ++maxNumId;
GlobalNumIdMap[(docGuid, srcNumId, absSignature)] = newNumForThis;
srcNumToAbsSignature[srcNumId] = absSignature;
var clonedNum = new XElement(srcNum);
clonedNum.SetAttributeValue(w + "numId", newNumForThis);
var absRefElem = clonedNum.Element(w + "abstractNumId");
if (absRefElem != null)
absRefElem.SetAttributeValue(w + "val", mappedAbsId);
else
clonedNum.Add(new XElement(w + "abstractNumId",
new XAttribute(w + "val", mappedAbsId)));
// Add lvlOverride resets
var mappedAbsElem = targetNumbering
.Descendants(w + "abstractNum")
.First(a => (int)a.Attribute(w + "abstractNumId") == mappedAbsId);
var ilvls = mappedAbsElem
.Elements(w + "lvl")
.Select(l => (int)l.Attribute(w + "ilvl"))
.ToList();
foreach (var i in ilvls)
{
var lo = clonedNum.Elements(w + "lvlOverride")
.FirstOrDefault(x => (int?)x.Attribute(w + "ilvl") == i);
if (lo == null)
{
lo = new XElement(w + "lvlOverride", new XAttribute(w + "ilvl", i));
clonedNum.Add(lo);
}
if (lo.Element(w + "startOverride") == null)
{
lo.Add(new XElement(w + "startOverride", new XAttribute(w + "val", 1)));
}
}
targetNumbering.Root.Add(clonedNum);
}
// 5) Update paragraphs
var updatedParagraphs = new List<XElement>();
foreach (var para in sourceDoc.MainDocumentPart.Document.Body.Elements<Paragraph>())
{
var paraClone = XElement.Parse(para.OuterXml);
var pPr = paraClone.Element(w + "pPr");
if (pPr == null)
{
pPr = new XElement(w + "pPr");
paraClone.AddFirst(pPr);
}
var numPr = pPr.Element(w + "numPr");
if (numPr == null)
{
numPr = new XElement(w + "numPr");
pPr.Add(numPr);
}
var numIdElem = numPr.Element(w + "numId");
if (numIdElem != null)
{
var valAttr = numIdElem.Attribute(w + "val");
if (valAttr != null && int.TryParse(valAttr.Value, out int originalNumId))
{
if (srcNumToAbsSignature.TryGetValue(originalNumId, out string absSignature))
{
if (GlobalNumIdMap.TryGetValue((docGuid, originalNumId, absSignature), out int mappedNumId))
{
// copy ilvl from source paragraph
var srcIlvlElem = paraClone.Descendants(w + "ilvl").FirstOrDefault();
if (srcIlvlElem != null)
{
var ilvlVal = srcIlvlElem.Attribute(w + "val")?.Value ?? "0";
var numPrIlvl = numPr.Element(w + "ilvl");
if (numPrIlvl != null)
numPrIlvl.SetAttributeValue(w + "val", ilvlVal);
else
numPr.Add(new XElement(w + "ilvl", new XAttribute(w + "val", ilvlVal)));
}
// set mapped numId
numIdElem.SetAttributeValue(w + "val", mappedNumId);
}
}
}
}
updatedParagraphs.Add(paraClone);
}
// 6) Save updated numbering.xml
using (var ms = new MemoryStream())
{
ms.Write(targetBytes, 0, targetBytes.Length);
ms.Position = 0;
using (var wDoc = WordprocessingDocument.Open(ms, true))
{
var mainPart = wDoc.MainDocumentPart;
var numberingPart = mainPart.NumberingDefinitionsPart ??
mainPart.AddNewPart<NumberingDefinitionsPart>();
using (var partStream = numberingPart.GetStream(FileMode.Create, FileAccess.Write))
{
targetNumbering.Save(partStream);
}
mainPart.Document.Save();
}
ms.Position = 0;
targetWml = new WmlDocument(targetWml.FileName, ms.ToArray());
}
return new NumberingResult
{
UpdatedDoc = targetWml,
UpdatedParagraphs = updatedParagraphs
};
}
}



numId.valis an index into the numbering part. You would have to merge the numbering parts of both documents and renumber each use. Perhaps in paragraph properties, perhaps in document styles.