Updated on

To read XML in C#, we load it into an XDocument, with Load() for a file or Parse() for a string, or we walk through it node by node with XmlReader. XDocument is the simpler of the two, and XmlReader gives us more control.

In the preceding article, we created custom XML documents. We have also seen how to serialize objects to XML and how to deserialize XML into objects. This time, we read the XML back.

To download the source code for this article, you can visit our GitHub repository.

How Do We Load an XML File From Disk?

To read an XML file, we pass its path to XDocument.Load() when we want the whole document in memory, or to XmlReader.Create() when we want to move through it one node at a time.

A relative path like "people.xml" resolves against the current working directory. That is the folder the program was started from, and it is often not the folder the compiled program sits in. Start the program from anywhere else and Load() throws a FileNotFoundException, even though the file is right there in the project.

Two small steps make the path reliable. We tell the build to copy the file into the output folder, next to the compiled program. Then we build the full path from AppContext.BaseDirectory, which points at that output folder however the program was started.

The file also has to be well-formed. Broken XML makes Load() throw an XmlException whose message names the line and position of the problem, so the problem shows up on the very first read.

Our sample file holds four people:

<?xml version="1.0" encoding="utf-8"?>
<people>
  <person>
    <name>
      <firstName>Olivia</firstName>
      <lastName>Davis</lastName>
    </name>
    <email>[email protected]</email>
    <age>34</age>
  </person>
  <person>
    <name>
      <firstName>John</firstName>
      <lastName>Davis</lastName>
    </name>
    <email>[email protected]</email>
    <age>41</age>
  </person>
  <person>
    <name>
      <firstName>Sarah</firstName>
      <lastName>Moore</lastName>
    </name>
    <email>[email protected]</email>
    <age>27</age>
  </person>
  <person>
    <name>
      <firstName>Sarah</firstName>
      <lastName>Smith</lastName>
    </name>
    <email>[email protected]</email>
    <age>62</age>
  </person>
</people>

We save it as people.xml in the project folder, next to ReadXML.csproj, and add one item to the project file so every build copies it to the output folder:

<ItemGroup>
  <None Update="people.xml" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

Then we read every email address from the file, once with each class:

using System.Xml;
using System.Xml.Linq;

public static class ReadXmlFromFile
{
    public static readonly string FilePath =
        Path.Combine(AppContext.BaseDirectory, "people.xml");

    public static IEnumerable<string> ReadEmailsUsingXDocument(string path)
    {
        var xmlDoc = XDocument.Load(path);

        return xmlDoc.Root!
            .Elements("person")
            .Select(person => person.Element("email")!.Value)
            .ToList();
    }

    public static IEnumerable<string> ReadEmailsUsingXmlReader(string path)
    {
        using var reader = XmlReader.Create(path);

        List<string> result = [];
        while (reader.Read())
        {
            if (reader.NodeType == XmlNodeType.Element && reader.Name == "email")
                result.Add(reader.ReadElementContentAsString());
        }

        return result;
    }
}

XDocument.Load() parses the whole file, so we go straight to the <person> elements under the root and take each <email> value. XmlReader.Create() opens the same file as a stream, and we pick up each <email> element as the reader passes it. Both methods return the same four addresses. The project item also copies the file into the test project’s output folder, which is why the sample’s tests can use the same FilePath. For other ways to locate a file our program ships with, see our article on reading a file without specifying the full path.

NOTE: The rest of the code uses the Person, People and CreateXmlUsingXmlWriter classes from the preceding article. The sample repository has its own copy of them, so we won’t duplicate the code here. In that copy, the CreateXmlUsingXmlWriter methods are static, while the preceding article declares them as instance methods. Its files also put the classes in the ReadXml and ReadXml.Read namespaces, so a file that uses them needs using ReadXml; and using ReadXml.Read; at the top.

Similar to creating XML documents, where we could choose between LinqToXml or XmlWriter, we once again have two potential approaches: utilizing the XDocument class or opting for the older XmlReader class.

The XDocument path is simpler and more user-friendly, while XmlReader provides greater control over the reading process.

How Do We Parse an XML String With XDocument?

Reading XML using XDocument couldn’t be simpler. Whether we are reading XML from a file or a string, the XDocument class provides functions for these scenarios.

To read an XML document from a string, we use the Parse() method. To read an XML from a file, we use the Load() method.

If the document is not syntactically correct, these methods will throw an exception:

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

public static class ReadXmlUsingXDocument
{
    public static XDocument ReadXmlAndCatchErrors(string xml)
    {
        try
        {
            return XDocument.Parse(xml);
        }
        catch (Exception ex)
        {
            Console.WriteLine("Something went wrong:");
            Console.WriteLine(ex);
        }

        return new XDocument();
    }
}

We’ve created a static ReadXmlUsingXDocument class utilizing the XDocument class. This class features a method, ReadXmlAndCatchErrors(), which parses the given XML string. If any errors occur during parsing, the method catches the error, displays it in the console, and returns an empty XDocument.

XDocument is part of LINQ to XML, so everything we read from it can be queried with ordinary LINQ.

To evaluate this method, we can generate XML documents using the CreateXmlUsingXmlWriter class. First, we create a valid XML document and then proceed to read it:

public static string TestValidXml()
{
    var xmlDoc = ReadXmlAndCatchErrors(CreateXmlUsingXmlWriter.CreateSimpleXml(People.GetOne()));

    return xmlDoc.ToString();
}

Everything unfolds as anticipated. The document is successfully created, and we can both read its content and output it to the console.

But what happens if we provide invalid XML:

public static string TestInvalidXml()
{
    var xmlDoc = ReadXmlAndCatchErrors(CreateXmlUsingXmlWriter.CreateWrongXml(People.GetOne()));

    return xmlDoc.ToString();
}

XDocument.Parse() will throw an exception and our ReadXmlAndCatchErrors() method catches it and returns an empty XDocument.

The exception is an XmlException, and its message tells us the line and position where the parser gave up.

Getting Values From XDocument

With the XML document now within the XDocument object, the question arises: How do we extract the name or age of a person from the XDocument object?

As this extends beyond the scope of this article, we will briefly outline a few options using the initially created ‘people document’:

<person>
  <name>
    <firstName>Emma</firstName>
    <lastName>Brown</lastName>
  </name>
  <email>[email protected]</email>
  <age>58</age>
</person>

XML document contains data about a person. Among them are the email and the age of the person we want to extract.

Reading With the Use of the Element Collection

Every element inside XDocument has an element collection of all its children. If an element has no children, this collection will be empty.

We traverse through the XML tree by referencing the elements by name and then read their values:

public static string TestReadWithElementCollection()
{
    var xmlDoc = ReadXmlAndCatchErrors(CreateXmlUsingXmlWriter.CreateSimpleXml(People.GetOne()));
    var name = xmlDoc.Root!.Element("name")!.Element("firstName")!.Value;
    var age = xmlDoc.Root!.Element("age")!.Value;

    return $"Name: {name}, Age: {age}";
}

We start from the root of the document, which is a <person> element. There we search for the <name> element, and inside the <name> element, we find the <firstName> element. From the <firstName> element, we take the value, which is, of course, the name of the person.

The same is true for the age.

Notice that here we are using the ‘!’ operator, as we know in our example case, that the XML document contains all the elements we are searching for. Our article on the null-forgiving operator explains what it does and does not check.

Using XPath

To retrieve data from an XML document, we can also utilize XPath. The XPath language is quite extensive, but in this article, we will provide a simple example of reading the name and age fields:

public static string TestReadUsingXPath()
{
    var xmlDoc = ReadXmlAndCatchErrors(CreateXmlUsingXmlWriter.CreateSimpleXml(People.GetOne()));

    var name = xmlDoc.XPathSelectElement("/person/name/firstName")!.Value;
    var age = xmlDoc.XPathSelectElement("/person/age")!.Value;

    return $"Name: {name}, Age: {age}";
}

In this example, we use XPath to locate elements like navigating a file system. For instance, to access the <firstName> element, we use the path “/person/name/firstName.”

There are various ways to express XPath queries, such as:

  • //firstName

  • /person/name/*[1]

  • /person/name/*[local-name()='firstName']

The first XPath expression selects all <firstName> elements from anywhere in the XML document. The second selects the first child element under the <name> element within the <person> element. The last one selects every child element under the <name> element within the <person> element whose local name is firstName, at any position and in any namespace.

XPath is an extensive language, and the examples given here offer only a brief overview. For a more in-depth exploration of this topic, check out our in-depth article: selecting XML nodes with XPath.

How Does XmlReader Read XML One Node at a Time?

The second option mentioned for reading XML is the XmlReader class. We put our XmlReader methods in a static ReadXmlUsingXmlReader class and add using System.Xml; at the top of its file.

Microsoft Learn describes it as a reader that “provides fast, noncached, forward-only access to XML data.” The reader only knows the node it is on. It moves forward and never back, and it does not keep the nodes it has already passed.

The XmlReader class parses the XML one node at a time. Those nodes include elements such as <name>, but also Whitespace, Text, CDATA, and others.

All these possibilities are described in an XmlNodeType enum. We use a reader to read the nodes one after another, and each time it gives us the next node with its type, value, and so on:

public static IEnumerable<string> ReadXml(string xml)
{
    using var reader = XmlReader.Create(new StringReader(xml));

    List<string> result = [];
    while (reader.Read())
    {
        result.Add($"> {reader.NodeType} | {reader.Name} | {reader.Value}");
    }

    return result;
}

Running this method will produce a long list of different types, but Whitespace type may ruin the format as every new line is also a Whitespace.

Let’s remove the whitespaces and see what we get:

public static IEnumerable<string> ReadXmlWithoutWhiteSpace(string xml)
{
    using var reader = XmlReader.Create(new StringReader(xml));

    List<string> result = [];
    while (reader.Read())
    {
        if (reader.NodeType == XmlNodeType.Whitespace)
            continue;

        result.Add($"> {reader.NodeType} | {reader.Name} | {reader.Value}");
    }

    return result;
}

This method is very similar, except it skips all whitespaces. For a one-person document made with CreateSimpleXml(), the output looks like this:

> XmlDeclaration | xml | version="1.0" encoding="utf-16"
> Element | person |
> Element | name |
> Element | firstName |
> Text |  | William
> EndElement | firstName |
> Element | lastName |
> Text |  | Taylor
> EndElement | lastName |
> EndElement | name |
> Element | email |
> Text |  | [email protected]
> EndElement | email |
> Element | age |
> Text |  | 20
> EndElement | age |
> EndElement | person |

We can learn a lot by examining the output of the method.

If nothing more, at least even the basic elements like <lastName>Taylor</lastName> is split into three parts: Element, Text, EndElement.

Using XmlReader in Practice

Now that we’ve understood the intricacies of reading custom XML documents let’s review a practical example.

We’ll create a method that traverses an XML file filled with names and surnames. The objective is to extract both the first and last names efficiently.

Reading Names and Surnames

We’ll use the people.xml file from the first section, which contains personal data with <firstName> and <lastName> elements. We aim to extract this information and display it in a tabular format.

Firstly, we’ll define a private class, PersonData, with FirstName and LastName properties to store individual names:

private class PersonData
{
    public string? FirstName { get; set; }
    public string? LastName { get; set; }
    public void Init() => FirstName = LastName = "";
}

Now, the implementation is straightforward:

public static IEnumerable<string> ReadNamesAndSurnames(string xml)
{
    var settings = new XmlReaderSettings
    {
        IgnoreWhitespace = true
    };

    using var reader = XmlReader.Create(new StringReader(xml), settings);

    List<string> result = [];
    var personData = new PersonData();
    var numberOfPersons = 0;

    while (reader.Read())
    {
        if (reader.NodeType == XmlNodeType.Element)
        {
            if (reader.Name == "person")
                personData.Init();

            if (reader.Name == "firstName")
                personData.FirstName = reader.ReadElementContentAsString();

            if (reader.Name == "lastName")
                personData.LastName = reader.ReadElementContentAsString();
        }

        if (reader.NodeType == XmlNodeType.EndElement)
        {
            if (reader.Name == "person")
                result.Add($"#{++numberOfPersons,3} | " +
                    $"{personData.FirstName,-15} | {personData.LastName,-15}");
        }
    }

    return result;
}

Initially, we set up the XmlReader to disregard Whitespaces, simplifying the process. Here, we employ yet another option to skip whitespace by utilizing the XmlReaderSettings class.

As we iterate through elements one by one, when we come across the <person> element, we initialize the PersonData object. For <firstName> or <lastName> elements, we update the respective properties.

Upon encountering the </person> end element, we know that we have all the data for a person, so we add a row for them. To run it against our file, we read the file into a string and print the rows:

var xml = File.ReadAllText(ReadXmlFromFile.FilePath);

foreach (var row in ReadXmlUsingXmlReader.ReadNamesAndSurnames(xml))
    Console.WriteLine(row);

This leads to a well-organized table of people from our XML file:

#  1 | Olivia          | Davis
#  2 | John            | Davis
#  3 | Sarah           | Moore
#  4 | Sarah           | Smith

The outcome is a well-organized table displaying the sequence number of a person along with their first and last names.

ReadElementContentAsString() reads the text of an element and then moves the reader past its end tag, onto the next node. After <firstName>, the reader already stands on <lastName>, and the lastName check right below picks it up in the same pass.

That only works because the checks follow the order of the elements in the file. If a file put <lastName> before <firstName>, the next Read() call would step over <firstName>, and that person’s first name would stay empty with no error. With XML we don’t control, we skip the Read() call after each ReadElementContentAsString(), or we read each <person> into an XElement and query that.

When Should We Still Use XmlDocument?

XmlDocument is the older of the two in-memory classes. It goes back to the early .NET Framework versions and implements the W3C Document Object Model, so it works like the DOM we may know from JavaScript. Load() or LoadXml() fills the document, and we move through XmlNode objects with DocumentElement, ChildNodes and GetElementsByTagName().

For new code, XDocument does the same job with less code. We load in one call instead of creating the document and then loading it, LINQ works on it directly, and Element("age")!.Value is shorter than the XmlNode route to the same text.

We still meet XmlDocument in existing code, and some older code bases use nothing else. Some APIs also expect it as input. SignedXml, the class that signs and verifies XML documents, takes an XmlDocument in its constructor. And SelectSingleNode() and SelectNodes() take XPath expressions directly on an XmlNode.

Reading the emails from people.xml with XmlDocument looks like this:

using System.Xml;

public static class ReadXmlUsingXmlDocument
{
    public static IEnumerable<string> ReadEmails(string path)
    {
        var xmlDoc = new XmlDocument();
        xmlDoc.Load(path);

        return xmlDoc.GetElementsByTagName("email")
            .Cast<XmlNode>()
            .Select(node => node.InnerText)
            .ToList();
    }
}

GetElementsByTagName() searches the whole document, much like the //email XPath expression would, and InnerText gives us the text inside each element. It returns the same four addresses as the two methods in the first section. To select nodes by path with SelectSingleNode(), the XPath article linked above goes into detail. And when we want the XML turned into our own C# classes rather than read value by value, XML deserialization with XmlSerializer does that mapping for us.

Conclusion

Despite JSON becoming the de facto standard for API communication, XML remains integral to many industries due to its longstanding presence. As we inevitably encounter XML standards, we must familiarize ourselves with XML handling in .NET, utilizing classes such as XDocument, XmlDocument, and XmlReader.

XDocument is a versatile tool for most tasks, simplifying the reading of diverse XML documents. For complex or poorly structured XML documents, the advanced capabilities of XmlReader are essential.

Regardless of your content or its complexity, mastering these tools is key to effective XML handling in .NET.

Tested with .NET 10.0.10 and MSTest 4.5.2.