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)
{
XmlDocument doc = new XmlDocument();
XmlNode mainElement = doc.CreateNode(XmlNodeType.Element, "Instance", "");
if (input != null)
{
XmlAttribute att = doc.CreateAttribute("Type");
att.Value = input.GetType().AssemblyQualifiedName;
mainElement.Attributes.Append(att);
doc.AppendChild(mainElement);
SerializeRecursive(doc, mainElement, input);
return doc.InnerXml;
}
else
{
return Serialize(new NullValue());
}
}
public static object Deserialize(string input)
{
XmlDocument doc = new XmlDocument();
doc.LoadXml(input);
Type mainType = Type.GetType(doc.DocumentElement.Attributes["Type"].Value);
object result = Activator.CreateInstance(mainType);
if (result is NullValue)
result = null;
if (result != null)
{
PropertyInfo[] properties = mainType.GetProperties();
foreach (PropertyInfo pi in properties)
{
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)
{
...
}
private static void SerializeRecursive
(XmlDocument doc, XmlNode parentNode, object parentObject)
{
object o = new NullValue();
if (parentObject != null)
o = parentObject;
Type objType = o.GetType();
if (objType.IsPrimitive || objType.IsEnum)
{
parentNode.InnerText = o.ToString();
}
else if (objType == typeof(string))
{
parentNode.InnerText = (string)o;
}
else if (typeof(IList).IsAssignableFrom(objType))
{
IList tempList = (IList)o;
foreach (object i in tempList)
{
XmlNode node = doc.CreateNode(XmlNodeType.Element, "ListEntry", "");
parentNode.AppendChild(node);
XmlNode valueNode = doc.CreateNode(XmlNodeType.Element, "Value", "");
node.AppendChild(valueNode);
object item = new NullValue();
if (i != null)
item = i;
XmlAttribute att = doc.CreateAttribute("Type");
att.Value = item.GetType().AssemblyQualifiedName;
valueNode.Attributes.Append(att);
SerializeRecursive(doc, valueNode, item);
}
}
else if (typeof(IDictionary).IsAssignableFrom(objType))
{
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);
}
}
else
{
PropertyInfo[] properties = objType.GetProperties();
foreach (PropertyInfo pi in 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);
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