Сериализация XML: проблема десериализации абстрактного свойства

Я все еще пытаюсь обернуть свой мозг вокруг всей этой сериализации XML, и кажется, что мне снова нужна помощь.

Мне нужно иметь возможность десериализовать свойство абстрактного типа. Этот тип будет иметь много разных конкретных типов, добавляемых с течением времени, и упоминается во многих разных моделях, поэтому явное перечисление каждого из конкретных типов не является идеальным решением.

Я прочитал поток XML Serialization and Inherited Types и придумал следующее:

<Page>
  <introCommand>
    <PlayElement />
  </introCommand>
</Page>

**

namespace TestService
{
    public class Page
    {
        [XmlElement("introCommand", Type = typeof(XmlCommandSerializer<AbstractCommandModel>))]
        //[XmlElement(typeof(PlayElement))] **NOTE: the example works if I use this instead
        public AbstractCommandModel introCommand;
    }
}

**

namespace TestService
{
    public class AbstractCommandModel
    {
    }
}

**

namespace TestService
{
    public class PlayElement : AbstractCommandModel
    {

    }
}

**

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

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

        private AbstractCommandModel _data;
        /// <summary>
        /// [Concrete] Data to be stored/is stored as XML.
        /// </summary>
        public AbstractCommandModel 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 XmlCommandSerializer()
        {
            // 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 AbstractCommandModel Specified.</param>
        public XmlCommandSerializer(AbstractCommandModel 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(AbstractCommandModel).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(AbstractCommandModel).Name +
                    "' because the type specified in the XML was not found.");

            // Check the Type is a Subclass of the AbstractCommandModel.
            if (!type.IsSubclassOf(typeof(AbstractCommandModel)))
                throw new InvalidCastException("Unable to Read Xml Data for Abstract Type '" + typeof(AbstractCommandModel).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 = (AbstractCommandModel)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
    }
}

Однако, когда я запускаю десериализатор, я получаю InvalidOperationException, в котором говорится: "В документе XML есть ошибка (3,3)". Единственное, что я изменил из примера в упомянутом выше потоке, это имена классов.

Я на правильном пути с этим? Если да, то что я испортил, чтобы вызвать эту ошибку?

Ответ 1

В вашем примере:

//[XmlElement(typeof(PlayElement))] **NOTE: the example works if I use this instead

Это рекомендуемый способ поддержки абстрактных классов. Вы можете переключать экземпляры по имени элемента и использовать несколько объявлений вроде этого:

[XmlElement("playElement", typeof(PlayElement))]
[XmlElement("testElement", typeof(TestElement))]
public AbstractCommandModel Command;

Конечно, вам все равно придется отбросить элемент "introCommand" или добавить еще один класс, чтобы вложить указанное выше объявление.

...

Если вам все же нужно выполнить сериализацию вручную, значит, вы находитесь на правильном пути. Ваш пример работает достаточно хорошо, я думаю, вот XML-вывод:

<Page>
  <introCommand type="TestService.PlayElement, TestService, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
    <PlayElement />
  </introCommand>
</Page>

И этот xml отлично читает ваш объект; однако это представляет собой проблему безопасности. Любой, кто имеет доступ к изменению или вложению этого XML в ваше приложение, может легко вводить код.

Для проверки кода примера использовалось следующее:

    private static void Main()
    {
        StringWriter dataOut = new StringWriter();
        XmlTextWriter writer = new XmlTextWriter(dataOut);
        writer.Formatting = Formatting.Indented;

        Page p = new Page();
        p.introCommand = new PlayElement();

        new XmlSerializer(typeof(Page)).Serialize(writer, p);
        string xml = dataOut.ToString();
        Console.WriteLine(xml);

        XmlTextReader reader = new XmlTextReader(new StringReader(xml));
        Page copy = (Page) new XmlSerializer(typeof (Page)).Deserialize(reader);
    }