Сериализация XML и унаследованные типы - PullRequest
82 голосов
/ 21 августа 2008

Исходя из моего предыдущего вопроса Я работал над тем, чтобы заставить мою объектную модель сериализоваться в XML. Но теперь я столкнулся с проблемой (неожиданный сюрприз!).

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

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

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

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

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

Ответы [ 7 ]

53 голосов
/ 12 июня 2009

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

ОК, так что я наконец-то пришел (правда, с лотом помощи от здесь !).

Итак, подведем итог:

Цели:

  • Я не хотел идти по маршруту 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>();
    }
}

Здесь вы видите, что у нас есть коллекция и одно свойство, и все, что нам нужно сделать, это добавить именованный параметр type в объявление Xml, просто! : D

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

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

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

9 голосов
/ 21 августа 2008

Следует обратить внимание на тот факт, что в конструкторе XmlSerialiser вы можете передать массив типов, которые у сериализатора могут возникнуть проблемы с разрешением. Мне приходилось использовать это довольно часто, когда необходимо сериализовать коллекцию или сложный набор структур данных, и эти типы жили в разных сборках и т. Д.

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

РЕДАКТИРОВАТЬ: Я хотел бы добавить, что этот подход имеет преимущество перед атрибутами XmlInclude и т. Д., Что вы можете разработать способ обнаружения и составления списка ваших возможных конкретных типов во время выполнения и вставлять их в.

3 голосов
/ 21 августа 2008

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

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

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

2 голосов
/ 25 декабря 2008

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

type.AssemblyQualifiedName

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

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

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

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

2 голосов
/ 22 августа 2008

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

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

Пока у меня есть следующее:

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

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

Смотри это место! ^ _ ^

1 голос
/ 15 октября 2014

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

[XmlRoot]
public class MyClass {
    public abstract class MyAbstract {} 
    public class MyInherited : MyAbstract {} 
    [XmlArray(), XmlArrayItem(typeof(MyInherited))] 
    public MyAbstract[] Items {get; set; } 
}
1 голос
/ 21 августа 2008

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

...