Introduction
As part of unit testing an application I came across a situation where I needed to store a complex object graph so that I could use it to drive my unit tests. Part of the requirement was to have the resulting persistence editable in notepad so that I could generate other scenarios based on the one file. My problems started when I realised
XmlSerializer
could not handle properties that are an interface and were compounded when
DataContractSerializer
failed to store the backing store for an object because it was held in an
XDocument
. So how could this be done?
Background
Consider the following Interface which will allow a flexible backing store for data represented by a class. In the real solution, this wrapped up a XML feed from a 3rd party application. To ensure my solution was 3rd party agnostic this provided a generic way to get hold of the data provided by any similar solution whether
it was XML or otherwise.
public interface IRawData
{
void Get(string fldName, out string value);
void Set(string fldName, string value);
string ToString();
}
A Sample implementation might look as follows. This acts as actual an implementation for accessing the backing store, which as described above came in XML form from the 3rd party. The implementation of the Get and Set methods use the XML as the source and destination rather than extracting from the XML to local fields and using those.
public class ItemData : IRawData
{
private XDocument _item = null;
public ItemData()
{
_item = XDocument.Parse(@"<Item></Item>");
}
public ItemData(string itemXml)
{
_item = XDocument.Parse(itemXml);
}
public override string ToString()
{
return _item.ToString();
}
public void Get(string fldName, out string value)
{
value = (from it in _item.Descendants("Item")
select (string)it.Element(fldName)).FirstOrDefault();
}
public void Set(string fldName, string value)
{
if (value == null)
value = "";
XElement item = _item.Descendants("Item").First().Element(fldName);
if (item == null)
_item.Element("Item").Add(new XElement(fldName, value));
else
item.Value = value;
}
}
This is then encapsulated in a further class. Although a lot of the information came from the 3rd party it was a requirement to add additional information so this class was introduced to hold the 3rd party data and my own 'derived' data. Whilst most of the raw data from the 3rd party was exposed via properties on this class, some, lesser used, data was not provided. If processes required this they could access the raw data themselves. This leads to the first issue of the ItemData
being exposed as an interface.
public class ItemDetails
{
private IRawData _itemData = new ItemData();
private string _field3 = string.Empty;
public IRawData ItemData
{
get { return _itemData; }
set { _itemData = value; }
}
public string Field1
{
get
{
string value = "";
_itemData.Get("Field1", out value);
return value;
}
set
{
_itemData.Set("Field1", value);
}
}
}
Due to the 3rd party providing several items for my 'transaction' the ComplexObject
was required to hold them all. Again it also needed to hold derived information for the onward processes.
public class ComplexObject
{
private List<ItemDetails> _items;
private string _name;
public ComplexObject()
{
_items = new List<ItemDetails>();
}
public List<ItemDetails> Items
{
get { return _items; }
set { _items = value; }
}
}
Having first tried the XmlSerialization
class to serialize the graph I found that it could not handle situations where a property was exposed using an interface.
Rather than refactoring the implementation by adding 'serializable' version of the property and wiring up the actual property to that, I went for using
the DataContractSerializer
that promised much more.
On calling serialization and then reconstituting the object via the deserialization, the unit test failed to return any data. The problem is that XDocument
does not have any properties or fields that can be serialized, so an empty tag is placed in the output stream. When this is deserialized then the backing object is lost and the methods to get information from it return null or raise an exception.
Type[] knownTypes = new Type[] { typeof(ItemDetails),
typeof(ItemData)
};
DataContractSerializer ser = new DataContractSerializer(obj.GetType(), knownTypes);
using (FileStream file = new FileStream("TestOut.xml", FileMode.Create, FileAccess.Write))
{
ser.WriteObject(file, obj);
}
Results in the ItemDetails
being stored as:
<ItemDetails>
<Field1>Field1 - 1</Field1>
<Field2>1</Field2>
<Field3>Other Info - 1</Field3>
<ItemData i:type="ItemData"/>
</ItemDetails>
Whilst it looks like the values for Field1
and Field2
are stored, because the implementation looks at the ItemData
class,
which uses the XML stored in the XDocument
and that is lost, deserialization results in the data not being available.
Solution
After the usual searches and finding no real answers, I went back to basics and read the
MSDN documentation for
DataContractSerializer
.
On closer inspection I found the DataContractSurrogate.
This can only be set via a constructor but is an implementation of the
IDataContractSurrogate
.
The idea is that the DataContractSerializer
calls methods on this 'surrogate' class to determine if a property type needs to be mapped from its actual type
to one that can be serialized. The data is then 'cloned' and returned for serialization. The reverse is true when deserializing the persisted store.
To start with I needed to define a serializable version of ItemData
. The obvious method is to take the XML string and store that.
So the ItemSurrogated
class is defined as below. Note the use of DataContract
and DataMember
to allow serialization.
[DataContract]
class ItemSurrogated
{
[DataMember]
public string xmlData;
}
Now we have the serializable class we need to implement the IDataContractSurrogate
interface to enable the translation and storage of the ItemData
.
Below shows just the minimal implementation. The demo project has the full implementation.
class SerializerSurrogate : IDataContractSurrogate
{
public object GetObjectToSerialize(object obj, Type targetType)
{
if (obj is ItemData)
{
ItemSurrogated ois = new ItemSurrogated();
ois.xmlData = obj.ToString();
return ois;
}
return obj;
}
public object GetDeserializedObject(object obj, Type targetType)
{
if (obj is ItemSurrogated)
{
ItemSurrogated ois = obj as ItemSurrogated;
return new ItemData(ois.xmlData);
}
return obj;
}
}
During serialization the DataContractSerializer
makes a call to GetObjectToSerialize
for each 'complex' object in the graph.
The method provides the opportunity for the object to be 'translated' into an object that can be serialised. So for my requirements, ItemData
did
not serialize correctly so I take the XML string from the ItemData.ToString()
, which simply calls ToString()
on the XDocument
,
and assign it to the xmlData
property of the ItemSurrogated
object. DataContractSerializer
now serializes the instance
of ItemSurrogated
rather than ItemData
.
Now when I create the DataContractSerializer
, specifing the surrogate implementation,
the actual data held by the XDocument
is stored in the XML.
Type[] knownTypes = new Type[] { typeof(ItemDetails),
typeof(ItemSurrogated)
};
DataContractSerializer ser = new DataContractSerializer(obj.GetType(), knownTypes,
Int16.MaxValue, false, true,
new SerializerSurrogate());
using (FileStream file = new FileStream("SurrogateTestOut.xml",
FileMode.Create, FileAccess.Write))
{
ser.WriteObject(file, obj);
}
<ItemDetails z:Id="3">
<Field1 z:Id="4">Field1 - 1</Field1>
<Field2>1</Field2>
<Field3 z:Id="5">Other Info - 1</Field3>
<ItemData z:Id="6" i:type="ItemSurrogated">
<xmlData z:Id="7">
<Item>
<Field1>Field1 - 1</Field1>
<Field2>1</Field2>
</Item>
</xmlData>
</ItemData>
</ItemDetails>
Note that we supply the ItemSurrogated
class rather than the ItemData
class as a known class as it is ItemSurrogated
that gets serialized not ItemData
.
Deserialization is achieved using:
using (FileStream file = new FileStream("SurrogateTestOut.xml",
FileMode.Open, FileAccess.Read))
{
obj = (ComplexObject)ser.ReadObject(file);
}
During the ReadObject
the DataContractSerializer
calls GetDeserializedObject
which, for my requirements,
detects that the object obtained from the XML source is an ItemSurrogated
and uses the xmlData
property to create a new instance
of the ItemData
class, so restoring the original object graph, with the original data restored properly.
Points of Interest
It is worth taking some time to read through the DataContractSerializer
documentation as there is clearly more capabilities than first meet the eye and remember;
Just because all your searches say it cannot be done, there is generally a work around somewhere!
History
18/09/2012 - First release.
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.