Friday, May 22, 2009

How to serialize generic dictionaries and other xml "unserializable" types with the XmlSerializer

Imagine you have this type of class:
public class test2
{
public string S { get; set; }
public Type T { get; set; }
public Dictionary<string,string> dict = new Dictionary<string,string>() { { "a", "b" }, { "c", "d" } };
}
and you want to serialize it to an XML like this:
<?xml version="1.0" encoding="utf-16"?>
<test2 S="some string" T="System.Int32, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<Dictionary Key="a" Value="b" />
<Dictionary Key="c" Value="d" />
</test2>
Then you may want to check this class which greatly facilitates the process.
OK, the task at hand doesn't seem very hard and can be achieved in at least two ways:
  • one is to define a doublet property for every property or field whose type is not XML serializable. The role of the doublet property will be to convert the value of the original property/field to some serializable type (the setter of this property will do the reverse conversion) - the idea here is simple - the original property/field will be marked with the [XmlIgnore] attribute and its value will be serialized via the doublet property. The main disadvantage of this approach is obvious - you will spend much time creating these doublet properties, your class will probably double in size, you'll have to introduce quite some extra members just for the sake of XML serialization.
  • the other approach is to make your class implement the IXmlSerializable interface and write your own serialization and deserialization logic - this one is even harder than the previous - basically what you want the XmlSerializer to do for you, you do it yourself.
And now let's have a look at my solution - here is the small class at the top with the extra stuff needed to serialize it the way shown above:

public class test2 : IXmlSerializable
{
[XmlAttribute]
public string S { get; set; }
[XmlAttribute]
public Type T { get; set; }
[XmlElement(ElementName = "Dictionary")]
public Dictionary<string, string> dict = new Dictionary<string, string>() { { "a", "b" }, { "c", "d" } };

#region IXmlSerializable Members
public System.Xml.Schema.XmlSchema GetSchema() { return null; }
public void ReadXml(System.Xml.XmlReader reader)
{
Stefan.Xml.XmlSerializer.ReadXmlDeserialize(this, reader, _proxyData);
}
public void WriteXml(System.Xml.XmlWriter writer)
{
Stefan.Xml.XmlSerializer.WriteXmlSerialize (this, writer, _proxyData);
}
public static readonly Stefan.Xml.XmlSerializer.XmlProxyData _proxyData = new Stefan.Xml.XmlSerializer.XmlProxyData(typeof(test2))
{
TypeMappings = new List<Stefan.Xml.XmlSerializer.XmlTypeMappingBase>()
{
new Stefan.Xml.XmlSerializer.XmlTypeMapping<Type, string> ()
{
GetterMethod = t => t == null ? null : t.AssemblyQualifiedName,
SetterMethod = s => s == null ? null : Type.GetType(s)
},

new Stefan.Xml.XmlSerializer.XmlTypeMapping<Dictionary<string, string>, DictPair[]> ()
{
GetterMethod = t => t == null ? null : t.Select(kp => new DictPair(){ Key = kp.Key, Value = kp.Value}).ToArray(),
SetterMethod = s => { if (s == null) return null; var d = new Dictionary<string, string>(); foreach (var el in s) { d[el.Key] = el.Value; } return d; }
}
}
};
#endregion
}

So what do we have:
  • the original properties decorated with standard XmlXXX attributes (you can use them to your liking), no extra properties added
  • the class implements the IXmlSerializable interface, but in the ReadXml and WriteXml methods you see just a single call to two methods of the Stefan.Xml.XmlSerializer utility class - this class does the trick and serializes and deserializes for you
  • and the most important ingredient - the Stefan.Xml.XmlSerializer.XmlProxyData class which you provide as parameter to the Stefan.Xml.XmlSerializer methods. With it you can specify mappings between types and provide method delegates which convert to and from the two mapped types of each mapping. What is the idea here - if you have properties of type which is not XML serializable you can define a mapping for this type to a type which is serializable - e.g. between System.Type and System.String; then you need to provide two method delegates - one which converts from System.Type to System.String and the other - from System.String to System.Type. And when you provide this type mapping to the Stefan.Xml.XmlSerializer class it will treat every property of the mapped type in your class as if it had the substitute type.
So I guess you start to get the whole idea behind this serialization approach - it is somewhat of a combination between the two trivial approaches mentioned above but eliminates most of the drawbacks in them and helps automating the process.
Let's have a look now at the
Stefan.Xml.XmlSerializer.XmlProxyData class and see how the type mappings can be defined. We have in it this property: List <XmlTypeMappingBase> TypeMappings. XmlTypeMappingBase is an abstract class which is inherited by this generics definition: public class XmlTypeMapping<TProxied, TProxy>, which has these two members that you need to actually set: Func<TProxy, TProxied> SetterMethod and Func<TProxied, TProxy> GetterMethod. These are none other than the conversion methods for the two types in the type mapping. The two type parameters in the XmlTypeMapping<TProxied, TProxy> definition are actually the two types you want to map. TProxied in our example will be System.Type and TProxy will be System.String.
In the sample class above besides the System.Type to System.String mapping, you can see one more mapping - it is between Dictionary<string,string> and DictPair[], the latter is a small class, defined as:
public class DictPair
{
[XmlAttribute]
public string Key { get; set; }
[XmlAttribute]
public string Value { get; set; }
}
Actually it is a good idea to have such auxiliary classes and the definitions of the
XmlTypeMapping<TProxied, TProxy>-s or of the whole List<> of type mappings in a separate utility class, so that they can be reused by the various classes you want to serialize.

How does this work

And several words about how this thing works - so I guess it is more or less clear that
Stefan.Xml.XmlSerializer uses internally the standard .NET XmlSerializer. There are yet some other similarities between the two XmlSerializer-s. The standard XmlSerializer generates dynamic classes (serializers for the specific types you serialize which inherit the XmlSerializer class) in dynamic assemblies. Well, this is basically what Stefan.Xml.XmlSerializer does - it creates a dynamic type that has properties matching all public properties and fields of the serialized type - for the properties whose type is in the type mapping list the matching properties are from the substitute type and their getters and setters actually call the conversion methods that you provide in the type mapping definitions. This dynamic proxy type holds an instance of the original type and the getters and setters of its properties get and set the corresponding members of the original type. Also the XmlXXX custom attributes set for the properties of the original type are set for the properties of the proxy type as well. So when you serialize calling Stefan.Xml.XmlSerializer, an instance of this proxy type is created and its internal member is initialized with the instance that you want to serialize - then this proxy instance is serialized by the standard XmlSerializer providing converted properties for all specified types. The deserialization works the opposite - the standard XmlSerializer creates an instance of the proxy type from the XML stream which initializes its internal instance of the original type, then the properties of this instance are copied to the original instance that you provide to the Stefan.Xml.XmlSerializer.ReadXmlDeserialize method.

2 comments:

  1. Thank you for making this.
    I needed to serialize object graphs - where an object reference gets replaced by a string that is also a key into a dictionary instead of being dumped in its entirety - and this code made it a lot easier.

    But there is one issue - I have complex structs that I want to serialize. I can implement IXmlSerializable on structs and serialization works fine, but when deserializing, the resulting struct has the default values. Any ideas?

    This is a simple example to reproduce the problem:

    public struct TestStruct : IXmlSerializable
    {
    public int TestInt;

    public TestStruct(int i)
    {
    TestInt = i;
    }

    #region IXmlSerializable Members

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

    public void ReadXml(System.Xml.XmlReader reader)
    {
    CustomXmlSerializer.ReadXmlDeserialize(this, reader, _proxyData);
    }

    public void WriteXml(System.Xml.XmlWriter writer)
    {
    CustomXmlSerializer.WriteXmlSerialize(this, writer, _proxyData);
    }

    public static readonly CustomXmlSerializer.XmlProxyData _proxyData = new CustomXmlSerializer.XmlProxyData(typeof(TestStruct))
    {
    TypeMappings = new List[LESSTHAN]CustomXmlSerializer.XmlTypeMappingBase>()
    {
    new CustomXmlSerializer.XmlTypeMapping[LESSTHAN]int, string> ()
    {
    GetterMethod = t => t.ToString(),
    SetterMethod = s => int.Parse(s)
    }
    }
    };

    #endregion

    }

    ReplyDelete
  2. hi happysad,

    sorry for the late reply, buy i've been very busy lately with ... babysitting.
    as for your issue - i quickly modified the custom xml serializer and now it works fine with structs too - you can download the updated sources from the same location.
    and you will need one small change in the ReadXml method of your struct - now it should look like this:

    public void ReadXml(System.Xml.XmlReader reader)
    {
    this = (TestStruct)Stefan.Xml.XmlSerializer.ReadXmlDeserializeStruct(this, reader, _proxyData);
    }

    don't worry about the assignment expression with the this keyword as l value - since structs are value types the assignment expression performs a bitwise copy of the contents of the right side struct to the "this" (current) one.
    i hope this reply is still useful and not too late.

    ReplyDelete