XML-сериализация по особым правилам

284
04 ноября 2017, 11:53

Имеется электронный документооборот. Обмен данными выполняется с помощью XML следующей структуры:

<document>
    <tag1 value="1"/>
    <tag2 value="text"/>
    <tag3 value="01.01.2017 10:20:15"/>
    <tag4 value="2"/>
    <tag5 value="02.02.2017 20:30:45"/>
    <tag6 value="text too"/>
    <tag7 value="3.5"/>
    <outerTag1>
        <innerTag11 value="5"/>
        <innerTag12 value="some text"/>
        <innerTag13 value="some text"/>
        <innerTag14 value="7"/>
        <innerTag15 value="8"/>
        <innerTag16 value="6"/>
        <innerOuterTag11>
            <innerInnerTag111 value="text"/>
            <innerInnerTag112 value="03.03.2017 03:03:03"/>
        </innerOuterTag11>
    </outerTag1>
    <outerTag2>
        <innerTag21 value="text"/>
        <innerTag22 value="text"/>
        <innerTag23 value="text"/>
        <innerTag24 value="text"/>
        <innerTag25 value="text"/>
        <innerTag26 value="text"/>
    </outerTag2>
</document>

Проблема на лицо - требуется куча классов примерно такой структуры:

[XmlRoot(ElementName="tag1")]
public class Tag1
{
    [XmlAttribute(AttributeName="value")]
    public int Value { get; set; }
}

и потом:

[XmlRoot(ElementName="document")]
public class Document
{
    [XmlElement(ElementName="tag1")]
    public Tag1 Tag1 { get; set; }
    [XmlElement(ElementName="tag2")]
    public Tag2 Tag2 { get; set; }
    [XmlElement(ElementName="tag3")]
    public Tag3 Tag3 { get; set; }
    [XmlElement(ElementName="tag4")]
    public Tag4 Tag4 { get; set; }
    [XmlElement(ElementName="tag5")]
    public Tag5 Tag5 { get; set; }
    [XmlElement(ElementName="tag6")]
    public Tag6 Tag6 { get; set; }
    [XmlElement(ElementName="tag7")]
    public Tag7 Tag7 { get; set; }
    [XmlElement(ElementName="outerTag1")]
    public OuterTag1 OuterTag1 { get; set; }
    [XmlElement(ElementName="outerTag2")]
    public OuterTag2 OuterTag2 { get; set; }
}

Хотелось бы вместо этого написать класс со свойствами простых типов:

[XmlRoot(ElementName="document")]
public class Document
{
    [...(ElementName="tag1")]
    public int Tag1 { get; set; }
    [...(ElementName="tag2")]
    public string Tag2 { get; set; }
    [...(ElementName="tag3")]
    public DateTime Tag3 { get; set; }
    [...(ElementName="tag4")]
    public int Tag4 { get; set; }
    [...(ElementName="tag5")]
    public DateTime Tag5 { get; set; }
    [...(ElementName="tag6")]
    public string Tag6 { get; set; }
    [...(ElementName="tag7")]
    public decimal Tag7 { get; set; }
    [XmlElement(ElementName="outerTag1")]
    public OuterTag1 OuterTag1 { get; set; }
    [XmlElement(ElementName="outerTag2")]
    public OuterTag2 OuterTag2 { get; set; }
}

и не плодить кучу мелких классов типа Tag1, Tag2, ...

Можно ли как-то это сделать? В идеале хотелось бы сделать кастомный атрибут MyXmlElement и использовать его вместо XmlElement, но как научить XmlSerializer понимать его и генерировать соответствующую разметку? Или может есть какой-то другой способ?

Answer 1

А давайте сделаем кодогенерацию? Т4 прекрасно подходит. Мы создадим два класса: один со вложенными классами для сериализации, и другой плоский, с которым легко и удобно работать. И конвертирующие функции.

Генерировать будем на основе вот такого XML-документа (я положил его в проект под названием DocumentProto.xml).

<?xml version="1.0" encoding="utf-8" ?>
<document>
  <tag1 type="int"/>
  <tag2 type="string"/>
  <tag3 type="DateTime"/>
  <tag4 type="int"/>
  <tag5 type="DateTime"/>
  <tag6 type="string"/>
  <tag7 type="double"/>
  <outerTag1>
    <innerTag11 type="int"/>
    <innerTag12 type="string"/>
    <innerTag13 type="string"/>
    <innerTag14 type="int"/>
    <innerTag15 type="int"/>
    <innerTag16 type="int"/>
    <innerOuterTag11>
      <innerInnerTag111 type="string"/>
      <innerInnerTag112 type="DateTime"/>
    </innerOuterTag11>
  </outerTag1>
  <outerTag2>
    <innerTag21 type="string"/>
    <innerTag22 type="string"/>
    <innerTag23 type="string"/>
    <innerTag24 type="string"/>
    <innerTag25 type="string"/>
    <innerTag26 type="string"/>
  </outerTag2>
</document>

Кладём в проект новый файл типа T4 через Add New Item → Text Template (не Runtime TextTemplate!). Я назвал его Document.tt.

В первой строке меняем hostspecific="false" на true. Добавляем нужные сборки:

<#@ assembly name="System.Xml" #>
<#@ assembly name="System.Xml.Linq" #>

и

<#@ import namespace="System.Xml.Linq" #>

меняем output extension на ".cs".

Дальше дело техники: нам нужно распарсить XML. Открываем документ, читаем его в память:

<#
    var xmlpath = Host.ResolvePath("DocumentProto.xml");
    XDocument xd = XDocument.Load(xmlpath);
#>

Создаём шаблон файла:

// Generated code! Do not edit!
using System;
using System.Xml.Serialization;
namespace CodegenTest
{
    // будем добавлять тут
}

Теперь, генерация набора классов для сериализации. Пишем (предварительно отладив это на тестовом приложении командной строки) в конце tt-файла:

<#+
    void GenerateNestedClasses(XElement element)
    {
        var childClasses = new Queue<XElement>();
        string className = element.Name.LocalName;
        string capitalizedClassName = char.ToUpper(className[0]) + className.Substring(1);
        WriteLine($"[XmlRoot(ElementName=\"{className}\")]");
        WriteLine($"class {capitalizedClassName}");
        WriteLine("{");
        foreach (var sub in element.Elements())
        {
            string type;
            string name = sub.Name.LocalName;
            string capitalizedName = char.ToUpper(name[0]) + name.Substring(1);
            if (!sub.HasAttributes) // nested class
            {
                type = capitalizedName;
                childClasses.Enqueue(sub);
            }
            else
            {
                type = (string)sub.Attribute("type");
            }
            WriteLine($"    [XmlElement(ElementName=\"{name}\")]");
            WriteLine($"    public {type} {capitalizedName} {{ get; set; }}");
        }
        WriteLine("}");
        WriteLine("");
        foreach (var child in childClasses)
            GenerateNestedClasses(child);
    }
#>

(Внутри тегов <#+ #> располагаются дополнительные методы для генерации.)

Пользуемся:

    namespace Serialization
    {
<#
    PushIndent("        ");
    GenerateNestedClasses(xd.Root);
    ClearIndent();
#>
    }

Получаем в Document.cs:

namespace Serialization
{
    [XmlRoot(ElementName="document")]
    class Document
    {
        [XmlElement(ElementName="tag1")]
        public int Tag1 { get; set; }
        [XmlElement(ElementName="tag2")]
        public string Tag2 { get; set; }
        [XmlElement(ElementName="tag3")]
        public DateTime Tag3 { get; set; }
        [XmlElement(ElementName="tag4")]
        public int Tag4 { get; set; }
        [XmlElement(ElementName="tag5")]
        public DateTime Tag5 { get; set; }
        [XmlElement(ElementName="tag6")]
        public string Tag6 { get; set; }
        [XmlElement(ElementName="tag7")]
        public double Tag7 { get; set; }
        [XmlElement(ElementName="outerTag1")]
        public OuterTag1 OuterTag1 { get; set; }
        [XmlElement(ElementName="outerTag2")]
        public OuterTag2 OuterTag2 { get; set; }
    }
    [XmlRoot(ElementName="outerTag1")]
    class OuterTag1
    {
        [XmlElement(ElementName="innerTag11")]
        public int InnerTag11 { get; set; }
        [XmlElement(ElementName="innerTag12")]
        public string InnerTag12 { get; set; }
        [XmlElement(ElementName="innerTag13")]
        public string InnerTag13 { get; set; }
        [XmlElement(ElementName="innerTag14")]
        public int InnerTag14 { get; set; }
        [XmlElement(ElementName="innerTag15")]
        public int InnerTag15 { get; set; }
        [XmlElement(ElementName="innerTag16")]
        public int InnerTag16 { get; set; }
        [XmlElement(ElementName="innerOuterTag11")]
        public InnerOuterTag11 InnerOuterTag11 { get; set; }
    }
    [XmlRoot(ElementName="innerOuterTag11")]
    class InnerOuterTag11
    {
        [XmlElement(ElementName="innerInnerTag111")]
        public string InnerInnerTag111 { get; set; }
        [XmlElement(ElementName="innerInnerTag112")]
        public DateTime InnerInnerTag112 { get; set; }
    }
    [XmlRoot(ElementName="outerTag2")]
    class OuterTag2
    {
        [XmlElement(ElementName="innerTag21")]
        public string InnerTag21 { get; set; }
        [XmlElement(ElementName="innerTag22")]
        public string InnerTag22 { get; set; }
        [XmlElement(ElementName="innerTag23")]
        public string InnerTag23 { get; set; }
        [XmlElement(ElementName="innerTag24")]
        public string InnerTag24 { get; set; }
        [XmlElement(ElementName="innerTag25")]
        public string InnerTag25 { get; set; }
        [XmlElement(ElementName="innerTag26")]
        public string InnerTag26 { get; set; }
    }
}

Добавляем ещё в конец файла генерацию свойств «плоского класса»:

void GenerateFlatClassProps(XElement element)
{
    foreach (var sub in element.Elements())
    {
        if (!sub.HasAttributes) // nested
            GenerateFlatClassProps(sub);
        else
        {
            var type = (string)sub.Attribute("type");
            string name = sub.Name.LocalName;
            string capitalizedName = char.ToUpper(name[0]) + name.Substring(1);
            WriteLine($"public {type} {capitalizedName} {{ get; set; }}");
        }
    }
}

и метода чтения свойств в плоский класс:

void GenerateFlatteningBody(XElement element, string path)
{
    string name = element.Name.LocalName;
    string capitalizedName = char.ToUpper(name[0]) + name.Substring(1);
    foreach (var sub in element.Elements())
    {
        string subName = sub.Name.LocalName;
        string capitalizedSubName = char.ToUpper(subName[0]) + subName.Substring(1);
        if (!sub.HasAttributes) // nested
            GenerateFlatteningBody(sub, path + "." + capitalizedSubName);
        else
            WriteLine($"this.{capitalizedSubName} = that{path}.{capitalizedSubName};");
    }
}

Пользуемся ими наверху:

    public class Document
    {
<#
    PushIndent("        ");
    GenerateFlatClassProps(xd.Root);
    ClearIndent();
#>
        private void AssignFromSerialized(Serialization.Document that)
        {
<#
    PushIndent("            ");
    GenerateFlatteningBody(xd.Root, "");
    ClearIndent();
#>
        }
        internal static Document FromSerialized(Serialization.Document sdoc)
        {
            var doc = new Document();
            doc.AssignFromSerialized(sdoc);
            return doc;
        }
    }

Получаем готовый сгенерированный набор классов для использования:

namespace CodegenTest
{
    public class Document
    {
        public int Tag1 { get; set; }
        public string Tag2 { get; set; }
        public DateTime Tag3 { get; set; }
        public int Tag4 { get; set; }
        public DateTime Tag5 { get; set; }
        // ...
        private void AssignFromSerialized(Serialization.Document that)
        {
            this.Tag1 = that.Tag1;
            this.Tag2 = that.Tag2;
            // ...
            this.InnerTag11 = that.OuterTag1.InnerTag11;
            this.InnerTag12 = that.OuterTag1.InnerTag12;
            this.InnerTag13 = that.OuterTag1.InnerTag13;
            this.InnerTag14 = that.OuterTag1.InnerTag14;
            this.InnerTag15 = that.OuterTag1.InnerTag15;
            this.InnerTag16 = that.OuterTag1.InnerTag16;
            this.InnerInnerTag111 = that.OuterTag1.InnerOuterTag11.InnerInnerTag111;
            // ...
        }
        internal static Document FromSerialized(Serialization.Document sdoc)
        {
            var doc = new Document();
            doc.AssignFromSerialized(sdoc);
            return doc;
        }
    }
    namespace Serialization
    {
        [XmlRoot(ElementName="document")]
        class Document
        {
            [XmlElement(ElementName="tag1")]
            public int Tag1 { get; set; }
            // ...
            [XmlElement(ElementName="outerTag1")]
            public OuterTag1 OuterTag1 { get; set; }
            [XmlElement(ElementName="outerTag2")]
            public OuterTag2 OuterTag2 { get; set; }
        }
        [XmlRoot(ElementName="outerTag1")]
        class OuterTag1
        {
            [XmlElement(ElementName="innerTag11")]
            public int InnerTag11 { get; set; }
            [XmlElement(ElementName="innerTag12")]
            public string InnerTag12 { get; set; }
            // ...
        }
        // ...
    }
}

На всякий случай, полный код tt-шаблона и сгенерированного результата: https://gist.github.com/vladd/7f25e0ceb625372bffdbf9b455452ae1

READ ALSO
Ошибка при декодировании BitmapImage

Ошибка при декодировании BitmapImage

Кодирую BitmapImage (в формате gif) в массив байт, далее передаю этот массив байт по сети, массивы байтов при отправке и пришествию совпадают, далее...

466
Залочить форму на время работы события

Залочить форму на время работы события

Есть на форме кнопка сохранитьКогда я её нажимаю, есть необходимость залочить форму, и запустить например колесо прокрутки с текстом на подобии...

280
Как добавить индекс при Entity Model First?

Как добавить индекс при Entity Model First?

Как добавить индекс по 2м столбцам в Модель таблицы базы данных при Entity подходе Model-first ?

307
Правильно ли выполнены операции с XOR и OR

Правильно ли выполнены операции с XOR и OR

Есть массив byte [] regs, в котором содержатся данные, к которым обращаются с помощью reg1 и reg2 (предварительно получают для них числовые данные с помощью...

258