Серіалізація XML та спадкові типи


84

Виходячи з мого попереднього запитання, я працював над тим, щоб моя об’єктна модель серіалізувалася до XML. Але я зараз зіткнувся з проблемою (несподіваний сюрприз!).

Проблема у мене полягає в тому, що у мене є колекція абстрактного базового класу, яка заповнюється конкретними похідними типами.

Я вважав, що було б добре просто додати атрибути XML до всіх класів, що беруть участь, і все було б персиково. На жаль, це не так!

Отже, я трохи скопався в Google і тепер розумію, чому це не працює. У тому, що XmlSerializerнасправді робить якусь розумну рефлексію, щоб серіалізувати об'єкти до / з XML, і оскільки він базується на абстрактному типі, він не може зрозуміти, з яким бісом він говорить . Прекрасно.

Я зіткнувся з цією сторінкою на CodeProject, яка, схоже, може дуже допомогти (поки не читати / споживати повністю), але я думав, що хотів би також перенести цю проблему в таблицю StackOverflow, щоб перевірити, чи є у вас акуратні хаки / хитрощі, щоб це можна було зробити найшвидшим / найлегшим способом.

Одне, що я також повинен додати, це те, що Я НЕ хочу спускатися по XmlIncludeмаршруту. З ним просто занадто багато зв’язку, і ця область системи переживає серйозні розробки, тому це був би справжній головний біль для технічного обслуговування!


1
Було б корисно побачити деякі відповідні фрагменти коду, витягнуті з класів, які ви намагаєтесь серіалізувати.
Рекс М

Mate: Я відкрив свою роботу знову, тому що відчуваю, що інші люди могли б знайти це корисним, але
сміливо

Трохи збентежений цим, оскільки так довго в цій темі нічого не було?
Роб Купер

Відповіді:


54

Проблема вирішена!

Добре, отже, нарешті я туди потрапив (правда, з великою допомогою звідси !).

Отже, підсумуйте:

Цілі:

  • Я не хотів їхати маршрутом XmlInclude через головний біль головного болю.
  • Як тільки рішення було знайдено, я хотів, щоб його швидко застосувати в інших додатках.
  • Можуть використовуватися колекції абстрактних типів, а також окремі властивості абстрактних.
  • Я не дуже хотів морочитися з тим, щоб робити конкретні речі на конкретних заняттях.

Визначені проблеми / зауваження:

  • XmlSerializer робить досить прохолодне відображення, але воно дуже обмежене, коли йдеться про абстрактні типи (тобто він буде працювати лише з екземплярами самого абстрактного типу, а не з підкласами).
  • Декоратори атрибутів Xml визначають, як XmlSerializer обробляє властивості, які знаходить. Також можна вказати фізичний тип, але це створює тісне зв’язок між класом та серіалізатором (не добре).
  • Ми можемо реалізувати наш власний XmlSerializer, створивши клас, який реалізує IXmlSerializable .

Рішення

Я створив загальний клас, в якому ви вказуєте загальний тип як абстрактний тип, з яким будете працювати. Це дає класу можливість "перекладати" між абстрактним типом і конкретним типом, оскільки ми можемо жорстко кодувати кастинг (тобто ми можемо отримати більше інформації, ніж може XmlSerializer).

Потім я реалізував інтерфейс IXmlSerializable , це досить прямо, але при серіалізації нам потрібно переконатись, що ми записуємо тип конкретного класу в XML, щоб ми могли повернути його назад, коли десеріалізуємо. Важливо також зазначити, що він повинен бути повністю кваліфікованим, оскільки склади, в яких знаходяться два класи, можуть відрізнятися. Звичайно, тут є невелика перевірка типу та інше.

Оскільки XmlSerializer не може транслювати, нам потрібно надати код для цього, тому неявний оператор потім перевантажується (я навіть ніколи не знав, що ви можете це зробити!).

Код для AbstractXmlSerializer такий:

using System;
using System.Collections.Generic;
using System.Text;
using System.Xml.Serialization;

namespace Utility.Xml
{
    public class AbstractXmlSerializer<AbstractType> : IXmlSerializable
    {
        // Override the Implicit Conversions Since the XmlSerializer
        // Casts to/from the required types implicitly.
        public static implicit operator AbstractType(AbstractXmlSerializer<AbstractType> o)
        {
            return o.Data;
        }

        public static implicit operator AbstractXmlSerializer<AbstractType>(AbstractType o)
        {
            return o == null ? null : new AbstractXmlSerializer<AbstractType>(o);
        }

        private AbstractType _data;
        /// <summary>
        /// [Concrete] Data to be stored/is stored as XML.
        /// </summary>
        public AbstractType Data
        {
            get { return _data; }
            set { _data = value; }
        }

        /// <summary>
        /// **DO NOT USE** This is only added to enable XML Serialization.
        /// </summary>
        /// <remarks>DO NOT USE THIS CONSTRUCTOR</remarks>
        public AbstractXmlSerializer()
        {
            // Default Ctor (Required for Xml Serialization - DO NOT USE)
        }

        /// <summary>
        /// Initialises the Serializer to work with the given data.
        /// </summary>
        /// <param name="data">Concrete Object of the AbstractType Specified.</param>
        public AbstractXmlSerializer(AbstractType data)
        {
            _data = data;
        }

        #region IXmlSerializable Members

        public System.Xml.Schema.XmlSchema GetSchema()
        {
            return null; // this is fine as schema is unknown.
        }

        public void ReadXml(System.Xml.XmlReader reader)
        {
            // Cast the Data back from the Abstract Type.
            string typeAttrib = reader.GetAttribute("type");

            // Ensure the Type was Specified
            if (typeAttrib == null)
                throw new ArgumentNullException("Unable to Read Xml Data for Abstract Type '" + typeof(AbstractType).Name +
                    "' because no 'type' attribute was specified in the XML.");

            Type type = Type.GetType(typeAttrib);

            // Check the Type is Found.
            if (type == null)
                throw new InvalidCastException("Unable to Read Xml Data for Abstract Type '" + typeof(AbstractType).Name +
                    "' because the type specified in the XML was not found.");

            // Check the Type is a Subclass of the AbstractType.
            if (!type.IsSubclassOf(typeof(AbstractType)))
                throw new InvalidCastException("Unable to Read Xml Data for Abstract Type '" + typeof(AbstractType).Name +
                    "' because the Type specified in the XML differs ('" + type.Name + "').");

            // Read the Data, Deserializing based on the (now known) concrete type.
            reader.ReadStartElement();
            this.Data = (AbstractType)new
                XmlSerializer(type).Deserialize(reader);
            reader.ReadEndElement();
        }

        public void WriteXml(System.Xml.XmlWriter writer)
        {
            // Write the Type Name to the XML Element as an Attrib and Serialize
            Type type = _data.GetType();

            // BugFix: Assembly must be FQN since Types can/are external to current.
            writer.WriteAttributeString("type", type.AssemblyQualifiedName);
            new XmlSerializer(type).Serialize(writer, _data);
        }

        #endregion
    }
}

Отже, як же сказати XmlSerializer працювати з нашим серіалізатором, а не за замовчуванням? Ми повинні передавати наш тип у властивості атрибутів Xml, наприклад:

[XmlRoot("ClassWithAbstractCollection")]
public class ClassWithAbstractCollection
{
    private List<AbstractType> _list;
    [XmlArray("ListItems")]
    [XmlArrayItem("ListItem", Type = typeof(AbstractXmlSerializer<AbstractType>))]
    public List<AbstractType> List
    {
        get { return _list; }
        set { _list = value; }
    }

    private AbstractType _prop;
    [XmlElement("MyProperty", Type=typeof(AbstractXmlSerializer<AbstractType>))]
    public AbstractType MyProperty
    {
        get { return _prop; }
        set { _prop = value; }
    }

    public ClassWithAbstractCollection()
    {
        _list = new List<AbstractType>();
    }
}

Тут ви можете бачити, у нас є колекція та виставляється одна властивість, і все, що нам потрібно зробити, це додати параметр типу з іменем до декларації Xml, просто! : D

ПРИМІТКА. Якщо ви використовуєте цей код, я б дуже вдячний за вигук. Це також допоможе залучити більше людей до спільноти :)

Тепер, але не впевнений, що робити з відповідями тут, оскільки всі вони мали свої плюси і мінуси. Я модернізую ті, які я вважаю корисними (не ображаю тих, хто ними не був), і закрию це, коли отримаю представника :)

Цікава проблема та хороше задоволення для вирішення! :)


Я сам зіткнувся з цією проблемою деякий час тому. Особисто я в кінцевому підсумку відмовився від XmlSerializer і безпосередньо використав інтерфейс IXmlSerializable, оскільки всі мої класи все одно потребували його реалізації. В іншому випадку рішення досить схожі. Хоча хороший запис :)
Торарін

Ми використовуємо властивості XML_, де перетворюємо список на масиви :)
Arcturus

2
Оскільки конструктор без параметрів потрібен для того, щоб динамічно створювати екземпляри класу.
Сайлас Хансен,

1
Привіт! Я шукав таке рішення вже досить давно. Я думаю, це блискуче! Незважаючи на те, що я не можу зрозуміти, як ним користуватися, не могли б ви навести приклад? Ви серіалізуєте свій клас або список, що містить ваші об’єкти?
Даніель

1
Хороший код. Зверніть увагу, що безпараметричний конструктор може бути оголошений privateабо protectedзабезпечити недоступність інших класів.
tcovo

9

Одне, на що слід звернути увагу, - це той факт, що в конструкторі XmlSerialiser ви можете передавати масив типів, які серіалізатор може мати труднощі з вирішенням. Мені доводилося користуватися цією кількістю випадків, коли колекція або складний набір структур даних потребував серіалізації, а ті типи жили в різних збірках тощо.

Конструктор XmlSerialiser із параметром extraTypes

EDIT: Я хотів би додати, що цей підхід має перевагу над атрибутами XmlInclude і т.д., що ви можете розробити спосіб виявлення та складання списку ваших можливих конкретних типів під час виконання та заповнення їх.


Це те, що я намагаюся зробити, але це не так просто, як я думав: stackoverflow.com/questions/3897818/…
Лука,

Це дуже стара публікація, але для тих, хто хоче реалізувати це, як і ми, зверніть увагу, конструктор XmlSerializer з параметром extraTypes не кешує збірки, які він генерує на льоту. Це коштує нам тижнів налагодження витоку пам'яті. Отже, якщо ви хочете використовувати додаткові типи з прийнятим кодом відповіді, кешуйте серіалізатор . Ця поведінка задокументована тут: support.microsoft.com/en-us/kb/886385
Julien Lebot

3

Серйозно кажучи, розширювана структура POCO ніколи не буде надійно серіалізуватися до XML. Я кажу це тому, що можу гарантувати, що хтось прийде, продовжить ваш клас і пошкодить його.

Вам слід вивчити використання XAML для серіалізації графіків об’єктів. Він призначений для цього, тоді як серіалізація XML - ні.

Серіалізатор і десериалізатор Xaml без проблем обробляє генерики, колекції базових класів та інтерфейси (за умови, що самі колекції реалізують IListабо IDictionary). Є деякі застереження, наприклад, позначення властивостей колекції лише для читання DesignerSerializationAttributeзначком, але переробка коду для обробки цих кутових випадків не так складна.


Посилання, здається, мертве
bkribbs

Ну добре. Я нукею цей шматочок. Багато інших ресурсів про цю тему.

2

Просто коротке оновлення про це, я не забув!

Просто ще кілька досліджень, схоже, я перемагаю, потрібно лише сортувати код.

Поки що я маю таке:

  • XmlSeralizer в основному це клас , який робить деякий спритне відображення на заняттях вона сериализация. Він визначає властивості, які серіалізовані на основі типу .
  • Причина, по якій виникає проблема, полягає в тому, що відбувається невідповідність типу, вона очікує BaseType, але насправді отримує DerivedType .. Хоча ви можете подумати, що це буде сприймати це поліморфно, це не так, оскільки це вимагає цілого додаткового навантаження відображення та перевірка типу, для чого він не призначений.

Здається, цю поведінку можна замінити (код очікує на розгляд), створивши проксі-клас, який виступатиме посередником для серіалізатора. Це в основному визначить тип похідного класу, а потім серіалізує це як звичайне. Потім цей проксі-клас надсилатиме цей XML до резервної копії рядка до основного серіалізатора ..

Дивіться цей простір! ^ _ ^


2

Це, безумовно, вирішення вашої проблеми, але є ще одна проблема, яка дещо підриває ваш намір використовувати "портативний" формат XML. Погано трапляється, коли ви вирішили змінити класи в наступній версії вашої програми, і вам потрібно підтримати обидва формати серіалізації - новий і старий (оскільки ваші клієнти все ще використовують свої старі файли / бази даних, або вони підключаються до вашого сервера, використовуючи стару версію вашого продукту). Але ви більше не можете використовувати цей серіалізатор, тому що раніше

type.AssemblyQualifiedName

що схоже

TopNamespace.SubNameSpace.ContainingClass+NestedClass, MyAssembly, Version=1.3.0.0, Culture=neutral, PublicKeyToken=b17a5c561934e089

тобто містить ваші атрибути збірки та версію ...

Тепер, якщо ви спробуєте змінити версію збірки або вирішите підписати її, ця десериалізація не спрацює ...


1

Я робив подібні речі. Зазвичай я переконуюсь, що всі атрибути серіалізації XML знаходяться в конкретному класі, і просто мають властивості цього класу, що викликаються до базових класів (де це потрібно), щоб отримати інформацію, яка буде де / серіалізована, коли серіалізатор викликає ці властивості. Це трохи більше роботи з кодуванням, але це працює набагато краще, ніж спроба змусити серіалізатор просто робити те, що потрібно.


1

Ще краще, використовуючи позначення:

[XmlRoot]
public class MyClass {
    public abstract class MyAbstract {} 
    public class MyInherited : MyAbstract {} 
    [XmlArray(), XmlArrayItem(typeof(MyInherited))] 
    public MyAbstract[] Items {get; set; } 
}

2
Це чудово, якщо ви знаєте свої заняття, це найелегантніше рішення. Якщо ви завантажуєте нові успадковані класи із зовнішнього джерела, то використовувати їх, на жаль, не можна.
Володимир
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.