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
For example, if you have the following class definition
for
ItemOption
(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