Работа с XSD и XML в Directum RX (и не только)

16 0

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

В статье нет ничего сложного, скорее всего вы всё это знаете, она полезна больше форматом мануала. Полезна еще тем, что есть некоторые нюансы, которые не сразу очевидны.

Логика работы с XSD и XML будет реализована в специальной сборке. Для доступа «снаружи» будет реализовано всего 2 публичных метода «Validate» и «Parse». Сборку можно подключить в Directum RX (или в любой другой системе, написанной на .NET).

Задача разобьется на следующие этапы:

  1. Получить / сформировать примеры файлов XSD и XML;
  2. Создать проект сборки;
  3. На основе XSD сгенерировать классы в проекте;
  4. В проекте создать ресурс и занести туда содержимое XSD файла;
  5. Реализовать метод валидации XML по XSD схеме;
  6. Реализовать метод парсинга (десериализации) XML;
  7. Собрать библиотеку, подключить ее в Directum RX и использовать.

Примеры файлов, на которых будем решать задачу:

Создадим проект сборки.

Сборку назовем, например, XmlClassLibrary. Целевая платформа .NET Standard. Основной класс переименуем в XmlClass.

Сгенерируем класс на основе XSD и подключим его к проекту.

Для этого выполним команду:

"C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools\xsd.exe" UTDIcoXsd.xsd /classes

В итоге сформируется файл UTDIcoXsd.cs (при условии что исходный файл называется UTDIcoXsd.xsd) с классом UTD_Document. Данный файл необходимо добавить в проект XmlClassLibrary.

Создадим в проекте ресурс и занесем туда содержимое XSD файла.

Назовем файл ресурса XsdResource, а сам ресурс UTDIcoXsd.

Реализуем метод валидации XML по XSD схеме.

Метод на вход будет получать поток с XML файлом. Содержимое XSD получим из ресурса XsdResource.UTDIcoXsd. В результате, если валидация пройдена, вернется пустая строка, либо строка с сообщением об ошибке валидации.

public static string Validate(Stream stream)
{
  using (var reader = XmlReader.Create(new StringReader(XsdResource.UTDIcoXsd)))
  {
    var schemas = new XmlSchemaSet();
    schemas.Add(XmlSchema.Read(reader, null));
    schemas.Compile();

    var xml = XDocument.Load(stream);
    var errors = string.Empty;
    xml.Validate(schemas, (o, e) =>
    {
      errors = e.Message;
    });

    return errors;
  }
}

Текущий пример довольно простой, XSD схема не ссылается на другие схемы. Но мы сразу предусмотрели занесение прочитанной XSD в набор.

В случае, когда в XSD есть ссылки на другие схемы, потребуется разрешение данных ссылок. Для этого потребуется создать своего наследника класса XmlUrlResolver.

class XmlResolver : XmlUrlResolver
{
  internal const string BaseUri = "schema://";

  public override object GetEntity(Uri absoluteUri, string role, Type ofObjectToReturn)
  {
    if (absoluteUri.Scheme == "schema")
      return new MemoryStream(Encoding.UTF8.GetBytes(XsdResource.UTDIcoXsd));

    return base.GetEntity(absoluteUri, role, ofObjectToReturn);
  }
}

И адаптировать чтение XSD.

using (var reader = XmlReader.Create(new StringReader(XsdResource.UTDIcoXsd), new XmlReaderSettings(), XmlResolver.BaseUri))
{
  var schemas = new XmlSchemaSet() { XmlResolver = new XmlResolver() };
  ...

Добавим консольную утилиту для отладки.

Я решил собрать ее на .NET Core 3.1.

static void Main(string[] args)
{
  Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

  using (FileStream fstream = File.OpenRead(@"..\..\..\xml\813_DIS0244421_20210622_134415_nok.xml"))
  {
    var result = XmlClassLibrary.XmlClass.Validate(fstream);
    if (!string.IsNullOrEmpty(result))
      Console.WriteLine(result);
  }
  Console.ReadLine();
}

Код практически идентичен тому, что было бы в .NET Framework, за одним исключением. Строка Encoding.RegisterProvider(CodePagesEncodingProvider.Instance) дает команду утилите загрузить кодировки, поддерживаемые .NET Framework. Так как, в отличии от обычного .NET Framework, в котором изначально поддерживается большое количество кодировок, в .NET Core по умолчанию доступны лишь несколько базовых.

Результатом валидации XML из данной статьи будет сообщение:

The element 'Line' has incomplete content. List of possible elements expected: 'LineId'.

Реализуем метод десериализации XML.

Сначала добавим в проект класс XmlParser, содержащий кодировку по умолчанию, приватные методы, получающие сериализатор по типу и метод расширения для десериализации массива байт.

public static class XmlParser
{
  private static readonly Encoding Encoding = Encoding.GetEncoding(1251);

  internal static T FromXml<T>(this byte[] data)
  {
    return FromXml<T>(data, typeof(T));
  }

  private static T FromXml<T>(this byte[] data, Type type)
  {
    try
    {
      using (var reader = new StreamReader(new MemoryStream(data), Encoding))
      {
        return (T)GetXmlSerializer(type).Deserialize(reader);
      }
    }
    catch (Exception)
    {
      return default(T);
    }
  }

  private static XmlSerializer GetXmlSerializer(Type type)
  {
    return new XmlSerializer(type);
  }
}

Добавим публичный метод в класс XmlClass, который на вход будет получать массив байт с XML файлом и возвращать объект UTD_Document, сгенерированный из XSD.

public static UTD_Document Parse(byte[] xml)
{
  return xml.FromXml<UTD_Document>();
}

Адаптируем консольную утилиту для отладки метода.

static void Main(string[] args)
{
  Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

  using (FileStream fstream = File.OpenRead(@"..\..\..\xml\813_DIS0244421_20210622_134415_nok.xml"))
  {
    var result = XmlClassLibrary.XmlClass.Validate(fstream);
    if (string.IsNullOrEmpty(result))
    {
      fstream.Seek(0, SeekOrigin.Begin);
      byte[] array = new byte[fstream.Length];
      fstream.Read(array, 0, array.Length);
      var objFromXML = XmlClassLibrary.XmlClass.Parse(array);
    }
    else
      Console.WriteLine(result);
  }

  Console.ReadLine();
}

Тут стоит обратить внимание на строку fstream.Seek(0, SeekOrigin.Begin). После валидации поток является «прочитанным», соответственно указатель стоит в конце потока. Для того, чтобы его вернуть в начало и используется эта строчка.

Как это выглядит в Directum RX.

Практически также, как и в консольной утилите отладки.

public static void ReadUTDXmlFile(string pathFile, string fileName)
{
  try
  {
    using (var fstream = System.IO.File.OpenRead(pathFile))
    {
      var validateResult = XmlClassLibrary.XmlClass.Validate(fstream);
      if (string.IsNullOrEmpty(validateResult))
      {
        fstream.Seek(0, SeekOrigin.Begin);
        byte[] bytes = new byte[fstream.Length];
        fstream.Read(bytes, 0, bytes.Length);
        var document = XmlClassLibrary.XmlClass.Parse(bytes);
        // ... Какая-то работа с полученным объектом.
      }
      else
        Logger.Error(validateResult);
    }
  }
  catch (Exception ex)
  {
    Logger.Error(ex.Message, ex);
  }
}

В случае с Directum RX важно отметить - специфичные для .NET Framework сборки должны лежать в папке net, для .NET Core - в папке core, для .Net Standard - можно не раскидывать по папкам, а подключать как обычно.

16
Авторизуйтесь, чтобы оценить материал.
2
Пока комментариев нет.

Авторизуйтесь, чтобы написать комментарий