XPath in Java and .NET

The JDK and the BCL both ship a complete XPath 1.0 engine: compile once, reuse the object, and configure namespaces before you evaluate anything.

Java: JAXP

import javax.xml.namespace.NamespaceContext;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.*;
import java.util.Iterator;
import java.util.Map;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;

public class Query {
  static final Map<String, String> NS = Map.of("c", "urn:example:catalog");

  public static void main(String[] args) throws Exception {
    Document doc = DocumentBuilderFactory.newInstance()
        .newDocumentBuilder().parse(new java.io.File("catalog.xml"));

    XPath xp = XPathFactory.newInstance().newXPath();
    xp.setNamespaceContext(new NamespaceContext() {
      public String getNamespaceURI(String p) { return NS.getOrDefault(p, ""); }
      public String getPrefix(String uri) { return null; }
      public Iterator<String> getPrefixes(String uri) { return null; }
    });

    // Compile once when the expression is used in a loop
    XPathExpression count = xp.compile("count(//c:book)");
    System.out.println(count.evaluate(doc, XPathConstants.NUMBER));

    XPathExpression ids = xp.compile("//c:book/@id");
    NodeList nodes = (NodeList) ids.evaluate(doc, XPathConstants.NODESET);
    for (int i = 0; i < nodes.getLength(); i++) {
      System.out.println(nodes.item(i).getNodeValue());
    }
  }
}
  • XPathConstants has exactly four values: NODESET, NODE, STRING, NUMBER and BOOLEAN.
  • XPathExpression is not thread-safe; keep one compiled object per thread or recompile inside the request.
  • Without a namespace context, any prefixed name in the expression throws XPathExpressionException.
  • Large NODESETs hold the whole DOM in memory — for very large documents prefer streaming with StAX instead.

.NET: XPathDocument and XPathNavigator

using System.Xml;
using System.Xml.XPath;

var doc = new XPathDocument("catalog.xml");
var nav = doc.CreateNavigator();
var ns = new XmlNamespaceManager(nav.NameTable);
ns.AddNamespace("c", "urn:example:catalog");

// Evaluate a scalar
double count = (double)nav.Evaluate("count(//c:book)", ns);

// Select and read through the navigator, which is far cheaper than DOM nodes
foreach (XPathNavigator book in nav.Select("//c:book", ns))
{
    Console.WriteLine(book.GetAttribute("id", ""));
    Console.WriteLine(book.SelectSingleNode("title", ns)?.Value);
}

// Compile an expression that runs many times
var compiled = XPathExpression.Compile("//c:book[@id=$id]");
compiled.SetContext(ns);
var vars = new XsltArgumentList();
vars.AddParam("id", "", "B-1001");
foreach (XPathNavigator n in nav.Select(compiled, vars))
    Console.WriteLine(n.OuterXml);
TaskJava.NET
LoadDocumentBuilderXPathDocument or XmlDocument
Namespace bindingsetNamespaceContextXmlNamespaceManager
CompileXPath.compileXPathExpression.Compile
Scalar resultXPathConstants.NUMBERcast the Evaluate result
VariablesSet on the expressionXsltArgumentList
Streaming sourceStAX + manual matchingXPathReader, or read forward
⚠️
The classic .NET mistake is mixing XmlDocument.SelectNodes (DOM, slower but writable) with XPathNavigator (read-only, fast) and expecting identical attribute handling: GetAttribute on the navigator takes the namespace URI as a second argument, and passing the wrong one silently returns null.

FAQ

Is a compiled XPathExpression actually faster?
Yes for expressions evaluated in a loop, because parsing happens once. For a single evaluation the compile step costs more than it saves.
Which is better, XPathDocument or XmlDocument?
XPathDocument is optimised for read-only queries and is the right default. Use XmlDocument only when you also need to modify the tree.

XPath in Python: lxml and ElementTree XPath 1.0, 2.0 and 3.1: what changed

Last refreshed 2026-09-18.