Как читать и разбирать XML-файлы в C#
Прочитать XML в C# можно четырьмя основными способами: через LINQ to XML (XDocument), XmlDocument с XPath, потоковый XmlReader и десериализацию в классы. Ниже у каждого способа свой пример на .NET 8, таблица выбора, работа с атрибутами и пространствами имён, а также разбор ошибок, из-за которых код падает на реальных файлах.

Содержание
Пример XML-файла для разбора
Все примеры используют один файл books.xml: каталог книг с атрибутом, вложенными элементами и необязательным полем. Сохраните его рядом с исполняемым файлом программы.
<?xml version="1.0" encoding="utf-8"?>
<catalog>
<book id="1">
<title>Язык C# в подробностях</title>
<author>Иван Петров</author>
<price currency="RUB">1290.50</price>
<year>2022</year>
</book>
<book id="2">
<title>Алгоритмы на практике</title>
<author>Мария Орлова</author>
<price currency="RUB">990</price>
</book>
</catalog>
Обратите внимание, что у второй книги нет элемента year. Именно такие необязательные поля чаще всего вызывают NullReferenceException при наивном чтении, и ниже показано, как с ними обращаться.
Какой способ выбрать
| Способ | Когда подходит | Память | Особенности |
|---|---|---|---|
| XDocument (LINQ to XML) | Файлы до десятков мегабайт, запросы и фильтры | Весь документ в памяти | Самый удобный современный API |
| XmlDocument и XPath | Готовый XPath-запрос, старый код | Весь документ в памяти | Старый DOM-интерфейс, но он всё ещё работает |
| XmlReader | Огромные файлы, потоки | Почти не растёт | Только вперёд, больше кода |
| XmlSerializer | Структура известна заранее | Объекты в памяти | Получаем типизированные классы |
Если сомневаетесь, начинайте с XDocument: он покрывает большинство задач и читается лучше остальных вариантов.
Чтение через XDocument и LINQ to XML
Пространство имён System.Xml.Linq позволяет загрузить файл одной строкой и выбирать данные запросами LINQ. Метод Elements возвращает дочерние элементы, Element - первый дочерний элемент с таким именем, а Attribute - атрибут.
using System.Globalization;
using System.Xml.Linq;
XDocument doc = XDocument.Load("books.xml");
var books = doc.Root!.Elements("book").Select(b => new
{
Id = (int)b.Attribute("id")!,
Title = (string?)b.Element("title"),
Author = (string?)b.Element("author"),
Price = decimal.Parse(b.Element("price")!.Value, CultureInfo.InvariantCulture),
Year = (int?)b.Element("year")
});
foreach (var b in books)
Console.WriteLine($"{b.Id}: {b.Title}, {b.Author}, {b.Price} руб., год: {b.Year?.ToString() ?? "не указан"}");
Ключевой приём здесь - явное приведение элемента к типу: (int?)b.Element("year") вернёт null, если элемента нет, и не бросит исключения. Приведение (string?) работает аналогично. Цену мы парсим с CultureInfo.InvariantCulture, потому что в XML десятичный разделитель - точка, а на русской локали decimal.Parse без этого параметра споткнётся на "1290.50".
Фильтры и сортировка
Поскольку это обычный LINQ, условия пишутся как для любой коллекции. Вот выборка книг дороже тысячи рублей по возрастанию года.
var expensive = doc.Descendants("book")
.Where(b => (decimal)b.Element("price")! > 1000m)
.OrderBy(b => (int?)b.Element("year") ?? 0)
.Select(b => (string?)b.Element("title"));
Метод Descendants ищет элементы на любой глубине, а не только среди прямых потомков, что удобно, когда вложенность нестабильна. Но он медленнее, чем Elements, на больших документах.
Чтение атрибутов
Атрибуты читаются методом Attribute либо коллекцией Attributes(). Значение цены лежит в тексте элемента, а валюта - в атрибуте.
XElement price = doc.Root!.Element("book")!.Element("price")!;
string? currency = (string?)price.Attribute("currency"); // RUB
foreach (XAttribute a in price.Attributes())
Console.WriteLine($"{a.Name} = {a.Value}");
XmlDocument и XPath
Класс XmlDocument появился ещё в первых версиях .NET и до сих пор встречается в старом коде. Его сильная сторона - XPath: выражения вроде //book[price>1000]/title выбирают узлы одной строкой.
using System.Xml;
var doc = new XmlDocument();
doc.Load("books.xml");
XmlNodeList? titles = doc.SelectNodes("//book[price>1000]/title");
foreach (XmlNode node in titles!)
Console.WriteLine(node.InnerText);
XmlNode? first = doc.SelectSingleNode("/catalog/book[@id='2']");
Console.WriteLine(first?.SelectSingleNode("author")?.InnerText);
XPath доступен и для XDocument: достаточно подключить System.Xml.XPath и вызвать doc.XPathSelectElements(...). Так что выражения можно использовать и в современном коде, не переходя на старый DOM.
Потоковое чтение через XmlReader
Если файл весит сотни мегабайт, загружать его целиком нельзя. XmlReader читает документ последовательно, держа в памяти только текущий узел. Платой за экономию становится ручное управление состоянием.
using System.Xml;
var settings = new XmlReaderSettings { IgnoreWhitespace = true, IgnoreComments = true };
using var reader = XmlReader.Create("books.xml", settings);
while (reader.Read())
{
if (reader.NodeType == XmlNodeType.Element && reader.Name == "book")
{
string? id = reader.GetAttribute("id");
Console.WriteLine($"Книга {id}");
}
else if (reader.NodeType == XmlNodeType.Element && reader.Name == "title")
{
Console.WriteLine(" " + reader.ReadElementContentAsString());
}
}
Хорошая практика для больших файлов - комбинировать подходы: XmlReader перемещается между записями, а каждую запись превращает в XElement через XNode.ReadFrom и разбирает уже удобным LINQ.
static IEnumerable<XElement> StreamBooks(string path)
{
using var reader = XmlReader.Create(path);
reader.MoveToContent();
while (reader.Read())
{
if (reader.NodeType == XmlNodeType.Element && reader.Name == "book")
yield return (XElement)XNode.ReadFrom(reader);
}
}
Здесь в памяти одновременно находится только одна книга. Один нюанс: после ReadFrom читатель уже стоит на следующем узле, поэтому если книги идут встык без пробелов, часть записей можно пропустить. Надёжнее вызвать reader.ReadToFollowing("book") в цикле.
Десериализация в классы
Когда структура известна заранее, проще описать классы и получить сразу типизированные объекты. Для этого есть XmlSerializer и атрибуты из System.Xml.Serialization. Подробный разбор с вложенными коллекциями приведён в отдельной статье про десериализацию XML в C#, здесь только каркас.
using System.Xml.Serialization;
[XmlRoot("catalog")]
public class Catalog
{
[XmlElement("book")]
public List<Book> Books { get; set; } = new();
}
public class Book
{
[XmlAttribute("id")] public int Id { get; set; }
[XmlElement("title")] public string Title { get; set; } = "";
[XmlElement("author")] public string Author { get; set; } = "";
[XmlElement("price")] public decimal Price { get; set; }
[XmlElement("year")] public int? Year { get; set; }
}
var serializer = new XmlSerializer(typeof(Catalog));
using var stream = File.OpenRead("books.xml");
var catalog = (Catalog)serializer.Deserialize(stream)!;
Console.WriteLine(catalog.Books.Count);
Этот способ короче всех остальных, но жёстко привязан к схеме: любой неожиданный элемент либо игнорируется, либо приводит к ошибке в зависимости от настроек. Для нерегулярных документов безопаснее LINQ to XML.
Пространства имён XML
Самая частая причина "код не находит элемент, хотя он есть в файле" - пространство имён. Если корневой элемент содержит xmlns, то имя каждого элемента нужно задавать вместе с ним.
XNamespace ns = "http://example.com/catalog";
var doc = XDocument.Load("catalog-ns.xml");
var titles = doc.Descendants(ns + "title").Select(t => t.Value);
// Если пространство имён неважно:
var anyTitles = doc.Descendants().Where(e => e.Name.LocalName == "title");
В XmlDocument для XPath приходится создавать XmlNamespaceManager и использовать префикс в выражении, иначе запрос вернёт пустой результат.
Загрузка из строки, потока и сети
Не обязательно читать с диска. XDocument.Parse принимает строку, Load - путь, поток или TextReader. Для HTTP используйте HttpClient и передавайте поток.
XDocument fromString = XDocument.Parse("<a><b>1</b></a>");
using var http = new HttpClient();
await using Stream s = await http.GetStreamAsync("https://example.com/feed.xml");
XDocument fromWeb = await XDocument.LoadAsync(s, LoadOptions.None, CancellationToken.None);
Не создавайте HttpClient на каждый запрос в реальном приложении, используйте один экземпляр или IHttpClientFactory.
Безопасность: внешние сущности
XML умеет подключать внешние ресурсы через DTD, и разбор документа от недоверенного источника может привести к атакам на чтение файлов (XXE) или раздуванию памяти. В современном .NET XDocument и XmlReader по умолчанию запрещают обработку DTD, но в старом коде на XmlDocument и в .NET Framework 4.5.1 и ниже это нужно было отключать вручную. Проверяйте настройки явно, если принимаете файлы от пользователей.
var settings = new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Prohibit,
XmlResolver = null,
MaxCharactersFromEntities = 1024
};
using var reader = XmlReader.Create(stream, settings);
var doc = XDocument.Load(reader);
Типичные ошибки
NullReferenceExceptionпри обращении кElement("x").Value, если элемента нет. Приводите к типу:(string?)el.Element("x").XmlException: Data at the root level is invalid. В начале файла стоит BOM или лишний текст либо файл не в той кодировке. Откройте файл как поток и дайте парсеру определить кодировку по объявлению.FormatExceptionпри разборе чисел: десятичная точка и русская локаль. ИспользуйтеCultureInfo.InvariantCulture.- Пустой результат из-за пространства имён.
OutOfMemoryExceptionна большом файле: переходите на XmlReader.- Недопустимые символы (например, управляющие коды) в тексте: такой файл не является корректным XML и требует предварительной очистки.
Если после чтения нужны данные в другом формате, например JSON, посмотрите статью про преобразование объектов в JSON.
Частые вопросы
Что лучше использовать для чтения XML в C#: XDocument или XmlDocument?
В новом коде выбирайте XDocument. У него лаконичный синтаксис, интеграция с LINQ и более удобная работа с null. XmlDocument оставляйте для поддержки старых проектов.
Как прочитать XML-файл, если он очень большой?
Используйте XmlReader и обрабатывайте записи по одной. Для удобства каждую запись можно превратить в XElement и разобрать LINQ, не загружая остальной документ.
Как получить значение атрибута?
Для XElement - (string?)element.Attribute("name"), для XmlNode - node.Attributes?["name"]?.Value, для XmlReader - reader.GetAttribute("name").
Почему Descendants не находит элемент, который есть в файле?
Почти всегда виновато пространство имён: имя элемента нужно задавать как ns + "title". Проверьте, нет ли в корне атрибута xmlns.
Как прочитать XML из строки?
Метод XDocument.Parse(text) разберёт строку. Для XmlDocument - LoadXml(text).
Можно ли изменять документ и сохранять его обратно?
Да. В XDocument меняйте значения через SetValue, добавляйте узлы через Add, удаляйте через Remove, а затем вызывайте doc.Save("books.xml").





