Using IXmlSerializable To Overcome “not expected” Error On Derived Classes

December 10th, 2006 Leave a comment Go to comments

This article extends upon Simon Hewitt’s article on CodeProject. It is strongly recommended that you read Simon’s article also in order to fully understand the concepts.

BACKGROUND
XML serialization is an indispensable technique that has built-in support for in the .NET Framework. Using XML serialization, objects can be serialized to XML streams that may be persisted to permanent storage such as files, as well as XML streams can be converted back to objects with exactly the same state as at the time of serialization. With .NET Framework’s built-in support for XML serialization, all this can be achieved with only a few lines of code using the System.Xml.Serialization.XmlSerializer object.

PROBLEM
The problem with XmlSerializer is, however, that it works by generating an on-the-fly assembly behind the scenes at compilation time, that has logic for serialization and deserialization of a given type. For this reason, the exact (concrete) type of the object and it’s public properties must be known to the compiler at the time of compilation. If we try to serialize an object that, for example, has a public member of a given base type, then at run-time we receive a System.InvalidOperationException if the public member was set to a derived type.

WORKAROUND
The workaround to above problem has generally been resorting to custom XML serialization, which can get pretty complicated at times.

SOLUTION
An ingenious solutions was discovered by Simon Hewitt using the System.Xml.Serialization.IXmlSerializable interface that allows us to mitigate the by-design issue of XmlSerializer object. While this solution works very well, it requires that a new class be created for each base class that we need serialization support for.

Using C# generics, I took the liberty of extending Simon’s solution, eliminating the need for such new classes, and encapsulating the grunt work into just one class. Once this class has been added to our project (our referenced from another assembly), all we really need to do is decorate any of our public members of base type (that may be substituted with derived types at runtime) with an attribute.

using System;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;

namespace Vocalsoft.Xml.Serialization
{
public class CustomSerializer : IXmlSerializable
{
#region Private Members
///
/// Holds the object that this serializer operates on.
///
private ItemType _parameters;
#endregion

#region Static Methods
///
/// Implicit operators the specified p.
///
/// The p.
///
public static implicit operator
CustomSerializer(ItemType p)
{
return p == null ? null : new CustomSerializer(p);
}

///
/// Implicit operators the specified p.
///
/// The p.
///
public static implicit operator
ItemType(CustomSerializer p)
{
return p.Equals(default(ItemType)) ? default(ItemType) : p.Parameters;
}
#endregion Static

#region Constructors
///
/// Initializes a new instance of the "/> class.
///
public CustomSerializer()
{
}

///
/// Initializes a new instance of the "/> class.
///
/// The parameters.
public CustomSerializer(ItemType parameters)
{
this._parameters = parameters;
}
#endregion Constructors

#region Properties

///
/// Gets parameters.
///
/// The parameters.
public ItemType Parameters
{
get { return _parameters; }
}

#endregion Properties

#region IXmlSerializable Implementation

///
/// Returns schema of the XML document representation of the object that is produced by the method and consumed by the method.
///
///
/// An that describes the XML representation of the object that is produced by the method and consumed by the method.
///
XmlSchema IXmlSerializable.GetSchema()
{
return null;
}

///
/// Generates an object from its XML representation.
///
/// The stream from which the object is deserialized.
void IXmlSerializable.ReadXml(XmlReader reader)
{
// Get type from xml attribute
Type type = Type.GetType(reader.GetAttribute("type"));

// Deserialize
reader.ReadStartElement();
this._parameters = (ItemType)new XmlSerializer(type).Deserialize(reader);
reader.ReadEndElement();
}

///
/// Converts an object into its XML representation.
///
/// The stream to which the object is serialized.
void IXmlSerializable.WriteXml(XmlWriter writer)
{
// Write type as xml attribute
writer.WriteAttributeString("type", _parameters.GetType().ToString());
new XmlSerializer(_parameters.GetType()).Serialize(writer, _parameters);
}

#endregion IXmlSerializable Implementation

}
}

Once the above class has been added to our project, all we need to do is add an attribute directly on the base class property as follows:

[XmlElement(Type = typeof(CustomSerializer))]

  1. Anonymous
    March 7th, 2007 at 11:19 | #1

    I am new to xmlSerialization as a whole. From what I gather this is a very convenient solution. I do however have a few questions:

    1. The second part where you said to add an attribute directonly on the base class property, are you referring in this case to the _parameters?

    2. Do you have any examples of how you would create serialize a derived object using this class?

    Many thanks for your efforts,

    Joe – sorry no blogger identity

  2. M. Haroon
    March 7th, 2007 at 12:27 | #2

    In (1) what I am referring to is that e.g. if you have a property in a class of some base class type, you need to place this attribute on that property.

    Example:

    [XmlElement(Type = ...]
    public EmployeeBase Employee
    {
    get {// retrun employee;}
    }

    I hope it helps.

  3. Anonymous
    May 13th, 2007 at 08:17 | #3

    Muhammad, with this post you saved my day!

    Thank you!
    Nicola

  4. Jeremy
    June 13th, 2007 at 18:14 | #4

    what if i had a base collection within the collection? could you show an example of that

  5. M. Haroon
    June 13th, 2007 at 20:50 | #5

    Jeremy, I am not sure what you mean by “a base collection within the collection”. If you can elaborate, I might be able to help.

  6. fusai
    July 10th, 2007 at 06:13 | #6

    What about List<baseClass> ?
    __________________________________
    I have this situation:

    class Container
    {
    List<myBaseClasses> _list;
    List<myBaseClasses> List { get and set on _list }

    void SetupList() {
    _list.Add(new myInheritedClass1());
    _list.Add(new myInheritedClass2());
    }
    }

    where both myInheritedClass1 and myInheritedClass2 inherit from myBaseclass..

    The question is: how can i serialize a Container in a simple way?
    ___________________________________

  7. M. Haroon
    July 10th, 2007 at 07:41 | #7

    fusai, what you have here is a list of lists. The technique mentioned in this article doesn’t care about the actual data structure you are using. As long as base class properties can be XML serialized, and you use proper attributes on the public properties of the (container) class being serialized, this technique should work just fine.

  8. R. Fusai
    July 11th, 2007 at 09:10 | #8

    sorry, I didn’t explain very well..

    what I have is a list of object of the BaseClass type, not a list of lists.

    The problem is that every object in the list belongs to a different type, InheritedClass1 and InheritedClass2, ecc..

    the property that i have made exposes the entire list, and I want to serialize this property.

    do I have to tag with “[XmlElement(Type = ...]
    ” the property that expose the list
    ?

  9. M. Haroon
    July 11th, 2007 at 11:21 | #9

    Fusai, no, I am afraid that won’t work. For this to work, you’ll have to add the “[XmlElement(Type=...)]” attribute to the item property of the list itself. This attribute basically tells .NET that the property it’s been applied to should be serialized using the base class type specified in the attribute. I hope it makes sense.

  10. Moonshield
    July 19th, 2007 at 18:42 | #10

    Hi Fusai,

    I had the same problem with generics list and here is what I’ve done.
    I have a Filter object that one of its properties was a List< Criteria > that I was unable to serialize/deserialize properly with the technique proposed above.
    Like you, Criteria is my base class for others like ClientCriteria, InvoiceCriteria etc.

    First of all, I got rid of my List< Criteria > and I replaced it with a new class CriteriaList that contains the list.
    I’ve implemented the IXmlSerializable interface.
    WriteXml method serialize each criteria (whatever is real type is).
    Do be able to deserialize it properly, I’ve added a Type property set as xmlattribute in Critera with “return this.GetType().FullName” as get.

    Please let me know of any improvement.

    The xml generated looks like this :

    < CriteriaCollection >
    < ClientCriteria Name="ClientGuID" Value="8acaa757-41bc-498a-9505-9cbcd4c5720d" SelectionType="Include" Apply="true" Type="Criterias.ClientCriteria" / >
    < Criteria Value="318d15a0-43b0-47b3-aae1-a0b3b11b9b0a" SelectionType="Include" Apply="true" Type="Criterias.Criteria" / >
    < Criteria Value="02c3a6cd-1a2c-4361-8ffa-abad4dceabda" SelectionType="Include" Apply="true" Type="Criterias.Criteria" / >
    < /CriteriaCollection >

    /// < summary >< /summary >
    [Serializable()]
    public class CriteriaList : IXmlSerializable
    {
    #region Public Property Member…

    /// < summary >< /summary >
    public System.Collections.Generic.List< Criteria > List
    {
    get { return this._List; }
    set { this._List = value; }
    }

    #endregion
    #region Private Property Member…

    /// < summary >< /summary >
    System.Collections.Generic.List< Criteria > _List = new System.Collections.Generic.List< Criteria >();

    #endregion

    #region Public Method Member…
    #region Constructor
    #endregion

    /// < summary >< /summary >
    /// < returns >< /returns >
    public System.Xml.Schema.XmlSchema GetSchema()
    {
    return null;
    }

    /// < summary >< /summary >
    /// < param name="reader" >< /param >
    public void ReadXml(System.Xml.XmlReader reader)
    {
    System.Xml.XmlReader subReader = reader.ReadSubtree();
    subReader.ReadStartElement();

    while (subReader.Depth > 0)
    this.List.Add((Criteria)new XmlSerializer(Type.GetType(reader.GetAttribute(“Type”))).Deserialize(subReader));
    }

    /// < summary >< /summary >
    /// < param name="writer" >< /param >
    public void WriteXml(System.Xml.XmlWriter writer)
    {
    foreach (Criteria oCriteria in List)
    new XmlSerializer(oCriteria.GetType()).Serialize(writer, oCriteria);
    }

    #endregion
    #region Private Method Member
    #endregion

    #region Public Class Member
    #endregion
    #region Private Class Member
    #endregion
    }

  11. Brandon
    August 9th, 2007 at 13:50 | #11

    Moonshield,

    Thanks you very much for your comment. It helped me out tremendiously.

  12. birsch
    January 17th, 2008 at 02:10 | #12

    Here’s a takeoff on the Generics List solution. It doesn’t require adding any additional information to your interface or XMLs. Instead it uses the actual list element tag to reconstruct and deserialize the object.

    NOTE: This solution was also modified to skip whitespaces and remove annoying xsi attributes.

    Items in the list will be written to XML using the real class name, as in:
    <MyList>
    <ClassA>

    </ClassA>
    <ClassB>

    </ClassB>
    </MyList>

    (assuming both ClassA and ClassB implement IMyInterface)

    public class MyList : IXmlSerializable
    {

    private List<IMyInterface> list;

    public List<IMyInterface> List
    {
    get { return list; }
    set { list = value;}
    }

    public System.Xml.Schema.XmlSchema GetSchema()
    {
    return null;
    }

    public void ReadXml(System.Xml.XmlReader reader)
    {
    //Skip whitespaces
    XmlReaderSettings settings = new XmlReaderSettings();
    settings.IgnoreWhitespace = true;

    //Create a reader that will read the list’s content
    System.Xml.XmlReader subReader = XmlReader.Create(reader.ReadSubtree(), settings);
    subReader.ReadStartElement();

    while (subReader.Depth > 0)
    {
    //turn the element name back to a fully-qualified concrete class name
    //NOTE: Assuming this class is sharing the same namespace. If not,
    // place your own logic here to reconstruct full class name
    Type type = Type.GetType(this.GetType().Namespace + “.” + reader.Name);
    Items.Add((IMyInterface)new XmlSerializer(type).Deserialize(subReader));
    }
    }

    public void WriteXml(System.Xml.XmlWriter writer)
    {
    //avoid adding xsi attributes to keep the XMLs nice and clean
    XmlSerializerNamespaces ns = new XmlSerializerNamespaces();
    ns.Add(“”, “”);
    foreach (IMyInterface item in List)
    new XmlSerializer(item.GetType()).Serialize(writer, item, ns);
    }
    }

  13. noon
    August 17th, 2008 at 12:36 | #13

    birsch your method works but there is a bug in the ReadXML function.
    The reader is not at the right position when exiting. You should call reader.Skip at the end.

    Check this :
    http://forums.xna.com/forums/p/15939/83573.aspx

  14. Peter
    September 4th, 2008 at 08:56 | #14

    Is there any way to create custom name instead class name in the generated XML?

  15. stevesp4644
    September 26th, 2008 at 05:26 | #15

    Has anyone tried any of these solutions where the derived classes are in a different assembly AND the derived xml elements are in a different XML-namespace?

    I've tried all these techniques but I keep getting the "&ltMyType xmlns='MyNamespace'&gt was not expected" error.

    I've got two schemas – the first defines the abstract base in one namespace, the second defines the derived types and I have matching assemblies for each schema.

    I'm beginning to think the namespace thing may be a blocker as the XmlSerializer-related classes only ever allow you to define one namespace (unlike the XmlDocument-related classes) as far as I can tell.

    Thanks.

  16. Odi
    December 18th, 2008 at 13:41 | #16

    Muhammad,

    This might sound silly, but would you please confirm that you are releasing your modifications to Simon Hewitt’s code into the public domain and that you retain no copyrights and such on it?

    Thanks,
    Odi

  1. No trackbacks yet.
 

Comment moderation is enabled. Your comment may take some time to appear.