Thursday 2 February 2012

Deserializing XML to Dynamic Object in C#

Little bit background about this blog entry – most of the applications today are driven by (BIG) settings file (usually in XML format) , this is absolutely fine and will work as expected. Let’s look at a real world scenario of XML driven applications – suppose user wants an extra feature which is controlled based on an entry in the XML settings file that means we have to handle that extra settings parameter in our application.

Not a BIG deal; we could do it in following ways
#1) if you are using a custom XML Settings parser then change it (add XPATH/read node attribute etc.) - Old fashion approach :-(.
Or
#2) Create a new XML schema from XML and use XSD.exe to generate classes from the schema (if it simple change like adding attribute etc. then you can modify the auto generated class file directly ) ;And deserialize the XML to an object and use this object everywhere.
Or
#3) LinqToXML
etc...

Since I came from strong OOPS background I personally prefer to use everything as objects, so obviously my choice is second one (of course I won’t generate class file from schema for every change!!! )
These patterns are good still I was looking something DYNAMIC until DotNet 4.0 released with ‘dynamic’ type support.

Let’s look at an example with ‘Dynamic’ type(these are samples so keeping it simple as possible :-) to explain the idea)

In version 1.0 of settings (config) file [application will read this settings file and set the button text – simple yaaaa ],

<?xml version="1.0" encoding="utf-8" ?>
<UISettings >
<Button Text="Show Data" />
</UISettings>


//Code snippet
dynamic settingObject = SomeXmlToDynamicObjectParser.Parse(“Settings.xml”);
Then real beauty,
MyForm.btnData.Text = settingObject.Button.Text;

Now in version 2.0 of settings ,suppose we have added one more button setting (i.e. width)

<? version="1.0" encoding="utf-8" ?>
<?UISettings>
<Button Text="Show Data" Width="50" />
</UISettings>


//Code snippet
dynamic settingObject = SomeXmlToDynamicObjectParser.Parse(“Settings.xml”);
MyForm.btnData.Text = settingObject.Button.Text;
MyForm.btnData.Width = double.Parse(settingObject.Button.Width); (So simple yaaa!!!!)

I started to love this pattern and applied in many BIG xml driven production applications.(This is just my view if you have a different opinion or better approach please share in comment section).

You can find the implementation of ‘SomeXmlToDynamicObjectParserHere.

Happy Coding Days ….

18 comments:

  1. YAY! I've not tried this yet, but intend to over the weekend. Looks awesome, thanks very much.

    Mike

    ps// I hope it works with repeat items and node nesting too right?

    ReplyDelete
  2. Hm, bummer. Doesn't work for anything but the simple case. See the following NUnit test for verification of failure on both the "repeat items" and "nested nodes" tests.

    Might you be able to update to handle this too? It's not really useful without that.

    Thanks,
    Mike
    using System.Collections.Generic;
    using System.Text;
    using NUnit.Framework;

    namespace Utility.Tests
    {
    [TestFixture]
    public class DynamicXmlReaderTest
    {
    [Test]
    public void CanParseSingleNode()
    {
    var xml = ToXml(new List\<string>
    {
    "\<?xml version=\"1.0\" encoding=\"utf-8\" ?>",
    "\<root>",
    " \<TopNode Text=\"Data\" Width=\"50\" />",
    "\</root>"
    });

    var dobj = DynamicXmlReader.Parse(xml);

    Assert.AreEqual("Data", dobj.TopNode.Text);
    Assert.AreEqual("50", dobj.TopNode.Width);
    }

    [Test]
    public void CanParseNestedNode()
    {
    var xml = ToXml(new List\<string>
    {
    "\<?xml version=\"1.0\" encoding=\"utf-8\" ?>",
    "\<root>",
    " \<TopNode>",
    " \<NestedNode Text=\"Data\" Width=\"50\" />",
    " \</TopNode>",
    "\</root>"
    });

    var dobj = DynamicXmlReader.Parse(xml);

    Assert.AreEqual("Data", dobj.TopNode.NestedNode.Text);
    Assert.AreEqual("50", dobj.TopNode.NestedNode.Width);
    }

    [Test]
    public void CanParseRepeatedNodes()
    {
    var xml = ToXml(new List\<string>
    {
    "\<?xml version=\"1.0\" encoding=\"utf-8\" ?>",
    "\<root>",
    " \<TopNode Text=\"Data 1\" Width=\"50\" />",
    " \<TopNode Text=\"Data 2\" Width=\"50\" />",
    "\</root>"
    });

    var dobj = DynamicXmlReader.Parse(xml);

    var i = 0;
    foreach (var node in dobj.TopNode)
    {
    i++;
    Assert.AreEqual("Data " + i, node.Text);
    Assert.AreEqual("50", node.Width);
    }
    }

    private static string ToXml(IEnumerable\<string> lines)
    {
    var stringBuilder = new StringBuilder();
    foreach (var line in lines)
    stringBuilder.AppendLine(line);
    return stringBuilder.ToString();
    }
    }
    }

    ReplyDelete
    Replies
    1. I have modified the simple implementation of abstract class'SomeXmlToDynamicObjectParser' to meet your requirments.

      modified functions are,

      public override DynamicMetaObject BindGetMember(GetMemberBinder binder)
      {
      Func attributeNameMatchPredicate = a => String.Equals(a.Name.LocalName, binder.Name, StringComparison.CurrentCultureIgnoreCase);
      Func elementNameMatchPredicate = e => String.Equals(e.Name.LocalName, binder.Name,StringComparison.CurrentCultureIgnoreCase);

      if (Value is XDocument)
      {
      var asDoc = Value as XDocument;
      return
      asDoc.Descendants().Count(elementNameMatchPredicate) == 1
      ? DynamicXmlReaderForSingleElement(asDoc.Descendants().Single(elementNameMatchPredicate))
      : ArrayOfDynamicsByContext(asDoc.Descendants().Where(elementNameMatchPredicate));
      }
      if (Value is XElement)
      {
      //if attribute, bind it to a string
      //if text with no children, bind it to a string
      //if neither, but it exists as an aggregate, bind it to the aggregate
      var asElem = Value as XElement;

      if (asElem.Attributes().Any(attributeNameMatchPredicate))
      return StringForFirstAttribute(asElem, attributeNameMatchPredicate);

      if (ElementContainsAtLeastOneMatchingElement(elementNameMatchPredicate, asElem))
      {
      var subElems = asElem.Elements().Where(elementNameMatchPredicate);

      if (subElems.Count() == 1)
      return subElems.First().HasElements || subElems.First().HasAttributes?
      DynamicXmlReaderForSingleElement(subElems.First()): StringForValue(subElems.First());

      return ArrayOfDynamicsByContext(subElems);
      }
      }
      throw new NotImplementedException();
      }

      private static DynamicMetaObject ArrayOfDynamicsByContext(IEnumerable subElems)
      {
      var xmlReaderArray = subElems.Select(e => e.HasElements || e.HasAttributes ? (dynamic)new DynamicXmlReader(e) : (dynamic)e.Value).ToArray();
      var target = Expression.Constant(xmlReaderArray);
      return new DynamicMetaObject(target, BindingRestrictions.GetTypeRestriction(target, typeof(dynamic[])), xmlReaderArray);
      }

      Delete
    2. Test case,


      namespace WpfApp1.Test
      {
      [TestFixture]
      public class XmlToDynamicTest
      {
      [Test]
      public void CanParseSingleNode()
      {
      var xml = ToXml(new List
      {
      "",
      "",
      "",
      ""
      });

      dynamic settingObject = DynamicXMLReader.DynamicXmlReader.Parse(xml);

      var dobj = DynamicXmlReader.Parse(xml);

      Assert.AreEqual("Data", dobj.TopNode.Text);
      Assert.AreEqual("50", dobj.TopNode.Width);
      }

      [Test]
      public void CanParseNestedNode()
      {
      var xml = ToXml(new List
      {
      "",
      "",
      "",
      "",
      "",
      ""
      });

      var dobj = DynamicXmlReader.Parse(xml);

      dynamic actual = dobj.TopNode.NestedNode.Text;

      Assert.AreEqual("Data", actual);
      Assert.AreEqual("50", dobj.TopNode.NestedNode.Width);
      }

      [Test]
      public void CanParseRepeatedNodes()
      {
      var xml = ToXml(new List
      {
      "",
      "",
      "",
      "",
      ""
      });

      var dobj = DynamicXmlReader.Parse(xml);

      Assert.AreEqual(2, dobj.TopNode.Length);
      Assert.AreEqual(1, dobj.TopNode[1].Text);
      Assert.AreEqual(2, dobj.TopNode[2].Text);

      }

      private static string ToXml(IEnumerable lines)
      {
      var stringBuilder = new StringBuilder();
      foreach (var line in lines)
      stringBuilder.AppendLine(line);
      return stringBuilder.ToString();
      }
      }
      }

      Delete
  3. Hey, cool, thanks for doing that! The "descendents" case now works, but not the "repeat items". Think that's doable?

    Also, btw, I had to add some type definition to your code to get it to compile for me, he's my equivalent for what you've pasted in. (Also, btw, you need to html encode the tests to get the strings/xml to post up here).

    public override DynamicMetaObject BindGetMember(GetMemberBinder binder)
    {
    Func attributeNameMatchPredicate = a => String.Equals(a.Name.LocalName, binder.Name, StringComparison.CurrentCultureIgnoreCase);
    Func elementNameMatchPredicate = e => String.Equals(e.Name.LocalName, binder.Name, StringComparison.CurrentCultureIgnoreCase);

    if (Value is XDocument)
    return BindXDoc(elementNameMatchPredicate);
    if (Value is XElement)
    return BindXElement(elementNameMatchPredicate, attributeNameMatchPredicate);
    throw new NotImplementedException();
    }

    private DynamicMetaObject BindXDoc(Func elementNameMatchPredicate)
    {
    var asDoc = Value as XDocument;
    return
    asDoc.Descendants().Count(elementNameMatchPredicate) == 1
    ? DynamicXmlReaderForSingleElement(asDoc.Descendants().Single(elementNameMatchPredicate))
    : ArrayOfDynamicsByContext(asDoc.Descendants().Where(elementNameMatchPredicate));
    }

    private DynamicMetaObject BindXElement(Func elementNameMatchPredicate, Func attributeNameMatchPredicate)
    {
    //if attribute, bind it to a string
    //if text with no children, bind it to a string
    //if neither, but it exists as an aggregate, bind it to the aggregate
    var asElem = Value as XElement;

    if (asElem.Attributes().Any(attributeNameMatchPredicate))
    return StringForFirstAttribute(asElem, attributeNameMatchPredicate);

    if (ElementContainsAtLeastOneMatchingElement(elementNameMatchPredicate, asElem))
    {
    var subElems = asElem.Elements().Where(elementNameMatchPredicate);

    if (subElems.Count() == 1)
    {
    var firstSubElement = subElems.First();
    return (firstSubElement.HasElements || firstSubElement.HasAttributes)
    ? DynamicXmlReaderForSingleElement(firstSubElement)
    : StringForValue(firstSubElement);
    }

    return ArrayOfDynamicsByContext(subElems);
    }
    throw new NotImplementedException();
    }

    private static DynamicMetaObject ArrayOfDynamicsByContext(IEnumerable subElems)
    {
    var xmlReaderArray = subElems.Select(e => e.HasElements || e.HasAttributes ? (dynamic)new DynamicXmlReader(e) : (dynamic)e.Value).ToArray();
    var target = Expression.Constant(xmlReaderArray);
    return new DynamicMetaObject(target, BindingRestrictions.GetTypeRestriction(target, typeof(dynamic[])), xmlReaderArray);
    }

    ReplyDelete
  4. Oh shoot - I see what happened (to both of us): need html encode for the prod code too!

    public override DynamicMetaObject BindGetMember(GetMemberBinder binder)
    {
    Func<XAttribute, bool> attributeNameMatchPredicate = a => String.Equals(a.Name.LocalName, binder.Name, StringComparison.CurrentCultureIgnoreCase);
    Func<XElement, bool> elementNameMatchPredicate = e => String.Equals(e.Name.LocalName, binder.Name, StringComparison.CurrentCultureIgnoreCase);

    if (Value is XDocument)
    return BindXDoc(elementNameMatchPredicate);
    if (Value is XElement)
    return BindXElement(elementNameMatchPredicate, attributeNameMatchPredicate);
    throw new NotImplementedException();
    }

    private DynamicMetaObject BindXDoc(Func<XElement, bool> elementNameMatchPredicate)
    {
    var asDoc = Value as XDocument;
    return
    asDoc.Descendants().Count(elementNameMatchPredicate) == 1
    ? DynamicXmlReaderForSingleElement(asDoc.Descendants().Single(elementNameMatchPredicate))
    : ArrayOfDynamicsByContext(asDoc.Descendants().Where(elementNameMatchPredicate));
    }

    private DynamicMetaObject BindXElement(Func<XElement, bool> elementNameMatchPredicate, Func<XAttribute, bool> attributeNameMatchPredicate)
    {
    //if attribute, bind it to a string
    //if text with no children, bind it to a string
    //if neither, but it exists as an aggregate, bind it to the aggregate
    var asElem = Value as XElement;

    if (asElem.Attributes().Any(attributeNameMatchPredicate))
    return StringForFirstAttribute(asElem, attributeNameMatchPredicate);

    if (ElementContainsAtLeastOneMatchingElement(elementNameMatchPredicate, asElem))
    {
    var subElems = asElem.Elements().Where(elementNameMatchPredicate);

    if (subElems.Count() == 1)
    {
    var firstSubElement = subElems.First();
    return (firstSubElement.HasElements || firstSubElement.HasAttributes)
    ? DynamicXmlReaderForSingleElement(firstSubElement)
    : StringForValue(firstSubElement);
    }

    return ArrayOfDynamicsByContext(subElems);
    }
    throw new NotImplementedException();
    }

    private static DynamicMetaObject ArrayOfDynamicsByContext(IEnumerable<XElement> subElems)
    {
    var xmlReaderArray = subElems.Select(e => e.HasElements || e.HasAttributes ? (dynamic)new DynamicXmlReader(e) : (dynamic)e.Value).ToArray();
    var target = Expression.Constant(xmlReaderArray);
    return new DynamicMetaObject(target, BindingRestrictions.GetTypeRestriction(target, typeof(dynamic[])), xmlReaderArray);
    }

    ReplyDelete
    Replies
    1. I pasted without proper formatting in a hurry :-) , I had to remove the namespace import statement to deal with number of characters restriction.

      in b/w can you check last test case with same xml posted by you, it worked for me (pasted below my passed test case).

      var dobj = DynamicXmlReader.Parse(xml);

      Assert.AreEqual(2, dobj.TopNode.Length);
      Assert.AreEqual(1, dobj.TopNode[1].Text);
      Assert.AreEqual(2, dobj.TopNode[2].Text);

      Delete
    2. Hm. Very strange. The "explicitly ask by index" way, like you have above, indeed works. But, doing what one would think is the same thing within a loop does not:

      var dobj = DynamicXmlReader.Parse(xml);

      Assert.AreEqual(2, dobj.TopNode.Length);
      Assert.AreEqual("Data 0", dobj.TopNode[0].Text);
      Assert.AreEqual("Data 1", dobj.TopNode[1].Text);

      for (var i = 0; i < dobj.TopNode.Length; i++)
      {
      var node = dobj.TopNode[i];
      var text = node.Text;
      var width = node.Width;
      Assert.AreEqual("Data " + i, text);
      Assert.AreEqual("50" + i, width);
      }

      In the loop, it seems to get "stuck" on the first one you ask for, no matter what the value of 'i' is. IE, you get the same node on the second trip thru the loop as first.

      Same problem when trying a foreach:

      var i = 0;
      foreach (var node in dobj.TopNode)
      {
      var text = node.Text;
      var width = node.Width;
      Assert.AreEqual("Data " + i, text);
      Assert.AreEqual("50" + i, width);
      i++;
      }

      How can this be? I'm stumped, and I really need the loop to work as my use case will be "unknown amount of 'TopNodes'".

      Thanks,
      Mike

      Delete
    3. Mike,

      This implementation basically returns the first matching element for a given expression,when you iterate through TopNode.
      Can you modify it? (or you have to wait until weekend :-) for me)

      Delete
    4. I may, but may not, quite busy myself. If I get to it, I'll certainly post it back here.

      Mike

      Delete
    5. Are you able to post your updated version that supports iterating through the collection?

      Delete
  5. hello .. may i know on how to code the xor not gate in mathlab.. so that the output will be given as [1 0 1 0] ..thx a lot

    ReplyDelete
    Replies
    1. Please post your comment under appropriate blog entry ,deserializing XML to dynamic object in dotnet has nothing to do with Matlab...

      Delete
  6. hi thanks a ton for ur gui tutorial, i need to kno more abt coding the gui since i need it for my thesis, if u can help asap..i shall b highly grateful.
    thanks

    ReplyDelete
    Replies
    1. Sure,shoot ur questions.
      Please don't ask me to do complete thesis :-).

      Delete
  7. I wrapped the DynamicXmlReader in a config section handler, and, viola, dynamic config sections. Sweet! Thanks for sharing.

    ReplyDelete
    Replies
    1. That's cool ; Can you share sample code ?

      Delete
    2. This comment has been removed by the author.

      Delete