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

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

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

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

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

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

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

21.08.2008 14:30:53
Было бы полезно увидеть некоторые соответствующие фрагменты кода, извлеченные из классов, которые вы пытаетесь сериализовать.
Rex M 21.08.2008 14:34:19
Mate: Я снова открыл, потому что я чувствую, что другие люди могут найти это полезным, но не стесняйтесь закрывать, если вы не согласны
JamesSugrue 22.10.2008 22:06:52
Немного смущен этим, так как в этой теме ничего не было так долго?
Rob Cooper 24.10.2008 20:44:22
Odys 16.08.2011 13:39:15
7 ОТВЕТОВ

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

1
21.08.2008 14:34:43

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

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

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

9
21.08.2008 14:40:35
Это то, что я пытаюсь сделать, но это не так просто, как я думал: stackoverflow.com/questions/3897818/…
Luca 4.11.2010 20:28:42
Это очень старый пост, но для тех, кто хочет реализовать это, как мы, обратите внимание, что конструктор XmlSerializer с параметром extraTypes не кэширует сборки, которые он генерирует на лету. Это отнимает у нас недели отладки утечки памяти. Поэтому, если вы хотите использовать дополнительные типы с принятым кодом ответа, кэшируйте сериализатор . Это поведение задокументировано здесь: support.microsoft.com/en-us/kb/886385
Julien Lebot 22.09.2016 08:38:14

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

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

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

3
21.08.2015 12:46:19
Ссылка кажется мертвой
bkribbs 21.08.2015 12:21:31
Ну что ж. Я разберусь с этим. Много других ресурсов на эту тему.
user1228 21.08.2015 12:47:23

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

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

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

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

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

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

2
22.08.2008 09:00:51

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

type.AssemblyQualifiedName

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

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

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

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

2
25.12.2008 09:49:27
РЕШЕНИЕ

Задача решена!

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

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

Цели:

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

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

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

Интересная проблема и хорошее удовольствие, чтобы решить! :)

54
12.06.2009 07:42:23
Я столкнулся с этой проблемой сам некоторое время назад. Лично я закончил тем, что отказался от XmlSerializer и напрямую использовал интерфейс IXmlSerializable, поскольку все мои классы все равно должны были его реализовать. В остальном решения довольно похожи. Хорошая рецензия, хотя :)
Thorarin 12.06.2009 08:26:13
Мы используем свойства XML_, где мы преобразуем список в массивы :)
Arcturus 3.03.2010 10:13:20
Потому что для динамического создания класса нужен конструктор без параметров.
Silas Hansen 19.05.2010 13:40:30
Здравствуйте! Я долго искал подобное решение. Я думаю, что это блестяще! Несмотря на то, что я не могу понять, как его использовать, вы не могли бы привести пример? Вы сериализуете свой класс или список, содержащий ваши объекты?
Daniel 8.07.2010 21:58:23
Хороший код Обратите внимание, что безпараметрический конструктор может быть объявлен privateили protectedобеспечить его недоступность для других классов.
tcovo 4.10.2011 13:30:49

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

[XmlRoot]
public class MyClass {
    public abstract class MyAbstract {} 
    public class MyInherited : MyAbstract {} 
    [XmlArray(), XmlArrayItem(typeof(MyInherited))] 
    public MyAbstract[] Items {get; set; } 
}
1
15.10.2014 01:59:53
Это замечательно, если вы знаете свои занятия, это самое элегантное решение. Если вы загружаете новые унаследованные классы из внешнего источника, то, к сожалению, вы не сможете его использовать.
Vladimir 27.07.2015 10:58:30