Конструируем XML с использованием LINQ to XML API

    До недавнего времени мне ужасно не нравилось работать с XML файлами, я старался избегать этого где только можно, по просту заменяя их стандартными конфигурационными файлами приложения. Но где нужно было использовать обмен данными – от XML’а было не уйти. Приходилось создавать большой объем кода для того чтобы сконвертировать либо прочитать небольшой объем информации. Да, таков уж XML Document Object Model (DOM) API.

    Пример использования XML DOM API


    Необходимо получить такое сообщение:
    <Groups>
      <Group Id="523063B0-47C0-4ABE-8D38-4A449137A00B" DepartmentId="D939743F-BFF7-41E4-9A1B-1193EEFF0486">
        <Name>555A</Name>
        <Course>5</Course>
      </Group>
    </Groups>


    Для этого потребуется написать довольно простой, но объемный код:
    private sealed class Nodes
    {
      public const string Root = "Groups";

      // Attributes.
      public const string Group = "Group";
      public const string Id = "Id";
      public const string DepartmentId = "DepartmentId";

      // Elements.
      public const string Name = "Name";
      public const string Course = "Course";
    }

    // Create XML document.
    XmlDocument document = new XmlDocument();

    // Create root element.
    XmlElement rootElement = document.CreateElement(Nodes.Root);

    // Create Group element.
    XmlElement groupElement = document.CreateElement(Nodes.Group);
          
    // Create attributes.
    XmlAttribute idAttribute = document.CreateAttribute(Nodes.Id);
    idAttribute.InnerText = "523063B0-47C0-4ABE-8D38-4A449137A00B";
    groupElement.Attributes.Append(idAttribute);

    XmlAttribute departmentIdAttribute = document.CreateAttribute(Nodes.DepartmentId);
    departmentIdAttribute.InnerText = "D939743F-BFF7-41E4-9A1B-1193EEFF0486";
    groupElement.Attributes.Append(departmentIdAttribute);
          
    // Create elements.
    XmlElement nameElement = document.CreateElement(Nodes.Name);
    nameElement.InnerText = "555A";
    groupElement.AppendChild(nameElement);

    XmlElement courseElement = document.CreateElement(Nodes.Course);
    courseElement.InnerText = "5";
    groupElement.AppendChild(courseElement);

    rootElement.AppendChild(groupElement);
    document.AppendChild(rootElement);


    Согласитесь со мной, что писать, разбирать и сопровождать такой код — не очень приятно.

    По данному коду невозможно получить представления о результирующей структуре XML. С каждым элементом или атрибутом необходимо всегда выполнить три шага: создать, задать свойство InnerText и добавить к уже существующему элементу или документу. Так же, без создания XmlDocument’а невозможно просто создать элемент, а ведь это необходимо достаточно часто.

    Благо компания Microsoft подарила нам не просто возможность выполнения интегрированных запросов LINQ to XML, но и переработала XML DOM API.
    Об этом я и хочу сегодня поведать подробнее. Для этого понадобиться подключить следующие пространства имен:
    using System.Collections.Generic;
    using System.Linq;
    using System.Xml.Linq;


    Как я уже говорил, одним из недостатков стандартной DOM модели было трудночитаемая структура получаемого XML файла. LINQ to XML API решает эту проблему, предлагая новую технику, называемую функциональным конструированием. Функциональное конструирование позволяет конструировать объекты XML и инициализировать их значения — одновременно, в единственном операторе, за счет множества перегруженных конструкторов.

    Давайте посмотрим как будет выглядеть конструирование уже известного нам XML файла при помощи LINQ to XML API:
    XElement document = new XElement(Nodes.Root,
      new XElement(Nodes.Group,
        new XAttribute(Nodes.Id, "523063B0-47C0-4ABE-8D38-4A449137A00B"),
        new XAttribute(Nodes.DepartmentId, "D939743F-BFF7-41E4-9A1B-1193EEFF0486"),
        new XElement(Nodes.Name, "555A"),
        new XElement(Nodes.Course, 5)));

    Ну как? Какой код вам легче читать? Ответ очевидный.

    Объектная модель





    Классы XObject, XNode и XContainer – являются абстрактными, так что создавать мы их не можем.
    Класс XAttribute – не наследуется от XNode, т.е. не является узлом, это простой элемент коллекции, который представляет пару «ключ-значение».
    Классы XDocument и XElement – единственные, которые могут содержать узлы, унаследованные от XNode.

    XDocument


    В LINQ to XML API совсем не обязательно создавать XDocument для того чтобы создать дерево XML или его фрагмент.
    XDocument document = new XDocument();
    Console.WriteLine(document.ToString());

    Получим пустой результат.

    Сам по себе XDocument не имеет ничего полезного, только возможность добавить к документу объявление (XDeclaration), создание типа (XDocumentType) либо инструкции обработки (XProcessingInstruction).

    Имеет статические методы для чтения содержимого документов, с всевозможными вариантами перегрузок:
    public static XDocument Load(Stream stream);
    public static XDocument Load(string uri);
    public static XDocument Load(TextReader textReader);
    public static XDocument Load(XmlReader reader);
    public static XDocument Load(Stream stream, LoadOptions options);
    public static XDocument Load(string uri, LoadOptions options);
    public static XDocument Load(TextReader textReader, LoadOptions options);
    public static XDocument Load(XmlReader reader, LoadOptions options);

    А так же для сохранения документа:
    public void Save(Stream stream);
    public void Save(string fileName);
    public void Save(TextWriter textWriter);
    public void Save(XmlWriter writer);
    public void Save(Stream stream, SaveOptions options);
    public void Save(string fileName, SaveOptions options);
    public void Save(TextWriter textWriter, SaveOptions options);

    Такие же методы имеет и элемент (XElement).

    Я использую XDocument только в том случае, если необходимо задать объявление для документа, которое содержит номер версии и кодировку.

    XDeclaration


    Объекты XDeclaration, как вы уже наверное догадались, содержат объявления и добавляются к документу, хотя компилятор допускает добавление объявления к элементу (XElement), но это не даст никакого видимого результата.
    XDocument document = new XDocument(
      new XDeclaration("1.0", "UTF-8", "yes"));
    Console.WriteLine(document.ToString());

    Данный код так же не выведет никаких строк, хотя если посмотреть отладчик, объявление все-таки добавилось:



    Объявление будет видно, если использовать метод Save(), класса XDocument.

    XElement


    Наиболее часто используемый объект API.
    Имеет множество конструкторов, рассмотрим два из них:

    public XElement(XName name, object content);
    public XElement(XName name, params object[] content);

    Первый конструктор – простейший случай, когда элемент не имеет дочерних элементов и мы задаем только его значение, второй конструктор – с заданием дочерних элементов, таких как элементы, атрибуты, инструкции обработки, комментарии (XComment) и т.д.

    Создадим простейший элемент:
    XElement element = new XElement("Element", 777);
    Console.WriteLine(element.ToString());
    Console.WriteLine((int)element);

    Результат:
    <Element>777</Element>
    777

    Как видите доступ к значению стал еще проще благодаря переопределенному явному приведению элементов, достаточно привести элемент к его исходному типу, для того чтобы посмотреть его значение. XElement переопределяет явное приведение для всех простейших типов. Теперь даже к свойству Value обращаться нет необходимости.
    Вы спросите, конструктор требует XName, а я передаю ему строку? И я вам отвечу: строка неявно приводится к объекту XName. По сути, XName – это и есть строка, только с возможностью задания пространства имен (XNamespace).

    Приведу более детальный пример с созданием XML из объектов:
    public sealed class Teacher
    {
      public Guid Id;
      public string FirstName;
      public string LastName;
    }

    IEnumerable<Teacher> teachers = new[]
    {
      new Teacher() { Id = Guid.NewGuid(), FirstName = "Василий", LastName = "Иванович" },
      new Teacher() { Id = Guid.NewGuid(), FirstName = "Мария", LastName = "Николаевна" }
    };

    XElement elements = new XElement("Teachers",
      teachers.Select(t => new XElement("Teacher",
        new XAttribute("Id", t.Id),
        new XElement("FirstName", t.FirstName),
        new XElement("LastName", t.LastName))));

    Console.WriteLine(elements.ToString());

    В итоге имеем:
    <Teachers>
      <Teacher Id="80321d66-ee4b-4b55-ace8-3892b682d483">
        <FirstName>Василий</FirstName>
        <LastName>Иванович</LastName>
      </Teacher>
      <Teacher Id="33f58b19-1bbb-468f-acee-961edb63ce0b">
        <FirstName>Мария</FirstName>
        <LastName>Николаевна</LastName>
      </Teacher>
    </Teachers>

    XAttribute


    Как я уже и говорил ранее, является парой «ключ-значение» для коллекции атрибутов элемента. Создание и добавление атрибутов к элементу достаточно простая задача:
    XAttribute attribute = new XAttribute("Id", Guid.NewGuid());
    XElement element = new XElement("Teachers");
    element.Add(attribute);
    Console.WriteLine(element.ToString());

    Либо:
    XElement element = new XElement("Teachers",
      new XAttribute("Id", Guid.NewGuid()));
    Console.WriteLine(element.ToString());

    Результат один и тот же:
    <Teachers Id="713c6cbf-f698-42fd-b5fc-74415b93d824" />


    XComment


    Создание и добавление комментариев так же не вызывает никаких сложностей:
    XElement teacherElement = new XElement("Teacher",
      new XComment("Teacher element"));
    Console.WriteLine(teacherElement.ToString());

    Результат:
    <Teacher>
      <!--Teacher element-->
    </Teacher>


    Заключение


    На данном этапе я хотел бы закончить. Да, я не рассмотрел на примерах использование инструкций обработки, создание типов документа и другие классы модели, но данных знаний будет достаточно для создания как простых так и сложных по структуре XML документов.

    В следующей статье я планирую рассмотреть проход по содержимому XML документа.

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 31

      +6
      Попробуйте простую серилизацию классов. На члены класса можно повесить различные XML атрибуты.
      XmlSerializer serializer = new XmlSerializer(typeof_object);
      fs = new FileStream(FileName, FileMode.Create, System.IO.FileAccess.ReadWrite);
      XmlWriter writer = new XmlTextWriter(fs, System.Text.Encoding.Unicode);
      serializer.Serialize(writer, obj);


      Десерилизация выполняется тоже в 4 строчки кода.
      XmlSerializer serializer = new XmlSerializer(typeof_object);
      fs = new FileStream(FileName, FileMode.Create, System.IO.FileAccess.ReadWrite);
      XmlWriter writer = new XmlTextWriter(fs, System.Text.Encoding.Unicode);
      serializer.Serialize(writer, obj);
        0
        А если у вас получатель этого документа написан на
        Java/ JS/ WhatEverNotOnDotNet?
          +4
          Эта штука серилизует как описано атрибутами в классе. Если нужно использовать XML вам — вы знаете как сохранить и загрузить. Автор же пишет, что он их заменял конфигурационными файлами.

          По мне так xml очень удобная вещь для хранения много чего — настроек, данных (не большого объема). Для передачи по сети тоже очень подходит. Ajax там и WCF.
            0
            >Ajax там

            Для Ajax XML — это точно плохой выбор. Ajax должен быть быстрым и легким, дублирование имен полей там явно лишнее. Для Ajax стандартом уже вроде как JSON стал.
            +1
            Без разницы на чем написана получатель, там самый обычный XML, без всяких прибамбасов.

            Так будет выглядить сериализированный ХМЛ, класс Teacher
            
            <Teacher xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http:/
            /www.w3.org/2001/XMLSchema">
              <Id>6eb64959-c267-4dbb-94cc-4163f3fe7a28</Id>
              <FirstName>Maria</FirstName>
              <LastName>Ivanovna</LastName>
            </Teacher>
            
            

            0
            Да, такой способ подходит, но вы здесь забыли упомянуть что для этого потребуется создать объект.
            А есть множество случаев когда XML конструируется из составных объектов, создавать только для сериализации объект — не вариант, в том случае, если получатель будет другой.
            Либо когда данные запрашиваются у БД, можно сразу их трансформировать в XML без загрузки в промежуточные объекты.

            Собственно, где мне пришлось это применять: на веб-сервисах, где источником является MS SQL база на выходе нужно получить XML, который будут использовать клиентское iPhone приложение и Windows Forms.
              0
              Собственно, где мне пришлось это применять: на веб-сервисах, где источником является MS SQL база на выходе нужно получить XML, который будут использовать клиентское iPhone приложение и Windows Forms.

              MS SQL начиная с 2005 отлично умеет конструировать и возвращать XML любой сложности. так же как и извлекать данные из xml полученного на вход.
              или у вас так и было, а проблема была в парсинге XML приложением?
                0
                Либо когда данные запрашиваются у БД, можно сразу их трансформировать в XML без загрузки в промежуточные объекты.

                XDocument тоже полностью грузит в память. Разнице невелика.
                0
                XmlSerializer требует, чтобы сборка, содержащая сериализуемый тип, находилась физически на диске. Иногда мешает.
                  0
                  да? разве не достаточно чтобы сборка была уже загружена в память, не важно каким образом? надо бы проверить, полено знать о таких граблях заранее :")
                    0
                    Вот важно почему-то. После Assembly.Load(byte[]) XmlSerializer работать отказывается.
                    Я потратил несколько часов на поиск ответа «почему не сериализуется», потому что эта сволочь (XmlSerializer) даже не соизволила сказать, чем именно недовольна.
                      0
                      да, действительно. интернет полон описаний проблем с dynamic assembly и разнообразных workaround'ов к ним. в будущем буду иметь в виду. И хотя до сих пор ни разу с этой проблемой не столкнулся знать о ней не помешает. Спасибо :)
                      0
                      XMLSerializer эмиттит код сериализации. А текущий компилятор шарпа не умеет компилировать в памяти, только из сборки на диске. Отсюда тебование на доступ к временной директории, где будет сгенерена сборка с кодом сериализации. Плюс — вторая и все последующие сериализации выполняются моментально, минус — необходимость прав на запись во временную папку.
                        0
                        Речь не об этом, а о том, что XMLSerializer'у нужно, чтобы сборка, содержащая сериализуемый тип, лежала на диске.
                          +1
                          Я уже как-то делал подобное, только что перепроверил. Ненужно.

                          
                          var asm = Assembly.Load(File.ReadAllBytes(@"C:\..\AnotherLib.dll"));
                          var t = asm.GetTypes().First(x => x.Name == "Teacher");
                          var serializer = new XmlSerializer(t);
                          serializer.Serialize(Console.Out, Activator.CreateInstance(t));
                          

                          Этот код отработал без проблем.
                            0
                            Deserialize не работает.
                              0
                              На собранном суррогатном примере всё работает. В «настоящем», в случае динамической загрузки — нет, в случае статической — всё ок. Надо копать, что влияет.
                                0
                                У меня связка Serialize/Deserialize работает. Правда у меня нет проблем с правами. Все в FullTrust режиме. Я сколняюсь что у вас проблемы с правами.

                                По ходу, наверное уже стоит пользовать DataContractSerializer, он как бы и побыстрее, и не требует временных папок/ассемблей. Правда наверное менее гибкий.
                    0
                    1.         public String Save()
                    2.         {
                    3.             String retVal = String.Empty;
                    4.             MemoryStream ms = new MemoryStream();
                    5.  
                    6.             TextWriter sw = null;
                    7.  
                    8.             try
                    9.             {
                    10.                 XmlSerializer ser = new XmlSerializer(this.GetType());
                    11.  
                    12.                 sw = new StreamWriter(ms, Encoding.UTF8);
                    13.  
                    14.                 ser.Serialize(sw, this);
                    15.             }
                    16.             catch (Exception ex)
                    17.             {
                    18.                 throw ex;
                    19.             }
                    20.             finally
                    21.             {
                    22.                 ms.Seek(0, SeekOrigin.Begin);
                    23.                 StreamReader sr = new StreamReader(ms);
                    24.  
                    25.                 retVal = sr.ReadToEnd();
                    26.                 retVal = retVal.Replace(Environment.NewLine, "");
                    27.  
                    28.                 if (sw != null)
                    29.                     sw.Close();
                    30.             }
                    31.  
                    32.             return retVal;
                    33.         }
                    34.  
                    35.         public static void Load(string objectValue, out D retVal)
                    36.         {
                    37.             byte[] byteArray = Encoding.UTF8.GetBytes(objectValue);
                    38.             MemoryStream ms = new MemoryStream(byteArray);
                    39.  
                    40.             XmlTextReader reader = new XmlTextReader(ms);
                    41.  
                    42.             try
                    43.             {
                    44.                 XmlSerializer ser = new XmlSerializer(typeof(D));
                    45.  
                    46.                 reader.Normalization = false;
                    47.                 retVal = (D)ser.Deserialize(reader);
                    48.             }
                    49.             catch (Exception ex)
                    50.             {
                    51.                 throw ex;
                    52.             }
                    53.             finally
                    54.             {
                    55.                 if (reader != null)
                    56.                     reader.Close();
                    57.             }
                    58.         }
                    59.  
                      +1
                      XML сериализация не всегда лучший вариант, как правило самый простой — да, но не всегда подходящий ;)
                      У меня было несколько ситуаций, когда надо было поддерживать две различные структуры xml документа для одного и того же документа как при сериализации, так и при десириализации. Решалось частично через XmlAttributeOvverides, но ясности коду это в итоге не прибавило. Так же иногда это было легче сделать просто «ручным» разбором или сохранением.
                      Так же был случай, когда надо было сериализовывать экземпляр класса из другой либы, в которой не было указано этих атрибутов (ну или просто они были указаны не так, как надо), решалось опять как правило через XmlAttributeOvverides или ручным генерированием XML.
                      Так же лично мне часто приходиться генерировать XML напрямую (например при формировании запроса), т.к. для того, что бы сгенерировать нужный XML на основе данных из каких то твоих классов и отослать его, не стоит делать отдельный класс представляющий отдельный XML (поди RequestClass), что бы потом создавать его на основе уже имеющихся и сериализовать затем.
                      Сериализация и просто работа с XML документами просто немного разные вещи (просто в примерах обычно показывают простую сериализацию), и признаться честно — XmlDocument менее удобный и менее читаемый, чем XDocument\XElement, хотя и у последнего есть свои минусы порой (в частности XmlDocument сохраняет оригинальный xml и всегда можно получить его часть через InnerXml, OuterXml).
                      +1
                      Не помню, чтобы мне когда-либо приходилось вот так руками составлять xml-файл.
                      Необходимо получить такое сообщение:

                      На этом месте для данного xml-файла очень просится обычное действие: создать класс с нужными полями и сериализовать его в xml-файл.
                        0
                        Для сохранения настроек или состояния программы да, сериализация является наилучшим вариантом, но бывает когда обмен данными происходит с помощью XML, при чем структура которого очень сложна, тут как правило без «ручного» составления XML не обойтись.
                          0
                          ну и десериализировать можно не только документ целиком, но и ноду. т.ч. «городить сложный объект» вовсе не обязательно, достаточно нагородить только нужный кусок с которым и работать. а в большой и сложный документ этот кусок вставлять/извлекать с помощью Dom, так должно получать гораздо проще.
                            +1
                            В моем приложении часто на основе моих классов, надо сформировать XML запрос к какому нибудь шлюзу. Работа с конкретным шлюзом реализуется с помощью отдельного класса, наследуемого от общего, т.е. везде этот XML запрос свой (хотя порой это даже не XML).
                            Можно конечно использовать XML сериализацию, но тогда надо для каждого такого класса (допустим Gate1… GateN) делать соответствующие классы запросов\ответов Gate1Request… GateNRequest\Gate1Response… GateNResponse. После этого внутри метода создать этот класс на основе входных параметров метода, и все это только что бы «красиво сериализовать стандартными средствами». Зачем если можно сразу начать формировать XML запрос «ручками»?
                        0
                        Спасибо, как раз искал, подобную статью, только для разбирания чужого документа (который сохраняют как раз вышеопомянутой серриализацией класса), доступа к исходникам нет. А писать тонну кода не хочется. Буду ждать следующую статью, про ток как разбирать документ.
                          0
                          А зачем статью ждать? :) Покурите МСДН на тему методов Element() и Elements() класса XElement, и будет вам счастье :)
                            –1
                            Тут статьи, по сути, писать не о чем.
                            К тому же была уже статья на эту тему:
                            habrahabr.ru/blogs/net/24673/
                            +1
                            Еще в копилочку linqtoxsd.codeplex.com/, типизированный доступ к XML. Вот где красота ;).
                              0
                              Попробуйте ещё xsd.exe из .net sdk (позволяет сгенерировать классы по xml-документу или xsd-схеме, к ним уже можете применить описанный выше XmlSerializer).
                              Здесь небольшой референс по использованию: msdn.microsoft.com/en-us/library/x6c1kb0s(v=VS.100).aspx

                              Можете сохранить себе кучу времени при разборе большого числа сложных xml-документов. Но не забывайте, что кодогенерация — не всегда добро.
                              • UFO just landed and posted this here
                                  0
                                  Вот изумительная статья по нестандартным задачам LINQ to XML: msmvps.com/blogs/martin_honnen/archive/2009/11/27/grouping-with-linq-to-xml.aspx

                                  Only users with full accounts can post comments. Log in, please.