Интерфейс LINQ to XML API

77

После нескольких лет существования с W3C DOM XML API от Microsoft, специалистами этой корпорации было выделено несколько ключевых областей, в которых проявляется неудобство, сложность или слабость исходного API-интерфейса. Чтобы побороть эти проблемы, были выделены следующие точки приложения усилий:

Каждая из этих проблемных областей представляла собой препятствие в работе с XML. Они не только вызывали "разбухание" кода работы с XML, часто непреднамеренно затеняя этот код. Их необходимо было преодолеть, чтобы обеспечить гладкую работу LINQ с XML. Например, когда нужно использовать проекцию для возврата XML из запроса LINQ, проблемой была невозможность создания экземпляра элемента операцией new. Такое ограничение существующего API-интерфейса XML должно было быть преодолено, чтобы обеспечить практическую работу LINQ с XML. Давайте рассмотрим каждую из перечисленных проблемных областей, а также способ их разрешения в новом API-интерфейсе LINQ to XML.

Преимущества LINQ to XML

Конструирование деревьев XML было упрощено с помощью функционального конструирования

Код создания дерева XML крайне громоздок, при использовании стандартного API-интерфейса для работы с XML. После создания документа XML понадобится создать некоторого типа узел XML, такой как элемент, установить его значение и добавить к родительскому элементу. Однако каждый из этих трех шагов должен быть выполнен отдельно с использованием W3C DOM API. Это приводит к неясной схеме и объемному коду. В этом API-интерфейсе не предусмотрена поддержка создания элемента или любого типа узла в определенном месте дерева XML по отношению к его родителю и его инициализации за одну операцию.

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

XMLObject оbj = 
       new XMLObject (ObjectName, 
                     XMLObject1, 
                     XMLObject2,
                     ...
                     XMLObjectN);

Приведенный фрагмент является просто псевдокодом, предназначенным для иллюстрации шаблона. Ни один из классов, присутствующих в нем, не существует на самом деле; они лишь представляют некоторый концептуальный абстрактный класс XML. При добавлении к элементу, реализованному классом XElement, атрибута XML, который реализован классом XAttribute из LINQ to XML, этот атрибут становится атрибутом данного элемента. Например, если в предыдущем псевдокоде XMLObject1 добавляется к вновь созданному XMLObject по имени оbj, и оbj является XElement, a XMLObject1 — XAttribute, то XMLObject1 становится атрибутом XElement по имени оbj.

Если XElement добавляется к XElement, то добавляемый XElement становится дочерним элементом того, к которому он добавлен. Поэтому, например, если XMLObject1 и оbj являются элементами, то XMLObject1 становится дочерним элементом оbj.

При создании экземпляра объекта XMLObject, как показано в приведенном псевдокоде, можно задавать его содержимое, указывая от 1 до N объектов XMLObject. Как будет показано в разделе "Создание текста с помощью XText" в статье "Создание XML", можно даже указывать его содержимое с помощью строки, поскольку строка автоматически преобразуется в XMLObject.

Все это совершенно логично и составляет суть функционального конструирования. Ниже приведен пример:

using System.Xml.Linq;
...
                  
XElement employees =
       new XElement("employee", 
                  new XElement("FirstName","Alex"), 
                  new XElement("LastName","Erohin"));

Console.WriteLine(employees.ToString());

Обратите внимание, что при конструировании элемента по имени employees в качестве его значения передаются два объекта XElement, каждый из которых становится его дочерним элементом. Также обратите внимание, что при конструировании элементов FirstName и LastName вместо указания дочерних объектов, указаны просто текстовые значения элементов. Вот результат работы этого кода:

Функциональное конструирование для создания схемы XML

Обратите внимание, насколько проще инициализировать в коде схему XML. Код LINQ to XML существенно короче, чем при использовании XML DOM. Для примера сравните следующие два листинга кода, выполняющих одинаковую работу, но использующие два разных интерфейса:

// Используем старый API-интерфейс XML DOM

            // Объявляю некоторые переменные, которые будут использоваться повторно.
            XmlElement xmlEmployee;//Employee
            XmlAttribute xmlEmployeeType; //EmployeeType
            XmlElement xmlFirstName;
            XmlElement xmlLastName;

            // Сначала я должен построить документ XML.
            XmlDocument xmlDoc = new XmlDocument();

            // Создаю корневой элемент и добавляю его в документ.
            XmlElement xmlEmployees = xmlDoc.CreateElement("Employees");
            xmlDoc.AppendChild(xmlEmployees);

            // Создаю список участников
            xmlEmployee = xmlDoc.CreateElement("Employee");
            xmlEmployeeType = xmlDoc.CreateAttribute("type");
            xmlEmployeeType.InnerText = "Programmer";
            xmlEmployee.Attributes.Append(xmlEmployeeType);
            xmlFirstName = xmlDoc.CreateElement("FirstName");
            xmlFirstName.InnerText = "Alex";
            xmlEmployee.AppendChild(xmlFirstName);
            xmlLastName = xmlDoc.CreateElement("LastName");
            xmlLastName.InnerText = "Erohin";
            xmlEmployee.AppendChild(xmlLastName);
            xmlEmployees.AppendChild(xmlEmployee);

            // Создаю еще одного участника
            xmlEmployee = xmlDoc.CreateElement("Employee");
            xmlEmployeeType = xmlDoc.CreateAttribute("type");
            xmlEmployeeType.InnerText = "Editor";
            xmlEmployee.Attributes.Append(xmlEmployeeType);
            xmlFirstName = xmlDoc.CreateElement("FirstName");
            xmlFirstName.InnerText = "Elena";
            xmlEmployee.AppendChild(xmlFirstName);
            xmlLastName = xmlDoc.CreateElement("LastName");
            xmlLastName.InnerText = "Volkova";
            xmlEmployee.AppendChild(xmlLastName);
            xmlEmployees.AppendChild(xmlEmployee);

            // Выводим на консоль
            xmlDoc.Save(Console.Out);
Создание XML-документа с помощью устаревшего интерфейса XML DOM

Писать, разбирать и сопровождать такой код — сущий кошмар. Он чрезвычайно многословен. Просто взглянув на него, невозможно получить никакого представления о том, как должна выглядеть результирующая структура XML.

Давайте теперь создадим то же самое дерево XML используя LINQ to XML:

// Используем новый API-интерфейс LINQ to XML
            XElement xEmployees =
                new XElement("Employees",
                    new XElement("Employee",
                        new XAttribute("type", "Programmer"),
                        new XElement("FirstName", "Alex"),
                        new XElement("LastName", "Erohin")),
                    new XElement("Employee",
                        new XAttribute("type", "Editor"),
                        new XElement("FirstName", "Elena"),
                        new XElement("LastName", "Volkova")));

            Console.WriteLine(xEmployees);

Результат получается аналогичным предыдущему, за исключением того, что мы не создаем новый XML-документ, поэтому в начале нет строки с кодировкой и версией XML:

Использование Linq to XML

Как видите, здесь пришлось создать намного меньше кода, что впоследствии упростит сопровождение. К тому же схему намного легче понять, просто прочитав этот код.

Центральная роль элемента вместо документа

В исходном W3C DOM API нельзя просто создать XML-элемент — XmlElement; нужно было иметь XML-документ — XmlDocument, из которого создавать его. Попытка создать экземпляр XmlElement вроде следующего:

XmlElement xmlEmployee = new XmlElement ("Employee");

привела бы к ошибке компиляции:

'System.Xml.XmlElement.XmlElement (string, string, string, System.Xml.XmlDocument)'
is inaccessible due to its protection level

'System.Xml.XmlElement.XmlElement (string, string, string, System.Xml.XmlDocument)'
не доступен из-за своего уровня защиты

С помощью W3C DOM API создавать XmlElement можно только вызовом метода XmlDocument по имени CreateElement, как показано ниже:

XmlDocument xmlDoc = new XmlDocument();
XmlElement xmlEmployee = xmlDoc.CreateElement ("Employee");

Этот код компилируется успешно. Но часто обязательное создание документа XML причиняет неудобства, особенно когда нужно просто создать элемент XML. Новый LINQ-оснащенный XML API позволяет создавать экземпляр элемента без создания документа XML:

XElement xEmployee = new XElement("Employee");

XML-элементы - не единственный тип узлов, подчиняющихся этому ограничению W3C DOM. Атрибуты, комментарии, разделы CData, инструкции обработки и ссылки на сущности — все это должно было создаваться из документа XML. К счастью LINQ to XML дает возможность непосредственно создавать каждый из этих объектов "на лету".

Конечно, ничто не мешает создавать XML-документы с помощью нового API-интерфейса. Например:

XDocument xDocument =
                new XDocument(
                    new XElement("Employees",
                        new XElement("Employee",
                            new XAttribute("type", "Programmer"),
                            new XElement("FirstName", "Alex"),
                            new XElement("LastName", "Erohin")),
                         new XElement("Employee",
                            new XAttribute("type", "Editor"),
                            new XElement("FirstName", "Elena"),
                            new XElement("LastName", "Volkova"))));

            Console.WriteLine(xDocument.ToString());

Разметка XML, произведенная предыдущим кодом, идентична XML-разметке, которая была создана ранее.

В дополнение к созданию деревьев XML без документов XML можно делать большинство тех же вещей, которые требуют документов, вроде чтения XML-разметки из файла и сохранение ее в файл.

Имена, пространства имен и префиксы

Чтобы исключить путаницу, связанную с именами, пространствами имен и префиксами пространств имен, последние изъяты из API-интерфейса. С помощью LINQ to XML префиксы пространств имен разворачиваются на вводе и возвращаются в выводе. Внутри они не существуют!

Пространство имен используется в XML с целью уникальной идентификации схемы XML для некоторой части дерева XML. В качестве пространств имен XML используются URI, потому что они уже уникальны для каждой организации. В некоторых примерах кода дерево XML создается следующим образом:

<Employees>
  <Employee type="Programmer">
    <FirstName>Alex</FirstName>
    <LastName>Erohin</LastName>
  </Employee>
  <Employee type="Editor">
    <FirstName>Elena</FirstName>
    <LastName>Volkova</LastName>
  </Employee>
</Employees>

Любой код, обрабатывающий эти данные XML, будет написан в предположении, что узел Employees может содержать множество узлов Employee, каждый из которых имеет атрибут type и узлы FirstName и LastName. Но что, если коду также придется обрабатывать XML из другого источника, в котором окажется узел Employees, но с отличающейся внутренней схемой?

Пространство имен известит код о том, как должна выглядеть схема, позволяя ему соответствующим образом обработать XML. В XML каждый элемент должен иметь имя. Когда элемент создается, если его имя указано в конструкторе, оно неявно преобразуется из string в объект XName. Объект XName состоит из пространства имен — объекта XNamespace — и своего локального имени, того, которое было указано. Поэтому, например, можно создать элемент Employees следующим образом:

XElement xEmployees = new XElement("Employees");

При создании элемента объект XName получает пустое пространство имен и локальное имя Employees. Если во время отладки этой строки кода просмотреть переменную xEmployees в окне слежения, то увидите, что ее член Name установлен в {Employees}. Развернув член Name, вы увидите, что он содержит член LocalName, установленный в Employees, и член по имени Namespace, который будет пустым — {}. В данном случае пространство имен отсутствует.

Чтобы указать пространство имен, понадобится просто создать объект XNamespace и предварить им локальное имя следующим образом:

XNamespace nameSpace = "http://www.professorweb.ru";
XElement xEmployees = new XElement(nameSpace + "Employees");

После этого при просмотре элемента xEmployees в окне слежения отладчика можно увидеть, что Name установлено в http://www.professorweb.ru/Employees. Разворачивание члена Name покажет, что LocalName будет по-прежнему Employees, а член Namespace установлен в http://www.professorweb.ru.

Для того чтобы указать пространство имен, не обязательно использовать объект XNamespace. Пространство имен можно задать в виде жестко закодированного строкового литерала:

XElement xEmployees = new XElement("{http://www.professorweb.ru}" + "Employees");

Обратите внимание, что пространство имен заключено в фигурные скобки. Это указывает конструктору XElement на тот факт, что данная часть означает пространство имен. Если вновь просмотреть член Name объекта Employees в окне слежения, можно увидеть, что член Name и встроенные в него члены LocalName и Namespace установлены идентично значениям, установленным в предыдущем примере, где для создания элемента применялся объект XNamespace.

Имейте в виду, что при установке пространства имен простого указания URI компании или домена организации может быть недостаточно для того, чтобы гарантировать уникальность. Это гарантирует лишь отсутствие коллизий с любой другой существующей организацией, которая соблюдает установленные соглашения названий пространств имен. Однако внутри организации могут случиться коллизии с любым другим подразделением, если в пространстве имен не будет указано ничего помимо URI организации. И здесь весьма пригодится знание организационной структуры предприятия — его подразделений, департаментов и т.д. Лучше всего, если пространство имен будет раскрывать весь путь до определенного уровня, находящегося под вашим контролем.

XNamespace nameSpace = "http://www.professorweb.ru/LINQ";
XDocument xDocument =
                new XDocument(
                    new XElement("Employees",
                        // Добавляем пространство имен с префиксом
                        new XAttribute(XNamespace.Xmlns + "linq", nameSpace),
                        new XElement("Employee",
                            new XAttribute("type", "Programmer"),
                            new XElement("FirstName", "Alex"),
                            new XElement("LastName", "Erohin")),
                        new XElement("Employee",
                            new XAttribute("type", "Editor"),
                            new XElement("FirstName", "Elena"),
                            new XElement("LastName", "Volkova"))));

            Console.WriteLine(xDocument.ToString());

В приведенном коде в качестве префикса пространства имен указывается linq и с помощью объекта XAttribute в схему включается спецификация префикса. Вот вывод этого кода:

Указание префикса пространства имен LINQ to XML

Извлечение значения узла

Если вы ознакомились с примером использования старого интерфейса XML DOM из предыдущего раздела, то наверняка вспомните, что вам приходилось сталкиваться с той же проблемой, которая помешала сразу получить результат, т.е. действительное значение узла. Если нет достаточного опыта работы с кодом XML DOM, то неизбежно возникает ошибка подобного рода, поскольку многие забывают о необходимости выполнения дополнительного шага для получения значения узла.

В LINQ to XML эта проблема очень изящно решена. Метод ToString элемента выводит саму строку XML, а не тип объекта, как это делается в W3C DOM API. Это очень удобно, когда необходим определенный фрагмент XML из некоторой точки дерева, к тому же имеет намного больше смысла, чем вывод типа объекта. Ниже приведен пример:

XElement name = new XElement("Name", "Alex");
Console.WriteLine(name.ToString());

Нажатие <Ctrl+F5> выдаст следующий результат:

Вызов метода ToString на элементе

Уже лучше. Однако можно добиться еще лучшего результата. Разумеется, дочерние узлы включены в вывод, и поскольку метод WriteLine не имеет явной перегрузки, принимающей XElement, он вызывает метод ToString. Что еще более важно — если привести узел к типу данных, к которому может быть преобразовано его значение, то будет выведено само значение. Ниже показан пример:

XElement name = new XElement("Person", 
                new XElement("FirstName","Alex"),
                new XElement("LastName","Erohin"));
                
Console.WriteLine("XML: \n\n" + name +
                "\n\nИзвлекаем значение: " + (string)name);

Вот результат этого кода:

Приведение элемента к типу данных

He правда ли, гладко? Но сколько за это придется платить? Существуют операции приведения для string, int, int?, uint, uint?, long, long?, ulong, ulong?, bool, bool?, float, float?, double, double?, decimal, decimal?, TimeSpan, TimeSpan?, DateTime, DateTime?, GUID и GUID?.

Все выглядит простым и интуитивно понятным. Похоже на то, что применение LINQ to XML вместо W3C DOM API оставит в прошлом ошибки при извлечении значений узлов.

В то время как все эти примеры упрощают получение значения элемента, все они представляют собой случаи приведения элемента к тому типу данных, который изначально имело значение этого элемента. Это не обязательно. Все, что нужно — чтобы значение элемента могло быть преобразовано к указанному типу данных.

К сожалению то, как именно значения преобразуются, не задано, но на самом деле для этой цели используются методы класса System.Xml.XmlConvert. Код из примера ниже демонстрирует, как это происходит при приведении к типу bool:

try
{
        XElement smoker = new XElement("Smoker", "Tue");
        Console.WriteLine(smoker);
        Console.WriteLine((bool)smoker);
}
catch (Exception ex)
{
        Console.WriteLine(ex);
}

Обратите внимание, что в слове "True" специально допущена опечатка, чтобы инициировать исключение, которое покажет, где именно происходило приведение. Повезет ли? Нажмем <Ctrl+F5>, чтобы проверить:

Приведение к bool, с вызовом метода System.Xml.XmlConvert.ToBoolean

Как видите, исключение было сгенерировано в методе System.Xml.XmlConvert.ToBoolean.

Пройди тесты
Лучший чат для C# программистов