Creating a DeSerializator is like reinventing the wheel but at the same time, it is a great task. Obviously, if the DeSerializator is not being made only for a specific class, then we have to use reflection, and during the implementation, we can meet many difficulties and interesting problems.

At first, the idea to implement a simple DeSerializator which can DeSerialize a simple class from an XML document seems easy until the class contains simple value type properties or some very simple class properties. The difficulties begin when we have some collection or interface type properties. Importantly, I wanted to use fewer tools from System.XML assembly.

The basic idea is to use a simple XML with this structure.

<tag>value</tag>

OR NOT USED TAG

<tag/>

CONCEPTION

An XML is a tree where there is a root element, and the tree can have many branches. The leaf element is actually the value element and each element in the path is a branch.

The class object that we want to instantiate by XML contains everything that is in the XML but there is no surity if the XML contains everything that is in the class.

It means that we have to traverse on the XML tree and instantiate the objects in the class accordingly.

I choose Pre-order traversal which works in the way shown below.

Pre-order: F, B, A, D, C, E, G, I, H.

Although it is a binary tree we can use this approach on our non-binary XML tree as well.

IMPLEMENTATION

For the sake of simplicity, first create a tree object from the XML and use this object from now on.

The node object of the tree contains the tag name which is the type of the object in the class, the possible value and possible child nodes,

  1. private class Node
  2. {
  3. public int level { get; set; }
  4. public int index { get; set; }
  5. public string tag { get; set; }
  6. public string value { get; set; }
  7. public List<Node> nodes { get; set; }
  8. public Node()
  9. {
  10. nodes = new List<Node>();
  11. }
  12. }

Get the text from the XML file and create a concatenated string from it by removing the possible namespaces and commented out parts,

  1. private string GetTextFromXml(XDocument doc)
  2. {
  3. //Remove Namespaces from the tags
  4. doc.Descendants().Attributes().Where(x => x.IsNamespaceDeclaration).Remove();
  5. foreach (var elem in doc.Descendants())
  6. elem.Name = elem.Name.LocalName;
  7. var xmlDocument = new XmlDocument();
  8. xmlDocument.Load(doc.CreateReader());
  9. string text = xmlDocument.OuterXml;
  10. //Remove breaks
  11. text = text.Replace("\n", "");
  12. //Remove whitespaces
  13. text = Regex.Replace(text, @"\s+", "");
  14. //Remove commented out tags
  15. text = Regex.Replace(text, @"(<!--)(.*?)(-->)", "");
  16. return text;
  17. }

When the concatenated string already exists, we can build the tree by recursion using regular expression in order to get the elements along with values.

  1. private void GetTag(string tag, Node n)
  2. {
  3. //If tag doesn't contain close tag '</' then the end of the branch is reached
  4. if (!tag.Contains("</")) { return; };
  5. //Get the complete XML node along with value
  6. foreach (Match match in Regex.Matches(tag, @"<([^>]+)>(.*?)</\1>"))
  7. {
  8. Node node = new Node();
  9. //Next level of the tree
  10. node.level = n.level + 1;
  11. //Name of the tag
  12. node.tag = match.Groups[1].ToString();
  13. //Value of the tag (maybe null)
  14. if (!match.Groups[2].Value.Contains("/")) node.value = match.Groups[2].Value;
  15. n.nodes.Add(node);
  16. //Next index on the current level
  17. node.index = n.nodes.Count;
  18. //Recursion
  19. GetTag(match.Groups[2].Value, node);
  20. }
  21. }

When we have a Node object which is a tree containing the values, we can traverse on the tree and create the class.

The procedure of the instantiation is simple but not so straight forward in many cases, like arrays or interfaces.

DIFFICULTIES

The class may contains arrays.

Instantiation of an array is easy if we know its length.

  1. Array.CreateInstance( typeof(Int32), 5 );

The class may contain interface. Obviously, we can't instantiate an interface so we have to find the behind class that implements it.

How to check if the property is a list or array

We can do it differently like this one but we have to be careful because it will be true for String type as well.

  1. typeof(IEnumerable).IsAssignableFrom(propInfo.PropertyType)

How to get the generic type of a list items

Obviously, the type of the PropertyInfo is List and not the type of its item. We can't instantiate a string object with the activator because it doesn't have parameterless constructor

  1. Activator.CreateInstance(propInfo.PropertyType) //string à Error!

SOLUTION

The traversal method completed with explanations in the code,

  1. private void Traverse(Node n, object o, Helper helperObj)
  2. {
  3. //All properties of the curent object
  4. PropertyInfo[] propInfos = o.GetType().GetProperties();
  5. PropertyInfo propInfo = null;
  6. //If there is no more children then the end of the branch is reached
  7. if (n.nodes.Count == 0)
  8. {
  9. return;
  10. }
  11. //Looping on the child nodes of the current object
  12. foreach (Node node in n.nodes)
  13. {
  14. object instance = null;
  15. object obj = null;
  16. //Get the actual property of the current node
  17. //Maybe it's name defined in an XmlArrayItemAttribute or an XmlElementAttribute
  18. propInfo = Array.Exists(propInfos, x => x.Name == node.tag) ?
  19. propInfos.Where(x => x.Name == node.tag).First() :
  20. Array.Exists(propInfos, y =>
  21. {
  22. var attribs = y.GetCustomAttributes(false);
  23. return Array.Exists(attribs, z =>
  24. {
  25. Type attribType = z.GetType();
  26. if (attribType == typeof(XmlArrayItemAttribute))
  27. {
  28. return ((XmlArrayItemAttribute)z).ElementName == node.tag;
  29. }
  30. else if (attribType == typeof(XmlElementAttribute))
  31. {
  32. return ((XmlElementAttribute)z).ElementName == node.tag;
  33. }
  34. return false;
  35. });
  36. }) ?
  37. propInfos.First(v => v.GetCustomAttributes(false).First(m =>
  38. (m.GetType() == typeof(XmlArrayItemAttribute) &&
  39. ((XmlArrayItemAttribute)m).ElementName == node.tag) ||
  40. (m.GetType() == typeof(XmlElementAttribute) &&
  41. ((XmlElementAttribute)m).ElementName == node.tag)) != null) :
  42. null;
  43. if (propInfo != null)
  44. {
  45. //If the property is an IEnumerable and not a String
  46. //then create a generic list with the type of the property
  47. if (typeof(IEnumerable).IsAssignableFrom(propInfo.PropertyType) &&
  48. (propInfo.PropertyType.Name != "String"))
  49. {
  50. var listType = typeof(List<>);
  51. //Get the proper generic type
  52. var genericType = propInfo.PropertyType.IsArray ?
  53. listType.MakeGenericType(Type.GetType(propInfo.PropertyType.FullName.Replace("[]", ""))) :
  54. listType.MakeGenericType(Type.GetType(propInfo.PropertyType.FullName).GetGenericArguments()[0]);
  55. //Create the generic instance
  56. instance = Activator.CreateInstance(genericType);
  57. //Create the concrete item instance
  58. var itemInstance = (propInfo.PropertyType.Name == "String[]" ||
  59. instance.GetType().GetGenericArguments().Single() == typeof(string)) ?
  60. new String(new Char[] { ' ' }) //If string
  61. : instance.GetType().GetGenericArguments().Single().IsInterface ? //If interface
  62. Activator.CreateInstance(Assembly.GetExecutingAssembly().GetTypes().First
  63. (x => x.GetInterfaces().Contains(instance.GetType().GetGenericArguments().Single()) && x.GetConstructor(Type.EmptyTypes) != null)) :
  64. Activator.CreateInstance(instance.GetType().GetGenericArguments().Single()); //If other
  65. //If the object already exists then don't need new instance
  66. object temp = propInfo.GetValue(o, null);
  67. //If the collection already exists
  68. if (temp != null)
  69. {
  70. //and it's an array then fill the created temporary list
  71. //with it's exisiting items
  72. if (propInfo.PropertyType.IsArray)
  73. {
  74. foreach (object item in ((Array)temp))
  75. {
  76. instance.GetType().GetMethod("Add").Invoke(instance, new[] { item });
  77. }
  78. }
  79. //or set the created instance to the existing one
  80. else
  81. instance = temp;
  82. }
  83. //Add the created item instance to the generic list
  84. instance.GetType().GetMethod("Add").Invoke(instance, new[] { itemInstance });
  85. helperObj.HelperObject = instance;
  86. //If the property is an array then loop through
  87. //the list and fill a new array with the items
  88. if (propInfo.PropertyType.IsArray)
  89. {
  90. //Initialze the length of the array in advance
  91. //by the number of the current child nodes.
  92. //The first one will be set and the others are null for the time being
  93. var CountofItem = node.nodes.Count;
  94. var array = Array.CreateInstance(itemInstance.GetType(), CountofItem);
  95. for (int j = 0; j < ((IList)instance).Count; j++)
  96. {
  97. array.SetValue(((IList)instance)[j], j);
  98. }
  99. //Finally set the array to the instance
  100. instance = array;
  101. helperObj.ItemIndex = 0;
  102. helperObj.HelperObject = instance;
  103. }
  104. obj = itemInstance;
  105. }
  106. else //If property is NOT Enumerable
  107. {
  108. //If the property is value type
  109. if (propInfo.PropertyType.IsValueType)
  110. {
  111. TypeConverter tc = TypeDescriptor.GetConverter(propInfo.PropertyType);
  112. instance = tc.ConvertFromString(node.value);
  113. } //or it's a String
  114. else if (propInfo.PropertyType.Name == "String")
  115. {
  116. instance = new String(node.value.ToCharArray());
  117. } //or it's a class
  118. else if (propInfo.PropertyType.IsClass)
  119. {
  120. instance = Activator.CreateInstance(propInfo.PropertyType);
  121. } //or it's an interface
  122. else if (propInfo.PropertyType.IsInterface)
  123. {
  124. //Find the implementation of the interface
  125. //Get all executing assemblies
  126. Type[] types = Assembly.GetExecutingAssembly().GetTypes();
  127. //Get the implemented type
  128. Type implementedType = types.First(x =>
  129. x.GetInterfaces().Contains(propInfo.PropertyType) &&
  130. x.GetConstructor(Type.EmptyTypes) != null);
  131. instance = Activator.CreateInstance(implementedType);
  132. }
  133. obj = instance;
  134. }
  135. //Finally set the property with the created object
  136. propInfo.SetValue(o, instance, null);
  137. }
  138. else
  139. {
  140. //If the current node index is 1 then it is the first item of the collection
  141. //therefore we use the existing first item and don't create a newer one
  142. if (node.index==1)
  143. {
  144. obj = o;
  145. } //Otherwise create a new object
  146. else if (o.GetType().Name != "String")
  147. obj = Activator.CreateInstance(o.GetType());
  148. else //Except if it is a String object
  149. obj = new String(new char[] { });
  150. //If the Node has value then set the value of the object
  151. if (node.value != null)
  152. {
  153. if (obj.GetType().IsValueType)
  154. {
  155. TypeConverter tc = TypeDescriptor.GetConverter(obj.GetType());
  156. obj = tc.ConvertFromString(node.value);
  157. } //or it's a String
  158. else if (obj.GetType().Name == "String")
  159. {
  160. obj = new String(node.value.ToCharArray());
  161. }
  162. }
  163. //If the HelperObject is a List or Array
  164. if (helperObj.HelperObject != null)
  165. {
  166. //If the collection is an array
  167. if (helperObj.HelperObject.GetType().IsArray)
  168. {
  169. //Set the created new object to the next item by using
  170. //the itemIndex of the Helper
  171. if (((Array)helperObj.HelperObject).GetValue(0).GetType() == obj.GetType())
  172. {
  173. ((Array)helperObj.HelperObject).SetValue(obj, helperObj.ItemIndex);
  174. helperObj.ItemIndex++;
  175. }
  176. else
  177. throw new Exception("Not possible to set this <" +node.tag+ "> into the class object!");
  178. }
  179. //If the collection is a generic list
  180. else if (typeof(IEnumerable).IsAssignableFrom(helperObj.HelperObject.GetType()) &&
  181. (helperObj.HelperObject.GetType().Name != "String"))
  182. {
  183. //If the current node index is 1 then it is the first item of the collection
  184. //therefore we set this item with the object
  185. if (node.index == 1)
  186. {
  187. ((IList)helperObj.HelperObject)[0] = obj;
  188. } //Otherwise add as a new item
  189. else
  190. ((IList)helperObj.HelperObject).Add(obj);
  191. }
  192. }
  193. //If the HelperObject is null then there isn't object
  194. //for the current node
  195. else
  196. throw new Exception("Not possible to set this <" +node.tag+ "> into the class object!");
  197. }
  198. //Recursion
  199. Traverse(node, obj, helperObj);
  200. }
  201. }
A helper object is needed in order to follow the filling of the collection object. The ItemIndex is for actual property in the object and HelperObject is the actual object itself.
  1. class Helper
  2. {
  3. public int ItemIndex { get; set; }
  4. public object HelperObject { get; set; }
  5. }
Finall, the caller which is the constructor of the DeSerializator class:

The object parameter will be the class object that we want to instantiate.

  1. public Deserializator(string path, object obj)
  2. {
  3. if (File.Exists(path))
  4. {
  5. string[] lines = File.ReadAllLines(path);
  6. XDocument doc = XDocument.Parse(String.Join("", lines));
  7. string text = GetTextFromXml(doc);
  8. Node n = new Node() { tag = "root", index = 0 };
  9. GetTag(text, n);
  10. Helper helperObj = new Helper();
  11. Traverse(n.nodes[0], obj, helperObj);
  12. }
  13. }

CONCLUSION

Although, this is an unnecessary solution as DeSerializator already exists for XML, but it was an interesting challenge to implement. It revealed some special cases of reflection and traversal. It can be interesting to implement other possible property types as well. Please let me know if you have any constructive ideas.