Страницы

Поиск по вопросам

пятница, 14 февраля 2020 г.

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

#c_sharp #xml #сериализация


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


    
    
    
    
    
    
    
    
        
        
        
        
        
        
        
            
            
        
    
    
        
        
        
        
        
        
    



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

[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 понимать его и генерировать
соответствующую разметку? Или может есть какой-то другой способ?
    


Ответы

Ответ 1



А давайте сделаем кодогенерацию? Т4 прекрасно подходит. Мы создадим два класса: один со вложенными классами для сериализации, и другой плоский, с которым легко и удобно работать. И конвертирующие функции. Генерировать будем на основе вот такого XML-документа (я положил его в проект под названием DocumentProto.xml). Кладём в проект новый файл типа 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(); 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; } // ... } // ... } } Обновление: исправил GenerateNestedClasses на такое: void GenerateNestedClasses(XElement element) { var childClasses = new Queue(); var leafClasses = new Queue(); 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 name = sub.Name.LocalName; string capitalizedName = char.ToUpper(name[0]) + name.Substring(1); if (!sub.HasAttributes) // nested class childClasses.Enqueue(sub); else leafClasses.Enqueue(sub); WriteLine($" [XmlElement(ElementName=\"{name}\")]"); WriteLine($" public {capitalizedName} {capitalizedName} {{ get; set; }}"); } WriteLine("}"); WriteLine(""); foreach (var leaf in leafClasses) GenerateLeafClass(leaf); foreach (var child in childClasses) GenerateNestedClasses(child); } void GenerateLeafClass(XElement element) { string className = element.Name.LocalName; string capitalizedClassName = char.ToUpper(className[0]) + className.Substring(1); string type = (string)element.Attribute("type"); WriteLine($"class {capitalizedClassName}"); WriteLine("{"); WriteLine($" [XmlAttribute]"); WriteLine($" public {type} Value {{ get; set; }}"); WriteLine("}"); WriteLine(""); } и в GenerateFlatteningBody последнюю строчку на WriteLine($"this.{capitalizedSubName} = that{path}.{capitalizedSubName}.Value;"); Получились промежуточные классы вида class Tag1 { [XmlAttribute] public int Value { get; set; } } На всякий случай, полный код tt-шаблона и сгенерированного результата: https://gist.github.com/vladd/7f25e0ceb625372bffdbf9b455452ae1

Ответ 2



Отказался от реализации интерфейса IXmlSerializable - решение получалось очень громоздким и не красивым, к тому же нужно учесть много всевозможных нюансов, которые учтены в штатной работе сериализатора. В итоге написал простой класс: public class Tag { [XmlAttribute(AttributeName = "value")] public T Value { get; set; } public override string ToString() => Value.ToString(); public static implicit operator Tag(T value) => new Tag { Value = value }; public static implicit operator T(Tag tag) => tag.Value; } Это позволило выбросить кучу мелких классов Tag1, Tag2 и т.д. Сам документ принял вид: [XmlRoot(ElementName="document")] public class Document { [XmlElement(ElementName="tag1")] public Tag Tag1 { get; set; } [XmlElement(ElementName="tag2")] public Tag Tag2 { get; set; } [XmlElement(ElementName="tag3")] public Tag Tag3 { get; set; } [XmlElement(ElementName="tag4")] public Tag Tag4 { get; set; } [XmlElement(ElementName="tag5")] public Tag Tag5 { get; set; } [XmlElement(ElementName="tag6")] public Tag Tag6 { get; set; } [XmlElement(ElementName="tag7")] public Tag Tag7 { get; set; } [XmlElement(ElementName="outerTag1")] public OuterTag1 OuterTag1 { get; set; } [XmlElement(ElementName="outerTag2")] public OuterTag2 OuterTag2 { get; set; } } Ну и благодаря операторам для неявного приведения типов документ создается так же просто: var doc = new Document { Tag1 = 1, Tag2 = "text", ... };

Комментариев нет:

Отправить комментарий