Click here to Skip to main content
Click here to Skip to main content
Go to top

Serializing Complex Data Containing XDocument

, 18 Sep 2012
Rate this:
Please Sign up or sign in to vote.
How to serialize a complex data graph containing interfaces properties and XDocument members

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);

    // ........... Other field type getters and setters ....................

    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
{
    //Backing data store
    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);
        }
    }

    // ------------ other properties -----------

}

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; }
    }
    // .............. Other properties and methods .......... 
}

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.  

/// <summary>
/// Class to act as a serialization object for the ItemData that contains the XDocument
/// </summary>
[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;
    }

    // ............ Rest of the implementation ...................
}

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">
      &lt;Item&gt;&#xD;
      &lt;Field1&gt;Field1 - 1&lt;/Field1&gt;&#xD;
      &lt;Field2&gt;1&lt;/Field2&gt;&#xD;
      &lt;/Item&gt;
    </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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Coxianuk
Software Developer (Senior)
United Kingdom United Kingdom
No Biography provided

You may also be interested in...

Comments and Discussions

 
QuestionNot an article PinmemberMike Hankey18-Sep-12 0:16 
AnswerRe: Not an article PinmvpSandeep Mewara18-Sep-12 2:45 
GeneralRe: Not an article PinmemberCoxianuk18-Sep-12 2:52 
AnswerRe: Not an article Pinmember_Amy18-Sep-12 5:33 
GeneralRe: Not an article PinmemberCoxianuk18-Sep-12 6:56 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.140926.1 | Last Updated 18 Sep 2012
Article Copyright 2012 by Coxianuk
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid