LINQ to XML and XDocument queries
XDocument is a queryable in-memory tree with namespaces as first-class objects, which is where most LINQ to XML code goes wrong.
Loading and querying
using System.Xml.Linq;
// Load from a file, a stream or a string
XDocument doc = XDocument.Load("orders.xml", LoadOptions.SetLineInfo);
XDocument fromString = XDocument.Parse(xml);
// Elements: direct children only
var top = doc.Root!.Elements("order").ToList();
// Descendants: any depth
var all = doc.Descendants("order").ToList();
// Attributes are strings, so parse them explicitly
var ids = doc.Descendants("order")
.Select(o => (string?)o.Attribute("id"))
.Where(id => id is not null)
.ToList();
// Safe numeric conversion with a fallback, rather than a throwing parse
var totals = doc.Descendants("total")
.Select(t => (decimal?)t)
.Where(v => v.HasValue)
.Select(v => v!.Value)
.ToList();
// Shape XML into objects in one query
var orders = doc.Descendants("order").Select(o => new
{
Id = (string?)o.Attribute("id") ?? throw new InvalidDataException("order without id"),
Currency = (string?)o.Attribute("currency") ?? "GBP",
Total = (decimal?)o.Element("total") ?? 0m,
Lines = o.Element("lines")?.Elements("line")
.Select(l => (string?)l).ToList() ?? new List<string?>(),
}).ToList();
// Build XML from objects, which is what LINQ to XML is best at
var built = new XElement("orders",
orders.Select(o => new XElement("order",
new XAttribute("id", o.Id),
new XAttribute("currency", o.Currency),
new XElement("total", o.Total))));
built.Save("generated.xml");(string?)elementreturns null for a missing element;(string)elementthrows. Use the nullable cast and decide deliberately.(decimal?)elementparses the text and returns null if it is missing or unparseable, which is usually the behaviour you want from a feed.Elements()is direct children,Descendants()is everything below. Confusing them is the most common source of a query returning too much.XDocumentkeeps the whole document in memory with an object per node, so it is not the tool for a multi-gigabyte file. UseXmlReaderand build objects as you stream.
Namespaces and safe updates
// A document with a default namespace needs the namespace in every name
XNamespace ns = "urn:example:orders";
XNamespace dc = "http://purl.org/dc/elements/1.1/";
var doc = XDocument.Parse("<orders xmlns='urn:example:orders'/>");
// This finds nothing, because the elements are in the namespace
var wrong = doc.Descendants("order").Count(); // 0
// This is correct: the name includes the namespace
var right = doc.Descendants(ns + "order").Count();
// A local-name match when you genuinely do not care about the namespace
var loose = doc.Descendants()
.Where(e => e.Name.LocalName == "order")
.Count();
// Read a namespaced attribute
var title = doc.Descendants(ns + "order")
.Select(o => (string?)o.Element(dc + "title"))
.ToList();
// Update and save: find, change, then write. XDocument has no query language
// for updates, so every change is explicit.
foreach (var order in doc.Descendants(ns + "order"))
{
if ((decimal?)order.Element(ns + "total") > 1000m)
order.SetAttributeValue("priority", "high");
}
// Add or replace safely
var target = doc.Descendants(ns + "order").First();
target.Element(ns + "note")?.Remove();
target.Add(new XElement(ns + "note", "reviewed"));
// Saving with indentation, and an explicit declaration
doc.Save("updated.xml", SaveOptions.None);
File.WriteAllText("updated2.xml", doc.Declaration + Environment.NewLine + doc);| Goal | Expression | Note |
|---|---|---|
| Direct children | e.Elements(ns + "name") | Namespaces are part of the name |
| Any depth | e.Descendants(ns + "name") | Includes the element itself only if it matches |
| Read text as a value | (decimal?)e.Element(ns + "total") | Null when missing or unparseable |
| Require the element | (string)e.Element(ns + "id") | Throws a NullReferenceException when missing |
| Match ignoring the namespace | e.Name.LocalName == "order" | Use sparingly; it hides a real contract problem |
| Append a child | e.Add(new XElement(...)) | Creates a copy, does not move an existing node |
⚠️
Loading XML from an untrusted source with
XDocument.Load uses an XmlReader with default settings, which resolves entities. Turn off DTD processing and entity resolution explicitly when the document came from a user, an upload or a feed.FAQ
Why does Descendants("order") return nothing?
Because the document declares a default namespace, so every unprefixed element is in that namespace. Create an XNamespace from the URI and use ns + "order".
Is LINQ to XML faster than XmlDocument?
It is a different tree with a cleaner API and better query support, roughly comparable in memory. Neither is suitable for very large documents; use a streaming reader there.
Related
Projection and shaping results Query performance and avoiding N+1
Last refreshed 2026-09-18.