Friday, January 15, 2016

Work with extendable xml files in C# using dynamic objects with custom deserialization

Sometime we need to work with xml files which schema is not static and can be changed in runtime. If schema is predefined we may define POC class, mark it with necessary Xml.. attributes (XmlRoot, XmlAttribute, XmlArray, etc.), deserialize data and process xml file in strongly typed way. However in order to work with extendable xml files in this way such approach can’t be used because in compile-time we don’t know what exact data will be in xml file.

However we still can work with such xml files in strongly typed way using C# dynamic objects with custom deserialization. Let’s consider the following example: suppose that we have the following xml defining some List entity (it is used from Sharepoint world, but exact area is not important here. It may be xml defining any entity):

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <List Title="MyList">
   3:   <Fields>
   4:     <Field Name="Title" />
   5:     <Field Name="LinkTitle" />
   6:   </Fields>
   7: </List>

In order to deserialize it and work with it in C# we create the following POC class:

   1: [XmlRoot("List")]
   2: public class ListEntity
   3: {
   4:    [XmlAttribute("Title")]
   5:     public string Title;
   6:  
   7:     [XmlArray("Fields")]
   8:     [XmlArrayItem("Field")]
   9:     public List<FieldEntity> Fields;
  10: }

In this POC class we defined properties for Title and Fields and mapped them on appropriate xml tags. FieldEntity class is shown below:

   1: public class FieldEntity
   2: {
   3:     [XmlAttribute("Name")]
   4:     public string Name;
   5:  
   6:     [XmlText]
   7:     public string FieldXml;
   8: }

Now suppose that we need to add new data to xml file. If we would know what exact data will be added we of course could just add new properties to our POC class and use them with new xml. However we don’t know yet what exact data will be added, so we need to use more tricky approach with dynamic object. Let’s define Extension class which inherits DynamicObject and implement custom deserialization for it:

   1: public class ExtensionEntity : DynamicObject, IXmlSerializable
   2: {
   3:     private XElement el;
   4:  
   5:     // parameterless constructor is called by xml serializer
   6:     public ExtensionEntity()
   7:     {
   8:     }
   9:  
  10:     // constructor with param is called by TryGetMember method
  11:     public ExtensionEntity(XElement el)
  12:     {
  13:         this.el = el;
  14:     }
  15:  
  16:     public XmlSchema GetSchema()
  17:     {
  18:         return null;
  19:     }
  20:  
  21:     public void ReadXml(XmlReader reader)
  22:     {
  23:         if (this.el != null)
  24:         {
  25:             // already initialized
  26:             return;
  27:         }
  28:  
  29:         using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(reader.ReadOuterXml())))
  30:         {
  31:             using (var textReader = new StreamReader(ms))
  32:             {
  33:                 this.el = XElement.Load(textReader);
  34:             }
  35:         }
  36:     }
  37:  
  38:     public void WriteXml(XmlWriter writer)
  39:     {
  40:         // don't need to implement serialization
  41:     }
  42:  
  43:     public override bool TryGetMember(GetMemberBinder binder, out object result)
  44:     {
  45:         result = null;
  46:  
  47:         var att = this.el.Attribute(binder.Name);
  48:         if (att != null)
  49:         {
  50:             result = att.Value;
  51:             return true;
  52:         }
  53:  
  54:         var nodes = this.el.Elements(binder.Name);
  55:         if (nodes.Count() > 1)
  56:         {
  57:             result = nodes.Select(n => new ExtensionEntity(n)).ToList();
  58:             return true;
  59:         }
  60:  
  61:         if (string.Compare(binder.Name, "XmlText", false) == 0)
  62:         {
  63:             result = this.el.Value;
  64:             return true;
  65:         }
  66:         else
  67:         {
  68:             var node = this.el.Element(binder.Name);
  69:             if (node != null)
  70:             {
  71:                 result = new ExtensionEntity(node);
  72:                 return true;
  73:             }
  74:         }
  75:  
  76:         return true;
  77:     }
  78: }

Custom deserialization (implementation of IXmlSerializable interface) is needed for making it possible to use any xml schema. And dynamic object is needed for possibility to work with deserialized objects in strongly typed way. Basically what happens here is that during deserialization we store whole XElement for our object in internal class variable and then in TryGetMember method (which comes from DynamicObject) map requested member name on xml tags, attributes and content.

Ok, now when we have dynamic object with custom xml deserialization we may extend our list POC with it:

   1: [XmlRoot("List")]
   2: public class ListEntity
   3: {
   4:     [XmlAttribute("Title")]
   5:     public string Title;
   6:  
   7:     [XmlArray("Fields")]
   8:     [XmlArrayItem("Field")]
   9:     public List<FieldEntity> Fields;
  10:  
  11:     [XmlArray("Extensions")]
  12:     [XmlArrayItem("Extension")]
  13:     public List<ExtensionEntity> Extensions;
  14: }

Alternatively we could work without ListEntity at all by using only Extension object for anything, but I would like to show situation when we have some legacy code which already uses POC classes and which we need to extend. Having such C# part we may now add new data to xml and work with it in strongly typed way:

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <List Title="MyList">
   3:   <Fields>
   4:     <Field Name="Title" />
   5:     <Field Name="LinkTitle" />
   6:   </Fields>
   7:   <Extensions>
   8:     <Extension>
   9:       <UserCustomAction Location="CommandUI.Ribbon"
  10: Sequence="5" Title="Test">
  11:         <![CDATA[<CommandUIExtension>
  12:             <CommandUIDefinitions>
  13:               <CommandUIDefinition
  14: Location="Ribbon.Tasks.Actions.Controls._children">
  15:                 <Button
  16:             Id="Ribbon.ListItem.Manage.Test"
  17:             Alt="Test"
  18:             Sequence="10001"
  19:             Command="Test"
  20:             Image32by32="/_layouts/images/rtrsendtoicon.png"
  21:             Image16by16="/_layouts/images/copy16.gif"
  22:             LabelText="Test"
  23:             TemplateAlias="o1" />
  24:               </CommandUIDefinition>
  25:             </CommandUIDefinitions>
  26:             <CommandUIHandlers>
  27:               <CommandUIHandler
  28:         Command="Test"
  29:         CommandAction="javascript: Test();" />
  30:             </CommandUIHandlers>
  31:           </CommandUIExtension>]]>
  32:       </UserCustomAction>
  33:     </Extension>
  34:     <Extension>
  35:       <Items>
  36:         <Item>
  37:           <Fields>
  38:             <Field Name="Title">test11</Field>
  39:             <Field Name="LinkTitle">test12</Field>
  40:           </Fields>
  41:         </Item>
  42:         <Item>
  43:           <Fields>
  44:             <Field Name="Title">test21</Field>
  45:             <Field Name="LinkTitle">test22</Field>
  46:           </Fields>
  47:         </Item>
  48:       </Items>
  49:     </Extension>
  50:   </Extensions>
  51: </List>

Here we added 2 extensions with own schema: one for UserCustomAction, another for Items. With our POC class we may work with it like this:

   1: ListEntity list = null;
   2: using (var reader = new XmlTextReader("data.xml"))
   3: {
   4:     var serializer = new XmlSerializer(typeof (ListEntity));
   5:     list = (ListEntity) serializer.Deserialize(reader);
   6:     reader.Close();
   7: }
   8:  
   9: dynamic extension = list.Extensions[0];
  10: dynamic customAction = extension.UserCustomAction;
  11: Console.WriteLine("Location: {0}", customAction.Location);
  12: Console.WriteLine("Sequence: {0}", customAction.Sequence);
  13: Console.WriteLine("Title: {0}", customAction.Title);
  14: Console.WriteLine("Content: {0}", customAction.XmlText);
  15:  
  16: extension = list.Extensions[1];
  17: foreach (dynamic item in extension.Items.Item)
  18: {
  19:     Console.WriteLine("Item");
  20:     foreach (dynamic field in item.Fields.Field)
  21:     {
  22:         Console.WriteLine("  Field name: {0}, value: {1}", field.Name, field.XmlText);
  23:     }
  24: }

As you can see this approach allows us to work both with single objects and with collections of objects in strongly typed way. Hope that this information will help someone.

No comments:

Post a Comment