Creating your own XmlSerializer

Very recently I came across an issue that required the creation of a new class derived from XmlSerializer. For reasons I don't want to get into here, we serialize an object instance into XML and store it into a database column so that we can reconstitute it later. This is a great approach except for the issue of changing class definitions.

Lastly, if you are just changing the definition of a top level class, then I suggest taking a look at XmlAttributeOverrides on msdn; however, if you are changing the definition of a class that aggregates other classes and one of your contained class has a different class definition, you need to look at using:

XmlAttributeEventHandler, XmlElementEventHandler or XmlNodeEventHandler

 

These events allows you to control the creation of these internal aggregated classes.

For example, if you have the following class definition for ItemOption that contains a collection of objects of type ProductAttribute and your ProductAttribute definition has changed, then this is a good candidate for creating your own XmlSerializer derived class.

(Simplified For Brevity)

      [Serializable]

      public class ItemOption

      {

            protected string _name=null;

            protected string _description=null;

            protected ProductAttributeCollection _generalAttributes=new ProductAttributeCollection();

 

            public ItemOption(){}

 

            #region properties

            [XmlElement ("Name")]

            public string Name

            {

                  get{return _name;}

                  set{_name=value;}

            }

            [XmlElement ("Description")]

            public string Description

            {

                  get{return _description;}

                  set{_description=value;}

            }

            [XmlArray("GeneralAttributes")]

            [XmlArrayItem("Attribute")]

            public ProductAttributeCollection GeneralAttributes

            {

                  get{return _generalAttributes;}

                  set{_generalAttributes=value;}

            }

      }

 

For the purposes of brevity, let's say that all you did was change your ProductAttribute definition from using XmlAttribute to  XmlElement.For example:

      [Serializable]

      public abstract class ProductAttribute

      {

            #region Member Variables

            protected string _name = null;

            protected object _value;

            protected bool _canOverride = false;

            #endregion

 

            [XmlElement("Name")] <-- Used to be XmlAttribute

            public string Name

            {

                  get{return _name;}

                  set{_name = value;}

            }

            [XmlIgnore()]

            public object Value

            {

                  get{return _value;}

                  set{_value = value;}

            }

            [XmlElement("CanOverride")] <-- Used to be XmlAttribute

            public bool CanOverride

            {

                  get{return _canOverride;}

                  set{_canOverride = value;}

            }

 

            public ProductAttribute(){}

            public ProductAttribute(int id, string name, bool canOverride, object attributeValue)

            {

                  _id = id;

                  _name = name;

                  _value = attributeValue;

                  _canOverride = canOverride;

            }

      }

If you make this seemingly innocuous change, your code will no longer work as expected. In this particular example, the deserialization process will NOT throw an exception and it will fill ItemOption.ProductAttributeCollection with the correct # of ProductAttributes; however, each ProductAttribute definition will contain their default values and NOT the values stored in your RDBMS.

 

The reason is that the Xml stream will contain XmlAttributes and your class definition is expecting XmlElements. This also means that you will have data stored that correspond to two different versions of your class structure. To resolve this issue, I suggest creating your own XmlSerializer class.  You want to do this because XmlSerializer will notify you when it encounters an unknown node/element or attribute. You just need to wire it up.

 

For example, I chose to use the XmlNodeEventHandler and to wire it up, you merely need to:

 

UnknownNode += new XmlNodeEventHandler(_unknownNode);

 

Then you need to create the _unknownNode f(x) to handle these events. In the following example, I have other derived classes from ProductAttribute and I omitted some of the method code for brevity, but the example illustrates how to handle this:

 

        protected void _unknownNode(object sender, XmlNodeEventArgs e)

        {

            object o = e.ObjectBeingDeserialized;

            if (o is ProductAttribute)

            {

                ProductAttribute productAttribute = (ProductAttribute)o;

                switch (e.Name)

                {

                    case "xsi:type":

                        break;

                    case "Name":

                        productAttribute.Name = e.Text;

                        break;

                    case "CanOverride":

                        productAttribute.CanOverride = Convert.ToBoolean(e.Text);

                        break;

                    default:

                        if (o is BooleanAttribute && e.Name == "Value")

productAttribute.Value = Convert.ToBoolean(e.Text);

                        else if (o is DoubleAttribute && e.Name == "Value")

productAttribute.Value = Convert.ToDouble(e.Text);

                        else if (o is IntegerAttribute && e.Name == "Value")

productAttribute.Value = Convert.ToInt32(e.Text);

                        else if (o is LongAttribute && e.Name == "Value")

productAttribute.Value = Convert.ToInt64(e.Text);

                        else if (o is PresentationAttribute && e.Name == "Value")

productAttribute.Value = e.Text;

                        else if (o is LocationAttribute && e.Name == "Value")

productAttribute.Value = e.Text;

                        else if (o is ImageAttribute && e.Name == "Value")

productAttribute.Value = e.Text;

                        else if (o is StringAttribute && e.Name == "Value")

productAttribute.Value = e.Text;

                        break;

                    }

            }

        }

The key thing to remember is that the property o.ObjectBeingDeserialized contains a pointer to the object having difficulty with the Deserialization process. In the example above, you just cast it and set the correct items depending upon the other properties contained in XmlNodeEventArgs. Also, you will noticed that I have a check for xsi:type that does nothing. I did this because even if your class definition and the Xml jibe, the XmlSerializer does not recognize this attribute name and will raise the event. Therefore, I want it to break out of the method as soon as possible.

 

Hope this helps

Mathew Nolton

1 Comment

Comments have been disabled for this content.