Сериализация XML и унаследованные типы

Следуя моему предыдущему вопросу я работал над тем, чтобы моя объектная модель сериализовалась в XML. Но я столкнулся с проблемой (quelle удивляет!).

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

Я подумал, что было бы просто добавить атрибуты XML во все классы, и все будет персик. К сожалению, это не так!

Итак, я сделал кое-какие работы в Google, и теперь я понимаю, почему он не работает. В этом XmlSerializer на самом деле делает некоторое умное отражение, чтобы сериализовать объекты в/из XML, и поскольку он основан на абстрактном типе, он не может понять, что, черт возьми, он говорит с. Хорошо.

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

Еще одна вещь, которую я должен добавить, это то, что я НЕ НЕ хочет спуститься по маршруту XmlInclude. С ним просто связано много, и эта область системы находится в тяжелом развитии, поэтому она будет настоящей головной болью обслуживания!

Ответ 1

Проблема решена!

ОК, поэтому я, наконец, добрался туда (по общему признанию, много из здесь!).

Итак, суммируем:

Цели:

  • Я не хотел спускаться по маршруту 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

ПРИМЕЧАНИЕ. Если вы используете этот код, я бы очень признателен за крик. Это также поможет привлечь больше людей к сообществу:)

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

Интересная проблема и хорошая забава решить!:)

Ответ 2

Одна вещь, на которую стоит обратить внимание, это тот факт, что в конструкторе XmlSerialiser вы можете передать массив типов, которые могут возникнуть при работе с сериализатором. Я должен был использовать это довольно много раз, когда сбор или набор комплексных данных необходимо было сериализовать, и эти типы жили в разных сборках и т.д.

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

EDIT: я бы добавил, что этот подход имеет преимущество по сравнению с атрибутами XmlInclude и т.д., что вы можете разработать способ обнаружения и составления списка ваших возможных конкретных типов во время выполнения и наполнить их.

Ответ 3

Серьезно, расширяемая структура POCOs никогда не будет последовательно сериализоваться в XML. Я говорю это, потому что я могу гарантировать, что кто-то придет, расширит ваш класс и убьет его.

Вы должны изучить использование XAML для сериализации графиков объектов. Он предназначен для этого, тогда как сериализация XML - нет.

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

Ответ 4

Просто быстрое обновление об этом, я не забыл!

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

До сих пор у меня есть следующее:

  • XmlSeralizer - это, в основном, класс, который делает отличное отражение классов, которые он сериализует. Он определяет свойства, которые сериализуются на основе Тип.
  • Причина возникновения проблемы заключается в том, что возникает несоответствие типа, она ожидает BaseType, но на самом деле получает DerivedType. Хотя вы можете думать, что она будет обрабатывать ее полиморфно, это не так, поскольку она будет включать в себя целое дополнительную нагрузку на отражение и проверку типов, которые она не предназначена для выполнения.

Такое поведение, по-видимому, может быть переопределено (ожидающий код), создав прокси-класс, чтобы действовать как промежуток для сериализатора. Это будет в основном определять тип производного класса, а затем сериализовать это как обычно. Этот прокси-класс затем подаст этот XML файл обратно в основной сериализатор.

Смотрите это пространство! ^ _ ^

Ответ 5

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

type.AssemblyQualifiedName

который выглядит как

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

который содержит ваши ассемблерные атрибуты и версию...

Теперь, если вы попытаетесь изменить версию сборки или решили ее подписать, эта десериализация не будет работать...

Ответ 6

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

Ответ 7

Еще лучше, используя обозначение:

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