1. Introduction
Imagine a giant XML that some SOAP service or banking API sent you. It has hundreds of nested elements, inconsistent structures, and you need to find just a single payment amount from last month. The serialization approach doesn't fit here — it's too clunky and you don't know in advance where to look. You need a "magnifying glass" for the XML tree and the skill to quickly walk its branches.
For these cases .NET has had a powerful tool for many years — the XmlDocument class, complemented by the query language XPath. Today you'll learn to:
- Load XML into a DOM tree;
- Navigate and find elements manually and with XPath;
- Modify the contents of an XML document;
- Add, remove and change nodes;
- Use XPath for complex selections.
2. What is XmlDocument?
XmlDocument is a class from the System.Xml namespace. It's an implementation of the DOM (Document Object Model) — a representation of the whole XML file as an in-memory tree of objects.
A bit of theory and analogies
If XmlSerializer is like a LEGO constructor — turning an object into a set of bricks and back — then XmlDocument is like working with a real tree: it has a root, branches (elements), leaves (text nodes), and you can walk the tree, add branches, trim leaves and transplant them wherever you want.
Simple XML document loading
Say we have this XML:
<users>
<user id="1">
<name>Olga</name>
<age>28</age>
</user>
<user id="2">
<name>Igor</name>
<age>35</age>
</user>
</users>
Load it into memory:
using System.Xml;
string xml = @"
<users>
<user id='1'>
<name>Olga</name>
<age>28</age>
</user>
<user id='2'>
<name>Igor</name>
<age>35</age>
</user>
</users>";
XmlDocument doc = new XmlDocument();
doc.LoadXml(xml); // or doc.Load("path_to_file.xml");
After calling LoadXml (or Load if the XML is from a file) you have full access to the document contents.
3. Navigating the DOM tree
DOM tree structure
Every XML document after loading becomes a tree made of nodes of different types:
| Node type | Class in .NET | Example |
|---|---|---|
| Document | XmlDocument | |
| Element | XmlElement | |
| Attribute | XmlAttribute | |
| Text | XmlText | Olga, 28 |
| Comment | XmlComment | |
To access the elements you need you'll have to "walk the tree" using properties like ChildNodes, Attributes, ParentNode, etc.
Get the root element
XmlElement root = doc.DocumentElement;
Console.WriteLine(root.Name); // users
Iterate child elements
foreach (XmlNode node in root.ChildNodes)
{
if (node is XmlElement user)
{
// user is <user id="...">...</user>
string id = user.GetAttribute("id");
string name = user["name"].InnerText;
string age = user["age"].InnerText;
Console.WriteLine($"User {id}: {name}, age {age}");
}
}
IMPORTANT: Access via user["name"] is only possible if there's a direct child element named <name> among the immediate descendants.
Access attributes and text
var firstUser = root.FirstChild as XmlElement;
string id = firstUser.GetAttribute("id"); // "1"
string name = firstUser["name"].InnerText; // "Olga"
4. Modifying the XML document
Change a value
Say Olga suddenly decided to "rejuvenate":
var olga = root.FirstChild as XmlElement;
olga["age"].InnerText = "22"; // now <age>22</age>
Add a new user
XmlElement newUser = doc.CreateElement("user");
newUser.SetAttribute("id", "3");
XmlElement name = doc.CreateElement("name");
name.InnerText = "Vasilisa";
XmlElement age = doc.CreateElement("age");
age.InnerText = "19";
newUser.AppendChild(name);
newUser.AppendChild(age);
root.AppendChild(newUser);
Remove an element
Remove the second user ("Igor"):
XmlNode userToDelete = root.SelectSingleNode("user[@id='2']");
if (userToDelete != null)
root.RemoveChild(userToDelete);
Save changes
doc.Save("users_updated.xml");
// Or doc.OuterXml — to get the XML string
5. XPath — the language for searching and selecting in XML
Working with the DOM tree gets tedious if you need to search elements "by condition" — e.g. all users older than 25. That's what XPath is for — a navigation language for XML trees.
Basic XPath usage
XPath queries can be executed via methods SelectSingleNode (returns the first matching node) and SelectNodes (returns a node collection).
Example: find user with id = 1
XmlNode user = root.SelectSingleNode("user[@id='1']");
Console.WriteLine(user["name"].InnerText); // Olga
Example: find all users older than 25
XmlNodeList nodes = root.SelectNodes("user[age>25]");
foreach (XmlNode u in nodes)
{
Console.WriteLine(u["name"].InnerText); // Will print Olga and Igor (if Igor hasn't been deleted)
}
XPath — short syntax
| XPath expression | What it does |
|---|---|
|
All <user> elements under root <users> |
|
User with id=3 |
|
Users older than 25 |
|
All <name> elements of all users |
|
The last <user> |
|
The first two <user> elements |
More examples and syntax: XPath documentation.
Another example: selection by nested elements
Suppose the XML is more complex:
<library>
<book>
<title>Lord of the Flies</title>
<author>
<firstname>William</firstname>
<lastname>Golding</lastname>
</author>
</book>
<book>
<title>Swann's Way</title>
<author>
<firstname>Marcel</firstname>
<lastname>Proust</lastname>
</author>
</book>
</library>
XmlDocument doc = new XmlDocument();
doc.LoadXml(libraryXml);
XmlNodeList authors = doc.SelectNodes("/library/book/author/lastname");
foreach (XmlNode lastName in authors)
Console.WriteLine(lastName.InnerText);
What will be printed:
Golding
Proust
6. XPath: filtering, logic, calculations
Logical filters
Print the names of all users whose age is between 18 and 30:
// [age>=18 and age<=30]
XmlNodeList youngUsers = root.SelectNodes("user[age>=18 and age<=30]");
foreach (XmlNode u in youngUsers)
Console.WriteLine(u["name"].InnerText);
Working with attributes
Attributes are selected with @. Find users whose id starts with "1":
XmlNodeList nodes = root.SelectNodes("user[starts-with(@id, '1')]");
Counting nodes
You can't directly use the count() function in the C# SelectNodes method — it returns a collection, not a number. But you can do:
int count = root.SelectNodes("user").Count;
Nested filters
XmlNodeList nodes = doc.SelectNodes("/library/book[author/lastname='Golding']");
7. XPath and namespaces
If your XML uses namespaces (xmlns), things get a lot more interesting! For those cases use XmlNamespaceManager:
<catalog xmlns="http://books.example.com">
<book>
<title>Algorithms</title>
</book>
</catalog>
doc.LoadXml(xml);
XmlNamespaceManager nsmgr = new XmlNamespaceManager(doc.NameTable);
nsmgr.AddNamespace("b", "http://books.example.com");
XmlNodeList books = doc.SelectNodes("/b:catalog/b:book", nsmgr);
foreach (XmlNode book in books)
Console.WriteLine(book.SelectSingleNode("b:title", nsmgr)?.InnerText);
8. Modifying XML with the DOM
Adding nodes "on the fly"
Add a new <email> element to the first user:
XmlElement email = doc.CreateElement("email");
email.InnerText = "olga@gmail.com";
olga.AppendChild(email);
Changing attributes
olga.SetAttribute("id", "99"); // Now id="99"
Removing nodes using XPath
XmlNodeList nodesToRemove = root.SelectNodes("user[age<20]");
foreach (XmlNode node in nodesToRemove)
root.RemoveChild(node);
9. Useful nuances
Searching and bulk structure changes
Suppose you receive a big XML with various <record type="..."> elements. You need to keep only those whose type attribute equals "customer", and add a child element <status>active</status> to each.
XmlNodeList customerNodes = root.SelectNodes("record[@type='customer']");
foreach (XmlElement record in customerNodes)
{
XmlElement status = doc.CreateElement("status");
status.InnerText = "active";
record.AppendChild(status);
}
XML node tree (DOM)
users
├─ user (id="1")
│ ├─ name ("Olga")
│ └─ age ("28")
├─ user (id="2")
│ ├─ name ("Igor")
│ └─ age ("35")
XPath selection examples
| XPath query | What it returns |
|---|---|
|
The first user (id="1") |
|
The last user (id="2") |
|
All older than 30 (Igor) |
|
User with id="2" |
Practical use cases
- Integrations with "legacy" APIs. In many government and banking systems SOAP/XML still rule. The ability to quickly find and change something in a large XML response can save dozens of hours.
- Data migration. When moving from one system to another you often need to parse XML and perform complex selections, transformations and bulk updates.
- Import-export to Excel. In many B2B products input still comes as XML. There XPath is a fast way to get what you need without building a large data model.
10. Errors, nuances and common traps
NullReferenceException: If you try to access a non-existing element, e.g. el["something"].InnerText, and that child element doesn't exist at all. Always check for null.
XPath and context: Remember that an expression without a leading / searches among the descendants of the current node, while with / it starts from the document root. Different reference points can lead to no results even though the data exists.
Namespaces handling: If you don't use XmlNamespaceManager, XPath queries against XML with xmlns will return empty results.
Modifying while iterating: If you want to remove nodes based on the results of SelectNodes, first copy them into an array, then iterate — otherwise the collection will change during iteration and errors may occur.
Difference between text and element nodes: There can be text nodes between elements — spaces and line breaks that XML treats as content. For strict selection use XmlElement only or filter by NodeType.
GO TO FULL VERSION