Monday, August 19, 2013

C# .NET : Serialize object to XML node using custom attribute

This serialization (if this is what I call it) of object to XML is based on an idea to serialize an object to XML node (not an XML document entirely). The second thing is that it only includes selected properties of the object (typically public properties).
This can be done using custom attributes applied to each selected property of a C# object.

I found out that in C# .NET (with .NET framework 4), the order of the declaration of properties inside a class is the same as the list of properties returned when you call typeof(T).GetProperties() where T is your custom class. So we can create XML nodes in the same order as we declare the class properties.

This type of serialization is useful in IEnumerable objects where each object must be included in an XML document.

To begin with, we shall create a custom attribute inheriting System.Attribute class. Our XmlTaggingAttribute attribute can be used to target the class and its properties, disallowing multiple declarations (one property cannot be duplicated as a child node in its respective parent node which is the class).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Class, AllowMultiple = false)]
public class XmlTaggingAttribute : Attribute
{
  /// <summary>
  /// Gets the name of the Xml node or node attribute
  /// </summary>
  public string Name { get; private set; }

  /// <summary>
  /// Gets the value whether the XmlTaggingAttribute.Name is an attribute name of a Xml node
  /// </summary>
  public bool IsAttribute { get; private set; }

  /// <summary>
  /// Gets the name of the node with the attribute name of XmlTaggingAttribute.Name
  /// </summary>
  public string Node { get; private set; }

  /// <summary>
  /// Initializes a new instance of XmlTaggingAttribute with the name of the Xml node
  /// </summary>
  /// <param name="name">The name of the Xml node</param>
  public XmlTaggingAttribute(string name)
  {
    Name = name;
    Node = "";
    IsAttribute = false;
  }

  /// <summary>
  /// Initializes a new instance of XmlTaggingAttribute with the attribute of a previously declared XmlTaggingAttribute.Name node name
  /// </summary>
  /// <param name="attributeName">The name of the attribute</param>
  /// <param name="nodeName">The name of the node to be attributed. Must be declared in previous class property XmlTaggingAttribute</param>
  public XmlTaggingAttribute(string attributeName, string nodeName)
    : this(attributeName)
  {
    IsAttribute = true;
    Node = nodeName;
  }
}

Now you can use XmlTaggingAttribute like the code below. For instance, the ReportItem class when serialized will create an XML node named "report" with all attributed properties as child nodes.

One thing worth noting is that XML is text-based document. If you have an attributed property in your class which is of type T, where T is one of your other classes, you either (1) make sure you override the ToString() base method to get the expected string value of the said property; or (2) modify the CreateXmlNode<T>(T obj) method below to meet expected results. When you're working with several classes you might want to choose option 1 to keep the generality of the CreateXmlNode method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
[XmlTagging("report")]
public class ReportItem
{
  DateTime _reportDate;

  // Other non-attributed public or private properties

  [XmlTagging("id")]
  public int ID { get; private set; }

  [XmlTagging("comment")]
  public string Comment { get; set; }

  [XmlTagging("reporter")]
  public string NameOfReporter { get; set; }

  [XmlTagging("fixed", "reporter")]
  public bool IsFixed { get; set; }

  [XmlTagging("date-reported")]
  public string ReportDate
  {
      get { return _reportDate.ToString("yyyy-MM-dd HH:mm"); }
      private set { _reportDate = DateTime.Parse(value); }
  }

  public ReportItem(int id)
  {
    ID = id;
    //Your constructor here
  }
}

To serialize the object of a class into an XML node, the code below is the key. You can put this method in your helper class. You can then get the XML node string by calling the ToString() method of the returned XElement object.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public static XElement CreateXmlNode<T>(T obj)
{
  XElement xml_element;

  object[] xml_tag_attributes = typeof(T).GetCustomAttributes(typeof(XmlTaggingAttribute), false);

  if (xml_tag_attributes.Length > 0)
    xml_element = new XElement(((XmlTaggingAttribute)xml_tag_attributes[0]).Name);
  else
    xml_element = new XElement("item"); // When class has no attribute, default to "item"

  PropertyInfo[] prop_infos = typeof(T).GetProperties();
  Dictionary<string, XAttribute> saved_xattributes = new Dictionary<string, XAttribute>();

  foreach (var pinfo in prop_infos)
  {
    string property_value;
    try { property_value = pinfo.GetValue(obj, null).ToString(); }
    catch { property_value = "Error getting string value"; }

    object[] prop_attributes = pinfo.GetCustomAttributes(typeof(XmlTaggingAttribute), false);
    if (prop_attributes.Length == 0)
      continue;

    XmlTaggingAttribute attrib = (XmlTaggingAttribute)prop_attributes[0];

    if (attrib.IsAttribute)
    {
      // Create XML attributes and save
      saved_xattributes.Add(attrib.Node, new XAttribute(attrib.Name, property_value));
    }
    else
    {
      xml_element.Add(new XElement(attrib.Name, property_value));
    }
  }

  // Add saved XML attributes to appropriate XElements
  foreach (string key in saved_xattributes.Keys)
  {
    IEnumerable<XElement> elements = from el in xml_element.Elements()
                                     where el.Name == key
                                     select el;

    if (elements.Count<XElement>() > 0) // Attribute for child node
      elements.ElementAt<XElement>(0).Add(saved_xattributes[key]);
    else if (xml_element.Name == key) // Attribute for parent xml node
      xml_element.Add(saved_xattributes[key]);
    else
      throw new Exception("XmlTaggingAttribute name '" + key + "' is not declared or defined.");
  }

  return xml_element;
}

A serialized ReportItem would look similar to this:
<report>
  <id>1</id>
  <comment>My comment</comment>
  <reporter fixed="true">Hardistones</reporter>
  <date-reported>2014-01-01 00:00</date-reported>
</report>