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

85

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

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

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

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

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

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

Роб Купер
источник
1
Было бы полезно увидеть некоторые соответствующие фрагменты кода, извлеченные из классов, которые вы пытаетесь сериализовать.
Rex M
Mate: Я снова открылся, потому что чувствую, что другие люди могут найти это полезным, но не стесняйтесь закрыть, если вы не согласны
JamesSugrue
Немного смущает это, так как в этой ветке так долго ничего не было?
Роб Купер,
Вот ответ: stackoverflow.com/questions/6737666/…
Odys

Ответы:

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_, где преобразуем список в массивы :)
Арктур
2
Поскольку конструктор без параметров необходим для динамического создания экземпляра класса.
Сайлас Хансен,
1
Здравствуйте! Я уже довольно давно искал подобное решение. Я думаю, это великолепно! Хотя я не могу понять, как его использовать, не могли бы вы привести пример? Вы сериализуете свой класс или список, содержащий ваши объекты?
Daniel
1
Хороший код. Обратите внимание, что конструктор без параметров может быть объявлен privateили protectedзаставить его быть недоступным для других классов.
tcovo
9

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

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

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

Шон Остин
источник
Это то, что я пытаюсь сделать, но это непросто, как я думал: stackoverflow.com/questions/3897818/…
Лука
Это очень старый пост, но для тех, кто пытается реализовать это, как это сделали мы, обратите внимание, что конструктор XmlSerializer с параметром extraTypes не кэширует сборки, которые он генерирует на лету. Это стоит нам недель на устранение утечки памяти. Поэтому, если вы хотите использовать дополнительные типы с принятым кодом ответа, кешируйте сериализатор . Это поведение
описано
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 находятся в конкретном классе, и просто свойства этого класса вызываются через базовые классы (где требуется) для получения информации, которая будет де / сериализована, когда сериализатор вызывает те свойства. Это немного больше работы по кодированию, но это работает намного лучше, чем попытки заставить сериализатор просто делать правильные вещи.

TheSmurf
источник
1

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

[XmlRoot]
public class MyClass {
    public abstract class MyAbstract {} 
    public class MyInherited : MyAbstract {} 
    [XmlArray(), XmlArrayItem(typeof(MyInherited))] 
    public MyAbstract[] Items {get; set; } 
}
user2009677
источник
2
Это здорово, если вы знаете свои классы, это самое элегантное решение. Если вы загружаете новые унаследованные классы из внешнего источника, вы, к сожалению, не можете его использовать.
Владимир