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?)element returns null for a missing element; (string)element throws. Use the nullable cast and decide deliberately.
  • (decimal?)element parses 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.
  • XDocument keeps the whole document in memory with an object per node, so it is not the tool for a multi-gigabyte file. Use XmlReader and 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);
GoalExpressionNote
Direct childrene.Elements(ns + "name")Namespaces are part of the name
Any depthe.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 namespacee.Name.LocalName == "order"Use sparingly; it hides a real contract problem
Append a childe.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.

Projection and shaping results Query performance and avoiding N+1

Last refreshed 2026-09-18.