ZetCode

C# XmlReader

last modified October 18, 2026

In this article we show how to read XML data in C# with XmlReader.

XmlReader

XmlReader represents a reader that provides fast, non-cached, forward-only access to XML data. It reads the document node by node, so it needs little memory regardless of the size of the document. The price is that we cannot go back or jump around; for random access and modification, use XmlDocument.

XmlReader is an abstract class in the System.Xml namespace. We create instances with the static Create method, which accepts a file name or URI, a Stream, or a TextReader, and optionally an XmlReaderSettings object.

C# XmlReader example

The following example creates a simple XmlReader.

data.xml
<?xml version="1.0" encoding="utf-8"?>
<value>6</value>

We have a very simple XML file.

Program.cs
using System.Xml;

using var reader = XmlReader.Create("data.xml");

reader.MoveToContent();
var data = reader.ReadElementContentAsString();

Console.WriteLine(data);

// the typed variant converts the text for us
using var reader2 = XmlReader.Create("data.xml");
reader2.MoveToContent();
int value = reader2.ReadElementContentAsInt();
Console.WriteLine(value * 7);

In the example, we read the value from the XML document with XmlReader.

using var reader = XmlReader.Create("data.xml");

The XmlReader is created with the Create method. The using declaration closes the underlying file when the reader goes out of scope.

reader.MoveToContent();

The MoveToContent method skips the non-content nodes, such as the XML declaration, comments, and white space, and moves to the first content node.

var data = reader.ReadElementContentAsString();

The ReadElementContentAsString method reads the current element and returns its text content as a string.

int value = reader2.ReadElementContentAsInt();

There are typed variants for the common types: ReadElementContentAsInt, AsLong, AsDecimal, AsDouble, AsBoolean, and AsDateTime. They convert the text with XML-schema rules, which are culture-independent, so a decimal point is always a period.

$ dotnet run
6
42

C# XmlReader ReadToFollowing

The ReadToFollowing method advances the reader to the next following element that matches the specified name and returns true if a matching element is found.

books.xml
<bookstore>
    <book genre='Science Fiction'>
        <title>Dune</title>
        <author>Frank Herbert</author>
        <price>8.99</price>
    </book>
    <book genre='Novel'>
        <title>Old Goriot</title>
        <author>Honoré de Balzac</author>
        <price>9.0</price>
    </book>
</bookstore>

This XML file contains books.

Program.cs
using System.Xml;

using var reader = XmlReader.Create("books.xml");

while (reader.ReadToFollowing("book"))
{
    Console.WriteLine($"genre: {reader.GetAttribute("genre")}");

    reader.ReadToFollowing("title");
    Console.WriteLine($"title: {reader.ReadElementContentAsString()}");

    reader.ReadToFollowing("author");
    Console.WriteLine($"author: {reader.ReadElementContentAsString()}");

    reader.ReadToFollowing("price");
    Console.WriteLine($"price: {reader.ReadElementContentAsDecimal()}");

    Console.WriteLine("-------------------------");
}

The example reads two books from the books.xml file.

while (reader.ReadToFollowing("book"))
{

We advance to each book element in turn. When there are no more books, the method returns false and the loop ends.

Console.WriteLine($"genre: {reader.GetAttribute("genre")}");

The GetAttribute method reads an attribute of the current element by name, without moving the reader.

reader.ReadToFollowing("title");
Console.WriteLine($"title: {reader.ReadElementContentAsString()}");

We read the contents of the title element. The ReadElementContentAs... methods leave the reader after the end tag of the element.

$ dotnet run
genre: Science Fiction
title: Dune
author: Frank Herbert
price: 8.99
-------------------------
genre: Novel
title: Old Goriot
author: Honoré de Balzac
price: 9.0
-------------------------

This approach depends on the order of the elements in the file. If a book lacked an author, ReadToFollowing("author") would run on into the next book. For documents with optional parts, use the node-by-node approach shown below.

C# XmlReader Read

The Read method reads the next node from the stream. It returns false at the end of the document.

products.xml
<?xml version="1.0" encoding="utf-8"?>
<products>
    <product>
        <id>1</id>
        <name>Product A</name>
        <price>1200</price>
    </product>

    <product>
        <id>2</id>
        <name>Product B</name>
        <price>1100</price>
    </product>
</products>

In this XML file, we have products.

Program.cs
using System.Xml;

Console.WriteLine("Products");
Console.WriteLine("----------------------");

var settings = new XmlReaderSettings { IgnoreWhitespace = true };
using var reader = XmlReader.Create("products.xml", settings);

reader.MoveToContent();           // skip the declaration, stop on <products>

while (reader.Read())
{
    string result = reader.NodeType switch
    {
        XmlNodeType.Element when reader.Name == "product" => $"{reader.Name}\n",
        XmlNodeType.Element => $"{reader.Name}: ",
        XmlNodeType.Text => $"{reader.Value}\n",
        XmlNodeType.EndElement when reader.Name == "product" => "----------------------\n",
        _ => ""
    };

    Console.Write(result);
}

The example presents the classic approach to reading XML files. We read the document node by node and deal with elements, text, and other node types in a switch expression.

var settings = new XmlReaderSettings { IgnoreWhitespace = true };

By default, the white space between elements is reported as Whitespace nodes. We ask the reader to skip it.

reader.MoveToContent();

We skip the XML declaration and position the reader on the root element.

string result = reader.NodeType switch
{
    XmlNodeType.Element when reader.Name == "product" => $"{reader.Name}\n",
    XmlNodeType.Element => $"{reader.Name}: ",
    XmlNodeType.Text => $"{reader.Value}\n",
    XmlNodeType.EndElement when reader.Name == "product" => "----------------------\n",
    _ => ""
};

We use a switch expression to process the different node types. The Name property gives the name of the current node, and the Value property gets its text value.

$ dotnet run
Products
----------------------
product
id: 1
name: Product A
price: 1200
----------------------
product
id: 2
name: Product B
price: 1100
----------------------

C# XmlReader node types and attributes

To see what the reader reports, we print every node of a small document with its type and depth. The example also shows how to read attributes.

Program.cs
using System.Xml;

string xml = """
    <?xml version="1.0"?>
    <!-- a comment -->
    <items>
        <item id="1" type="a">first</item>
        <item id="2" type="b"/>
        <![CDATA[raw <text>]]>
    </items>
    """;

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

while (reader.Read())
{
    Console.Write($"{reader.NodeType,-15} depth={reader.Depth}");

    if (reader.NodeType == XmlNodeType.Element)
    {
        Console.Write($" name={reader.Name} empty={reader.IsEmptyElement} attrs={reader.AttributeCount}");

        if (reader.HasAttributes)
        {
            while (reader.MoveToNextAttribute())
            {
                Console.Write($" [{reader.Name}={reader.Value}]");
            }

            reader.MoveToElement();
        }
    }
    else if (reader.HasValue)
    {
        Console.Write($" value='{reader.Value.Trim()}'");
    }

    Console.WriteLine();
}

The document contains a declaration, a comment, elements with attributes, an empty element, and a CDATA section.

if (reader.HasAttributes)
{
    while (reader.MoveToNextAttribute())
    {
        Console.Write($" [{reader.Name}={reader.Value}]");
    }

    reader.MoveToElement();
}

MoveToNextAttribute moves the reader from the element to its attributes one by one. When we are done, MoveToElement moves back to the owning element; otherwise the next Read would continue from the last attribute. The IsEmptyElement property is true for an element written as <item/>, which has no end tag node.

$ dotnet run
XmlDeclaration  depth=0 value='version="1.0"'
Whitespace      depth=0 value=''
Comment         depth=0 value='a comment'
Whitespace      depth=0 value=''
Element         depth=0 name=items empty=False attrs=0
Whitespace      depth=1 value=''
Element         depth=1 name=item empty=False attrs=2 [id=1] [type=a]
Text            depth=2 value='first'
EndElement      depth=1
Whitespace      depth=1 value=''
Element         depth=1 name=item empty=True attrs=2 [id=2] [type=b]
Whitespace      depth=1 value=''
CDATA           depth=1 value='raw <text>'
Whitespace      depth=1 value=''
EndElement      depth=0

C# XmlReaderSettings

The XmlReaderSettings class configures the reader. The most used properties are IgnoreWhitespace, IgnoreComments, IgnoreProcessingInstructions, DtdProcessing, ValidationType, and Async.

Program.cs
using System.Xml;

var settings = new XmlReaderSettings
{
    IgnoreWhitespace = true,
    IgnoreComments = true,
    IgnoreProcessingInstructions = true
};

string xml = """
    <?xml version="1.0"?>
    <!-- ignored -->
    <items>
        <item>one</item>
        <item>two</item>
    </items>
    """;

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

int nodes = 0;
while (reader.Read())
{
    nodes++;
}

Console.WriteLine($"{nodes} nodes");

We count the nodes the reader reports with white space, comments and processing instructions ignored.

$ dotnet run
9 nodes

The nodes are the declaration, the items element, two item elements with their text and end tags, and the final end tag. The XML declaration is still reported.

By default, the reader prohibits DTDs, which protects against entity expansion attacks. Do not enable DtdProcessing.Parse for untrusted input. The reader can also validate against an XML schema; see the XmlDocument tutorial.

C# XmlReader streaming large files

The real strength of XmlReader is processing files that are too big to load into a DOM. We can combine it with an iterator method to turn the XML into a lazy sequence of objects, and use ReadSubtree to confine the reading of each record.

Program.cs
using System.Xml;

foreach (var p in ReadProducts("products.xml"))
{
    Console.WriteLine(p);
}

// a lazy sequence: only one product is in memory at a time, even for a huge file
static IEnumerable<Product> ReadProducts(string path)
{
    using var reader = XmlReader.Create(path);

    reader.MoveToContent();

    while (reader.ReadToFollowing("product"))
    {
        using XmlReader sub = reader.ReadSubtree();
        sub.Read();

        int id = 0;
        string name = "";
        decimal price = 0;

        while (sub.Read())
        {
            if (sub.NodeType != XmlNodeType.Element)
            {
                continue;
            }

            switch (sub.Name)
            {
                case "id": id = sub.ReadElementContentAsInt(); break;
                case "name": name = sub.ReadElementContentAsString(); break;
                case "price": price = sub.ReadElementContentAsDecimal(); break;
            }
        }

        yield return new Product(id, name, price);
    }
}

record Product(int Id, string Name, decimal Price);

The ReadProducts method yields one Product at a time.

using XmlReader sub = reader.ReadSubtree();

ReadSubtree returns a reader that sees only the current element and its descendants. The inner loop cannot run past the end of the product, even if an optional element is missing. When the subtree reader is closed, the outer reader is positioned on the end tag of the element.

yield return new Product(id, name, price);

The caller receives each product as soon as it has been read. Only one product is in memory at any time, so a gigabyte file can be processed with a few kilobytes of memory, and the caller can stop early with break or LINQ's Take.

$ dotnet run
Product { Id = 1, Name = Product A, Price = 1200 }
Product { Id = 2, Name = Product B, Price = 1100 }

C# XmlReader async

When the XML comes from a network stream or a slow disk, we can read it asynchronously. The settings must have Async set to true; otherwise the asynchronous methods throw.

Program.cs
using System.Xml;

var settings = new XmlReaderSettings { Async = true, IgnoreWhitespace = true };

using var stream = File.OpenRead("books.xml");
using var reader = XmlReader.Create(stream, settings);

while (await reader.ReadAsync())
{
    if (reader.NodeType == XmlNodeType.Element && reader.Name == "title")
    {
        string title = await reader.ReadElementContentAsStringAsync();
        Console.WriteLine(title);
    }
}

The example prints the titles of the books asynchronously.

var settings = new XmlReaderSettings { Async = true, IgnoreWhitespace = true };

The Async property enables asynchronous reading.

while (await reader.ReadAsync())
{
    if (reader.NodeType == XmlNodeType.Element && reader.Name == "title")
    {
        string title = await reader.ReadElementContentAsStringAsync();
        Console.WriteLine(title);
    }
}

Each synchronous method has an asynchronous counterpart with the Async suffix.

$ dotnet run
Dune
Old Goriot

C# XmlReader Skip and errors

The Skip method skips the current node and all its children. Care is needed when mixing it with the ReadElementContentAs... methods, which already advance the reader; calling Read after them would skip the node that follows.

Program.cs
using System.Xml;

string xml = "<root><a>1</a><b>2</b><a>3</a></root>";

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

reader.MoveToContent();
reader.Read();                      // move inside <root>

int sum = 0;

while (!reader.EOF)
{
    if (reader.NodeType == XmlNodeType.Element && reader.Name == "a")
    {
        sum += reader.ReadElementContentAsInt();   // consumes </a> and moves on
    }
    else
    {
        reader.Skip();              // skips this node and all its children
    }
}

Console.WriteLine(sum);

// malformed XML throws as soon as the problem is read
try
{
    using var bad = XmlReader.Create(new StringReader("<a><b></a>"));
    while (bad.Read()) { }
}
catch (XmlException e)
{
    Console.WriteLine($"line {e.LineNumber}, position {e.LinePosition}: {e.Message}");
}

The program sums the values of the a elements and ignores the others. Then it reads malformed XML.

sum += reader.ReadElementContentAsInt();

The method consumes the whole element, so the loop must not call Read in this branch.

catch (XmlException e)
{
    Console.WriteLine($"line {e.LineNumber}, position {e.LinePosition}: {e.Message}");
}

Malformed XML throws an XmlException when the reader reaches the problem, and not earlier. The exception reports the line and position. Because the reader is forward-only, nodes that were read before the error have already been processed, so code that needs all-or-nothing behaviour must validate first or collect the results and use them only at the end.

$ dotnet run
4
line 1, position 9: The 'b' start tag on line 1 position 5 does not match the end tag of 'a'. Line 1, position 9.

C# XmlReader namespaces

XML namespaces qualify element and attribute names. The reader exposes the LocalName, NamespaceURI, and Prefix of every node, and several methods accept a namespace URI, so our code does not depend on the prefixes used in the document.

Program.cs
using System.Xml;

string xml = """
    <feed xmlns="http://example.com/feed" xmlns:m="http://example.com/meta">
        <entry m:lang="en">hello</entry>
        <entry m:lang="sk">ahoj</entry>
    </feed>
    """;

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

while (reader.ReadToFollowing("entry", "http://example.com/feed"))
{
    string? lang = reader.GetAttribute("lang", "http://example.com/meta");
    Console.WriteLine($"{reader.LocalName} ({reader.NamespaceURI}) lang={lang}: {reader.ReadElementContentAsString()}");
}

The document uses a default namespace for the elements and a prefixed namespace for an attribute.

while (reader.ReadToFollowing("entry", "http://example.com/feed"))
{
    string? lang = reader.GetAttribute("lang", "http://example.com/meta");

We look for the entry element in the given namespace, and for the lang attribute in the other one. An unprefixed attribute has no namespace, and an unprefixed element takes the default namespace.

$ dotnet run
entry (http://example.com/feed) lang=en: hello
entry (http://example.com/feed) lang=sk: ahoj

Source

XmlReader class

In this article we have read XML data with XmlReader.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all C# tutorials.