1

I am still trying to wrap my brain around this whole xml serialization thing, and it appears I am in need of some assistance yet again.

I need to be able to deserialize a property of an abstract type. This type will have many different concrete types being added over time, and is referenced in a lot of different models, so explicitly listing each of the concrete types is not an ideal solution.

I have read the XML Serialization and Inherited Types thread, and have come up with the following:

<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
    }
}

However, when I run the deserializer I get an InvalidOperationException stating "There is an error in XML document (3,3).". The only thing I have changed from the example in the before mentioned thread is the class names.

Am I on the right track with this? If so, what did I mess up to cause this error?

Community
  • 1
  • 1
drkstr
  • 609
  • 6
  • 9
  • 2
    This is why I long ago ditched the XmlSerializer for XamlServices and the NetDataContractSerializer. –  Jul 18 '11 at 18:46
  • @Will XamlServices actually looks a lot closer to what I need. In fact the entire purpose is to construct a complex object graph from an XML structure. The downside is this would require a considerable amount of refactoring on the XML to work. This is not out of the question, but not exactly ideal either. I'll have to look into it a bit more. – drkstr Jul 18 '11 at 21:39
  • I understand how that goes. But it is good to know, for future, that there are many new alternatives to the XmlSerializer. –  Jul 19 '11 at 10:26

1 Answers1

7

From your example:

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

This is the recommended way to support abstract classes. You can switch instances by element name and use multiple declarations like this:

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

Of course you will still need to drop the "introCommand" element or add another class to nest the above declaration into.

...

If you still need to do the serialization manually then you are on the correct path. Your example works well enough I guess, here is the XML output:

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

And this xml reads just fine into your object; however, this presents a security concern. Anyone with access to modify or inject this XML into your application can easily inject code.

The following was used to test your example code:

    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);
    }
csharptest.net
  • 62,602
  • 11
  • 71
  • 89
  • So basically I need to list every concrete type whenever I have a model property that takes an abstract type? There is no way to map the node name to the class it should instantiate and assign to the model property? – drkstr Jul 18 '11 at 19:58
  • Yes there is a way to "map the node name to the class it should instantiate" by providing the fixed list. To do anything else, like loading a type by name, is basically allowing code injection. That may not deter you if the environment is secure and the risk is deemed low enough, but it is one reason it is not supported without the code you wrote to explicitly support it. I still recommend just using a fixed list if possible. – csharptest.net Jul 18 '11 at 20:24
  • Basically I need to construct a model that has a property "introCommand" and assign an instance of whatever class is assigned to that property in XML. So in this case introCommand should contain an instance of PlayElement. I am not so concerned about listing each type explicitly (or security for that matter), as I am with constructing an object graph that conforms to an external API. Am I going about this the wrong way? Do I need to instead use a "serializable friendly" class to construct my model in the appropriate format? – drkstr Jul 18 '11 at 21:18
  • "Do I need to instead use a "serializable friendly" class to construct my model..." usually this is the end result ;) This is on reason so many have abandoned the XmlSerializer in favor of the DataContractSerializier – csharptest.net Jul 19 '11 at 16:59