Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Simple Object Serializer

0.00/5 (No votes)
8 Dec 2009 2  
Simple object serializer using .NET Reflection and XML

Introduction

How many times did you just want to build a simple client-server application via TCP/IP ? How many times did you mess around with the .NET Framework serializers (either XMLSerializer or BinarySerializer) to get your data objects sent correctly across the network ? Don't struggle anymore, take a look at the simple serialization approach described in this article. No need to mess around with MarshalByRefObject, XML Attribute classes a.s.o. any more.

Background

(If you are not familiar with the .NET Reflection namespace, I suggest you have a look at one of the tutorials about Reflection here at CodeProject first before reading this article. Otherwise the code presented here might seem a little cryptic and confusing.

Using the Code

Ok, let's get started. What we are going to do is develop a custom serializer which transforms any given object into a string in XML-format. In this article, I will limit the functionality of the serializer to serializing / deserializing objects which consist of Primitives, Enums, Lists (generic and non-generic) and Dictionaries (generic dictionary, Hashtable) as well as nested combinations of these basic types. The serializer will basically loop recursively through the given input and transform it to the requested output. For the process of serializing, the input will be an object instance which is limited to the usage of types specified above and the output will be a string instance containing an XML document. For the process of deserialization, the roles of input and output are swapped accordingly.

Following this thought, for the user of our serializer class, we will need just two public methods:

  • public string Serialize(object input);
  • public object Deserialize(string input);
namespace MySerialization
{
  public class NullValue
  {
    public NullValue()
    {
    }
  }

  public class ObjectSerializer
  {  
    public static string Serialize(object input)
    {
      //Create the xml document instance we are going to work with as well as
      //a main element node
      XmlDocument doc = new XmlDocument();
      XmlNode mainElement = doc.CreateNode(XmlNodeType.Element, "Instance", "");
          
      if (input != null)
      {
        //We need to store the type information of the input object, because we
        //will need it to deserialize this again later. The type information is
        //stored in an attribute of the main element node.
        XmlAttribute att = doc.CreateAttribute("Type");
        att.Value = input.GetType().AssemblyQualifiedName;
        mainElement.Attributes.Append(att);

        //Add the main element node to the xml document.
        doc.AppendChild(mainElement);

        //Here we enter the recursion, passing the xml document instance, the parent
        //xml node of the next recursion level and the parent object of the next 
        //recursion level in as parameters. Because the xml document instance and the 
        //node element are reference types (classes) we don't need a "ref" here.
        //The object could be a value type like a primitive or an enum, but as we are
        //serializing, we will only need read access to the object, so no "ref" needed
        //here either.
        SerializeRecursive(doc, mainElement, input);
        
        //At the end of the recursion, we return the string representation of our xml
        //document instance.
        return doc.InnerXml;
      }
      else
      {
        //During deserialization, we would get in trouble if we were trying to retrieve
        //type information of a null value. Therefore we replace any null values with a
        //dummy instance of a type NullValue.
        return Serialize(new NullValue());
      }
    }
    
    public static object Deserialize(string input)
    {
      //We put the input string back into an xml document instance. This will make life
      //easier in the recursion, because we can loop through the xml node objects.
      XmlDocument doc = new XmlDocument();
      doc.LoadXml(input);
      
      //Retrieve the type of the serialized object, using the Type-Attribute we added 
      //during the serialization. Using this type information, we can create an instance
      //of this type.
      Type mainType = Type.GetType(doc.DocumentElement.Attributes["Type"].Value);
      object result = Activator.CreateInstance(mainType);

      //Any NullValue instances we created during serialization 
      //to replace null values will
      //be transformed back into null values.
      if (result is NullValue)
        result = null;

      if (result != null)
      {
        //Now we get all properties of our object type so we can start recursion.
        PropertyInfo[] properties = mainType.GetProperties();
        foreach (PropertyInfo pi in properties)
        {
          //For this article we exclude indexed properties like type accessors.
          ParameterInfo[] paramInfos = pi.GetIndexParameters();
          if (paramInfos.Length == 0)
            DeserializeRecursive(pi, result, doc.DocumentElement);
        }
      }

      return result;
    }
  }
}

Note that in this article, for the matter of simplicity, we expect that the assemblies which contain the types we are serializing/deserializing are known to each process in the application. In a more universal context, we could also store the assembly information of the assemblies which contain our types, using a second XmlAttribute we could attach to the instance node. That way, we could load that assembly during deserialization and take the type of our serialized object out of that assembly. We could do that in every stage during the recursion but keep in mind that this blows up the size of your XML file big time, so you should ask yourself if you really need this flexibility or if you can live with the limitation that all processes know your types already.

Next we will take a look at the SerializeRecursive method and the different cases it handles:

  • Primitives
  • Enum Types
  • List Types
  • Dictionary Types
namespace MySerialization
{
  public class NullValue
  {
    public NullValue()
    {
    }
  }

  public class ObjectSerializer
  {  
    public static string Serialize(object input)
    {
      ...
    }
    
    public static object Deserialize(string input)
    {
      ...
    }
    
    //Keep in mind that the xml node is the parent node 
    //and the object is the parent object of the 
    //subobject we are going to serialize in the current recursion level.
    private static void SerializeRecursive
	(XmlDocument doc, XmlNode parentNode, object parentObject)
    {      
      //Handle null value by using a NullValue instance as replacement.
      object o = new NullValue();
      if (parentObject != null)
        o = parentObject;

      Type objType = o.GetType();

      //Case 1: Parent object is a primitive or an enum (or a string)
      if (objType.IsPrimitive || objType.IsEnum)
      {        
        //If our parent object is a primitive or an enum type, 
        //we have reached a leaf in the recursion.
        //A leaf always ends in a concrete value, 
        //which we then store as value of the last parent xml
        //node.
        parentNode.InnerText = o.ToString();        
      }
      else if (objType == typeof(string))
      {
        //The string type has to be handled separately, 
        //because it doesn't count as a primitive.
        parentNode.InnerText = (string)o;
      }
      //Case 2: Parent object is a list type
      else if (typeof(IList).IsAssignableFrom(objType))
      {
        //Unbox the list and loop through all elements, 
        //entering the next recursion level.
        IList tempList = (IList)o;        
        foreach (object i in tempList)
        {
          //For each entry in the list, 
          //we create a child node "ListEntry" and add it below
          //out current parent node.
          XmlNode node = doc.CreateNode(XmlNodeType.Element, "ListEntry", "");
          parentNode.AppendChild(node);

          //Below the ListEntry node, we add another child node 
          //which contains the value of
          //the list item.
          XmlNode valueNode = doc.CreateNode(XmlNodeType.Element, "Value", "");          
          node.AppendChild(valueNode);
          
          //Handle the case when the list item is a null value, 
          //by replacing it with a NullValue
          //instance.
          object item = new NullValue();
          if (i != null)
            item = i;

          //Save the type information of the list item object for deserialization.
          //We add this type info as an attribute, 
          //in the same way we did with the main document
          //node.
          XmlAttribute att = doc.CreateAttribute("Type");
          att.Value = item.GetType().AssemblyQualifiedName;
          valueNode.Attributes.Append(att);
          
          //Enter the next recursion step, using the valueNode 
          //as new parent node and the list item
          //as new parent object.
          SerializeRecursive(doc, valueNode, item);
        }

      }
      //Case 3: Parent object is a dictionary type.
      else if (typeof(IDictionary).IsAssignableFrom(objType))
      {
        //This case works in about the same way as the list type serialization above
        //and should be quite self-explainatory if you understood the list case.
        //The only difference here is that the recursion tree will split into two
        //branches here, one for the key and one for the value of each dictionary
        //entry.
        IDictionary tempDictionary = (IDictionary)o;
        foreach (object key in tempDictionary.Keys)
        {
          XmlNode node = doc.CreateNode(XmlNodeType.Element, "DictionaryEntry", "");
          parentNode.AppendChild(node);

          XmlNode keyNode = doc.CreateNode(XmlNodeType.Element, "Key", "");
          XmlAttribute kAtt = doc.CreateAttribute("Type");
          kAtt.Value = key.GetType().AssemblyQualifiedName;
          keyNode.Attributes.Append(kAtt);

          node.AppendChild(keyNode);
          SerializeRecursive(doc, keyNode, key);

          XmlNode valueNode = doc.CreateNode(XmlNodeType.Element, "Value", "");
          XmlAttribute vAtt = doc.CreateAttribute("Type");

          object entry = new NullValue();
          if (tempDictionary[key] != null)
            entry = tempDictionary[key];

          vAtt.Value = entry.GetType().AssemblyQualifiedName;
          valueNode.Attributes.Append(vAtt);

          node.AppendChild(valueNode);          
          SerializeRecursive(doc, valueNode, entry);
        }
      }
      //Case 4: Parent object is a complex type (class or struct)
      else
      {
        //This case looks similar to what we did in the original Serialize() method.
        //First we get all properties of the current object, then we loop through them,
        //continuing the recursion.
        PropertyInfo[] properties = objType.GetProperties();
        foreach (PropertyInfo pi in properties)
        {
          //Exclude indexed properties
          ParameterInfo[] test = pi.GetIndexParameters();
          if (test.Length == 0)
          {
            XmlNode node = doc.CreateNode(XmlNodeType.Element, pi.Name, "");
            XmlAttribute att = doc.CreateAttribute("Type");
            node.Attributes.Append(att);
            att.Value = pi.PropertyType.AssemblyQualifiedName;
            parentNode.AppendChild(node);
            
            //Get the concrete value of the current property in the parent object
            //so we can continue recursion based on that value object.
            SerializeRecursive(doc, node, pi.GetValue(o, null));
          }
        }
      }
    }
  }
}

If you worked through this recursive method and understood all cases and how they work, the DeserializeRecursive() method will be self-explanatory. You can find it annotated in the sample code attached to this article.

Points of Interest

The code in this example is meant to demonstrate the basics behind the idea. It does not contain any error handling and it does not handle "unhandled" types. Therefore I don't recommend to use it in a project as is.

Basically this serializer can handle any class or struct objects which contain only primitives, enums, strings, Lists and Dictionaries or class/struct objects which also only contain these basic types.

Keep in mind that there are some objects which cannot be serialized like Forms, Controls, Type or Assembly objects. Before you use this approach in one of your projects, you should handle these types accordingly.

History

  • Version 1.0: Running ObjectSerializer without error handling

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here