Io

8.2.2. XML Serialization та LINQ to XML

8.2.2. XML Serialization та LINQ to XML

Вступ: XML у сучасному світі

XML (eXtensible Markup Language) — це текстовий формат для представлення структурованих даних, який домінував у світі веб-сервісів до появи JSON. Хоча JSON витіснив XML у багатьох сценаріях (особливо REST API), XML все ще широко використовується у:

  • Корпоративні системи: SOAP веб-сервіси, Enterprise Service Bus (ESB)
  • Конфігураційні файли: web.config, app.config, MSBuild (.csproj)
  • Офісні документи: Microsoft Office (.docx, .xlsx — це ZIP архіви з XML всередині)
  • Android розробка: Layout файли, Manifest
  • Фінансові та медичні системи: HL7, FHIR, FpML (строгі стандарти вимагають XML Schema)
Передумови: Розуміння основного синтаксису XML, знання C# (класи, властивості, колекції, LINQ basics).

XML vs JSON: Коли що використовувати?

КритерійXMLJSON
ЧитабельністьВербознийКомпактний
РозмірБільший (теги, закриваючі теги)Менший
Швидкість парсингуПовільнішеШвидше
ТипізаціяXML Schema (XSD) validationJSON Schema (менш поширений)
Коментарі✅ Підтримка <!-- -->❌ Немає
Атрибути<person age="25">❌ Лише значення
Namespaces✅ Сильна підтримка❌ Немає
ВикористанняLegacy, Enterprise, ДокументиModern APIs, Web, Mobile

Приклад порівняння:

<?xml version="1.0" encoding="UTF-8"?>
<person>
  <name>Олександр</name>
  <age>30</age>
  <isStudent>true</isStudent>
  <skills>
    <skill>C#</skill>
    <skill>SQL</skill>
  </skills>
</person>
Рекомендація: Для нових проєктів використовуйте JSON. XML — тільки якщо вимагається legacy інтеграція, XML Schema validation, або специфічні галузеві стандарти.

XmlSerializer: Класичний підхід

XmlSerializer — це вбудований .NET клас для серіалізації об'єктів у XML та назад. Він використовує reflection та атрибути для контролю XML структури.

Базова серіалізація

using System;
using System.IO;
using System.Xml.Serialization;

[XmlRoot("Student")]  // Кореневий елемент XML
public class Student
{
    public int Id { get; set; }
    public string Name { get; set; }
    public double GPA { get; set; }
}

class XmlSerializationBasics
{
    static void Main()
    {
        var student = new Student
        {
            Id = 101,
            Name = "Марина Коваль",
            GPA = 4.5
        };
        
        // СЕРІАЛІЗАЦІЯ в XML
        var serializer = new XmlSerializer(typeof(Student));
        using (var writer = new StringWriter())
        {
            serializer.Serialize(writer, student);
            string xml = writer.ToString();
            Console.WriteLine("=== XML ===");
            Console.WriteLine(xml);
        }
        
        /* Вивід:
        <?xml version="1.0" encoding="utf-16"?>
        <Student xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
          <Id>101</Id>
          <Name>Марина Коваль</Name>
          <GPA>4.5</GPA>
        </Student>
        */
        
        // ДЕСЕРІАЛІЗАЦІЯ з XML
        string xmlInput = @"
        <Student>
            <Id>202</Id>
            <Name>Ігор Петренко</Name>
            <GPA>3.8</GPA>
        </Student>";
        
        using (var reader = new StringReader(xmlInput))
        {
            var deserialized = (Student)serializer.Deserialize(reader);
            Console.WriteLine($"\n=== ДЕСЕРІАЛІЗОВАНО ===");
            Console.WriteLine($"ID: {deserialized.Id}, Ім'я: {deserialized.Name}, GPA: {deserialized.GPA}");
        }
    }
}

Розбір коду:

  • Рядок 5: [XmlRoot("Student")] задає назву кореневого елемента (необов'язково, за замовчуванням — назва класу).
  • Рядок 25: Створюємо XmlSerializer для типу Student.
  • Рядок 28: Serialize(TextWriter, object) записує XML у потік.
  • Рядок 53: Deserialize(TextReader) читає XML та створює об'єкт.
Default Namespaces: За замовчуванням XmlSerializer додає XML Schema namespaces (xmlns:xsi, xmlns:xsd). Для їх видалення див. приклад нижче.

Видалення namespaces

var serializer = new XmlSerializer(typeof(Student));
var namespaces = new XmlSerializerNamespaces();
namespaces.Add("", "");  // Порожній namespace

using (var writer = new StringWriter())
{
    serializer.Serialize(writer, student, namespaces);
    Console.WriteLine(writer.ToString());
}

/* Вивід (без namespaces):
<?xml version="1.0" encoding="utf-16"?>
<Student>
  <Id>101</Id>
  <Name>Марина Коваль</Name>
  <GPA>4.5</GPA>
</Student>
*/

Атрибути серіалізації XML

XmlElement: Налаштування елементів

using System;
using System.Xml.Serialization;

public class Book
{
    [XmlElement("BookID")]
    public int Id { get; set; }
    
    [XmlElement("Title")]
    public string BookTitle { get; set; }
    
    [XmlElement("Author")]
    public string AuthorName { get; set; }
}

Результат XML:

<Book>
  <BookID>1</BookID>
  <Title>Clean Code</Title>
  <Author>Robert Martin</Author>
</Book>

Без атрибутів було б: <Id>1</Id>, <BookTitle>Clean Code</BookTitle> тощо.

XmlAttribute: Властивості як атрибути

using System;
using System.Xml.Serialization;

[XmlRoot("Product")]
public class Product
{
    [XmlAttribute("id")]
    public int ProductId { get; set; }
    
    [XmlAttribute("category")]
    public string Category { get; set; }
    
    [XmlElement("Name")]
    public string ProductName { get; set; }
    
    public decimal Price { get; set; }
}

Результат XML:

<Product id="42" category="Electronics">
  <Name>Ноутбук</Name>
  <Price>25000</Price>
</Product>

Розбір:

  • [XmlAttribute] — значення стає атрибутом тега (id="42").
  • [XmlElement] або без атрибуту — значення стає вкладеним елементом (<Name>...</Name>).
Коли використовувати атрибути vs елементи?
  • Атрибути: Метадані, ID, прості значення (не можуть містити вкладені об'єкти).
  • Елементи: Складні дані, колекції, вкладені об'єкти.

XmlIgnore: Виключення властивостей

public class User
{
    public string Username { get; set; }
    
    [XmlIgnore]
    public string PasswordHash { get; set; }  // НЕ серіалізується!
    
    public string Email { get; set; }
}

Результат XML:

<User>
  <Username>vasyl_ua</Username>
  <Email>vasyl@example.com</Email>
</User>

PasswordHash повністю відсутній у XML (аналогічно [JsonIgnore]).

XmlArray та XmlArrayItem: Колекції

using System;
using System.Collections.Generic;
using System.Xml.Serialization;

public class Course
{
    public string CourseName { get; set; }
    
    [XmlArray("Participants")]
    [XmlArrayItem("Student")]
    public List<string> Students { get; set; }
}

Результат XML:

<Course>
  <CourseName>C# Advanced</CourseName>
  <Participants>
    <Student>Олена</Student>
    <Student>Дмитро</Student>
    <Student>Анна</Student>
  </Participants>
</Course>

Без атрибутів було б:

<Students>
  <string>Олена</string>
  <string>Дмитро</string>
</Students>
Best Practice: Завжди вказуйте [XmlArrayItem] для читабельності XML (інакше будуть використані назви типів, наприклад <string>).

Робота з складними об'єктами

Вкладені об'єкти

using System;
using System.IO;
using System.Xml.Serialization;

public class Address
{
    public string City { get; set; }
    public string Street { get; set; }
    public string ZipCode { get; set; }
}

[XmlRoot("Employee")]
public class Employee
{
    [XmlAttribute("id")]
    public int Id { get; set; }
    
    public string FullName { get; set; }
    
    [XmlElement("HomeAddress")]
    public Address Address { get; set; }
}

class NestedObjectsDemo
{
    static void Main()
    {
        var employee = new Employee
        {
            Id = 12345,
            FullName = "Олександра Бондаренко",
            Address = new Address
            {
                City = "Львів",
                Street = "вул. Шевченка, 10",
                ZipCode = "79000"
            }
        };
        
        var serializer = new XmlSerializer(typeof(Employee));
        var namespaces = new XmlSerializerNamespaces();
        namespaces.Add("", "");
        
        using (var writer = new StringWriter())
        {
            serializer.Serialize(writer, employee, namespaces);
            Console.WriteLine(writer.ToString());
        }
    }
}

Результат XML:

<?xml version="1.0" encoding="utf-16"?>
<Employee id="12345">
  <FullName>Олександра Бондаренко</FullName>
  <HomeAddress>
    <City>Львів</City>
    <Street>вул. Шевченка, 10</Street>
    <ZipCode>79000</ZipCode>
  </HomeAddress>
</Employee>

Колекції складних об'єктів

using System;
using System.Collections.Generic;
using System.IO;
using System.Xml.Serialization;

public class Task
{
    [XmlAttribute("id")]
    public int TaskId { get; set; }
    
    public string Description { get; set; }
    public bool IsCompleted { get; set; }
}

[XmlRoot("TaskList")]
public class TaskList
{
    [XmlArray("Tasks")]
    [XmlArrayItem("Task")]
    public List<Task> Tasks { get; set; }
}

class CollectionDemo
{
    static void Main()
    {
        var taskList = new TaskList
        {
            Tasks = new List<Task>
            {
                new Task { TaskId = 1, Description = "Написати звіт", IsCompleted = true },
                new Task { TaskId = 2, Description = "Провести зустріч", IsCompleted = false },
                new Task { TaskId = 3, Description = "Оновити документацію", IsCompleted = false }
            }
        };
        
        var serializer = new XmlSerializer(typeof(TaskList));
        var namespaces = new XmlSerializerNamespaces();
        namespaces.Add("", "");
        
        using (var writer = new StringWriter())
        {
            serializer.Serialize(writer, taskList, namespaces);
            Console.WriteLine(writer.ToString());
        }
    }
}

Результат XML:

<?xml version="1.0" encoding="utf-16"?>
<TaskList>
  <Tasks>
    <Task id="1">
      <Description>Написати звіт</Description>
      <IsCompleted>true</IsCompleted>
    </Task>
    <Task id="2">
      <Description>Провести зустріч</Description>
      <IsCompleted>false</IsCompleted>
    </Task>
    <Task id="3">
      <Description>Оновити документацію</Description>
      <IsCompleted>false</IsCompleted>
    </Task>
  </Tasks>
</TaskList>

LINQ to XML: Сучасний підхід

XmlSerializer чудовий для серіалізації об'єктів, але що якщо потрібно:

  • Створити XML програмно (без класів)?
  • Запитати конкретні елементи з великого XML?
  • Модифікувати існуючий XML?

Для цього використовується LINQ to XML — функціональний API для роботи з XML через класи XDocument, XElement, XAttribute.

Створення XML з XElement

using System;
using System.Xml.Linq;

class LinqToXmlCreation
{
    static void Main()
    {
        // Створюємо XML програмно
        var xml = new XElement("Library",
            new XElement("Book",
                new XAttribute("isbn", "978-0-13-468599-1"),
                new XElement("Title", "Clean Code"),
                new XElement("Author", "Robert C. Martin"),
                new XElement("Year", 2008)
            ),
            new XElement("Book",
                new XAttribute("isbn", "978-0-201-61622-4"),
                new XElement("Title", "The Pragmatic Programmer"),
                new XElement("Author", "Andy Hunt"),
                new XElement("Year", 1999)
            )
        );
        
        Console.WriteLine(xml);
    }
}

Результат:

<Library>
  <Book isbn="978-0-13-468599-1">
    <Title>Clean Code</Title>
    <Author>Robert C. Martin</Author>
    <Year>2008</Year>
  </Book>
  <Book isbn="978-0-201-61622-4">
    <Title>The Pragmatic Programmer</Title>
    <Author>Andy Hunt</Author>
    <Year>1999</Year>
  </Book>
</Library>

Розбір коду:

  • Рядок 9: XElement — це один XML елемент.
  • Рядок 10-14: Вкладені елементи передаються як параметри конструктора.
  • Рядок 11: XAttribute додає атрибут isbn="...".
Читабельність: LINQ to XML дозволяє писати XML структуру майже так само, як вона виглядає у файлі — це називається functional construction.

Створення з LINQ запитом

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

class LinqToXmlFromData
{
    static void Main()
    {
        var students = new[]
        {
            new { Id = 1, Name = "Анна", Grade = 95 },
            new { Id = 2, Name = "Богдан", Grade = 88 },
            new { Id = 3, Name = "Вікторія", Grade = 92 }
        };
        
        // Генеруємо XML з масиву даних
        var xml = new XElement("Students",
            from student in students
            where student.Grade >= 90
            select new XElement("Student",
                new XAttribute("id", student.Id),
                new XElement("Name", student.Name),
                new XElement("Grade", student.Grade)
            )
        );
        
        Console.WriteLine(xml);
    }
}

Результат:

<Students>
  <Student id="1">
    <Name>Анна</Name>
    <Grade>95</Grade>
  </Student>
  <Student id="3">
    <Name>Вікторія</Name>
    <Grade>92</Grade>
  </Student>
</Students>

Тільки студенти з оцінкою ≥90!


Запити до XML з LINQ

Завантаження XML

using System;
using System.Xml.Linq;

class LoadXmlDemo
{
    static void Main()
    {
        string xmlString = @"
        <Library>
            <Book isbn='978-1'>
                <Title>Book A</Title>
                <Author>Author 1</Author>
                <Price>29.99</Price>
            </Book>
            <Book isbn='978-2'>
                <Title>Book B</Title>
                <Author>Author 2</Author>
                <Price>39.99</Price>
            </Book>
        </Library>";
        
        // Завантаження з рядка
        XDocument doc = XDocument.Parse(xmlString);
        
        // Або з файлу:
        // XDocument doc = XDocument.Load("library.xml");
        
        Console.WriteLine(doc);
    }
}

Запит елементів

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

class QueryXmlDemo
{
    static void Main()
    {
        var xml = XElement.Parse(@"
        <Library>
            <Book isbn='978-1'>
                <Title>Clean Code</Title>
                <Author>Robert Martin</Author>
                <Price>35.00</Price>
            </Book>
            <Book isbn='978-2'>
                <Title>Design Patterns</Title>
                <Author>Gang of Four</Author>
                <Price>45.00</Price>
            </Book>
            <Book isbn='978-3'>
                <Title>Refactoring</Title>
                <Author>Martin Fowler</Author>
                <Price>40.00</Price>
            </Book>
        </Library>");
        
        // Запит 1: Всі назви книг
        Console.WriteLine("=== ВСІ НАЗВИ ===");
        var titles = xml.Descendants("Title").Select(t => t.Value);
        foreach (var title in titles)
        {
            Console.WriteLine($"- {title}");
        }
        
        // Запит 2: Книги дорожче $40
        Console.WriteLine("\n=== КНИГИ ДОРОЖЧЕ $40 ===");
        var expensiveBooks = from book in xml.Elements("Book")
                             let price = (decimal)book.Element("Price")
                             where price > 40
                             select new
                             {
                                 Title = (string)book.Element("Title"),
                                 Price = price
                             };
        
        foreach (var book in expensiveBooks)
        {
            Console.WriteLine($"{book.Title}: ${book.Price}");
        }
        
        // Запит 3: Отримати атрибут ISBN
        Console.WriteLine("\n=== ISBN КНИЖОК ===");
        var isbns = xml.Elements("Book")
                       .Select(b => b.Attribute("isbn").Value);
        
        foreach (var isbn in isbns)
        {
            Console.WriteLine(isbn);
        }
    }
}

Вивід:

=== ВСІ НАЗВИ ===
- Clean Code
- Design Patterns
- Refactoring

=== КНИГИ ДОРОЖЧЕ $40 ===
Design Patterns: $45.00

=== ISBN КНИЖОК ===
978-1
978-2
978-3

Розбір коду:

  • Рядок 31: Descendants("Title") знаходить всі елементи <Title> на будь-якій глибині.
  • Рядок 39: Elements("Book") знаходить ПРЯМІ дочірні елементи <Book>.
  • Рядок 40: (decimal)book.Element("Price") — explicit cast для конвертації XML елемента у тип.
  • Рядок 56: Attribute("isbn").Value отримує значення атрибуту.
Elements vs Descendants:
  • Elements("Name") — тільки прямі нащадки.
  • Descendants("Name") — всідіректорно (усі рівні вглиб).

Модифікація XML

Додавання елементів

using System;
using System.Xml.Linq;

class ModifyXmlDemo
{
    static void Main()
    {
        var library = XElement.Parse(@"
        <Library>
            <Book>
                <Title>Existing Book</Title>
            </Book>
        </Library>");
        
        // Додаємо нову книгу
        library.Add(
            new XElement("Book",
                new XElement("Title", "New Book"),
                new XElement("Author", "New Author")
            )
        );
        
        Console.WriteLine(library);
    }
}

Результат:

<Library>
  <Book>
    <Title>Existing Book</Title>
  </Book>
  <Book>
    <Title>New Book</Title>
    <Author>New Author</Author>
  </Book>
</Library>

Оновлення значень

var priceElement = xml.Descendants("Price").First();
priceElement.Value = "29.99";  // Оновлюємо ціну

// Або через SetValue:
priceElement.SetValue(29.99m);

Видалення елементів

// Видаляємо всі книги дорожче $40
xml.Elements("Book")
   .Where(b => (decimal)b.Element("Price") > 40)
   .Remove();

Порівняння: XmlSerializer vs LINQ to XML

КритерійXmlSerializerLINQ to XML
ПризначенняObject ↔ XML serializationXML маніпуляція та запити
ПідхідДекларативний (атрибути)Імперативний (код)
ТипізаціяStrongly-typed (класи)Динамічний (XElement)
Запити❌ Немає✅ Потужні LINQ запити
Модифікація❌ Треба змінювати об'єкт✅ Пряма модифікація XML
ШвидкістьШвидка для серіалізаціїШвидка для запитів
Коли використовуватиDTO serialization, APIXML processing, конфігурації
Best Practice:
  • XmlSerializer — для серіалізації бізнес-об'єктів (API, збереження стану).
  • LINQ to XML — для обробки XML документів (читання конфігурацій, парсинг RSS, трансформації).

Практичні завдання

Завдання 1: RSS Feed Parser

Створіть парсер RSS feed:

  1. Завантажте XML з URL (наприклад, https://example.com/rss.xml).
  2. Використайте LINQ to XML для витягування: назва статті, дата, посилання.
  3. Виведіть топ-5 останніх статей.

Завдання 2: Configuration Manager

Реалізуйте систему конфігурацій на XML:

  1. Клас AppConfig з властивостями DatabaseConnection, LogLevel, Features.
  2. Серіалізація у app.config.xml за допомогою XmlSerializer.
  3. Метод для оновлення одного параметру через LINQ to XML (без повної десеріалізації).

Завдання 3: XML to JSON Converter

Створіть конвертер:

  1. Зчитайте XML файл через XDocument.
  2. Трансформуйте його у об'єкт C# (dynamic або конкретний клас).
  3. Серіалізуйте у JSON за допомогою System.Text.Json.
  4. Порівняйте розміри вихідного XML та JSON.

Резюме

XmlSerializer

Класичний підхід для серіалізації C# об'єктів у XML. Використовує атрибути для контролю структури.

Атрибути

[XmlRoot], [XmlElement], [XmlAttribute], [XmlIgnore], [XmlArray] — точний контроль серіалізації.

LINQ to XML

Сучасний API для створення, запитів та модифікації XML через XDocument, XElement, XAttribute.

Запити

LINQ дозволяє елегантно фільтрувати та трансформувати XML дані.
Наступний крок: У наступному матеріалі розглянемо Binary Serialization з сучасними бібліотеками MessagePack та Protocol Buffers як альтернативи deprecated BinaryFormatter.

Посилання