Click here to Skip to main content
15,885,985 members
Articles / Programming Languages / C# 4.0

Creating a dynamic object from XML using ExpandoObject

Rate me:
Please Sign up or sign in to vote.
4.67/5 (8 votes)
25 Oct 2012CPOL5 min read 96.8K   2.6K   29   18
Creating a dynamic object from XML using ExpandoObject.

Introduction 

Parsing XML can be a tedious, labor intensive process. Attempts to encapsulate XML into data object can improve the process. Depending on the problem space, creating and maintaining the data objects can be just as cumbersome, depending on the structure of the XML and how often the XML changes.

Background

In a project I was working recently, a third party produced XML files for import into the application I was writing. Each XML file held a batch of transactions. Each transaction consisted of a form and supporting documentation. Each file could contain any number of forms and the forms were of different types (each form type had different elements). Each form could have any number of supporting documents and the documents were of different types (each document type had different elements). When parsing, it wasn’t until you were knee deep into the XML that you really knew what information you had to be able to create a corresponding data object for the element. At that point, why bother creating a data object? I could just as easily have started processing the raw XML. There had to be a better way.

While searching for a better way, I found the .NET 4.0 System.Dynamic.ExpandoObjec<code><code>t.ExpandoObject class. ExpandoObject instances have the ability to have members added and removed at runtime. With a little more research, I found two examples of where an ExpandoObject was being used to represent an XML document. That is, while parsing, an ExpandoObject instance is created and each XML element is added as a member of the ExpandoObject. For example, this XML:

XML
<car>
   <make>Ford</make>
   <model>Explorer</model>
   <color>Silver</color>
</car>

... essentially becomes an instance of the following dynamic “virtual” class:

C#
public class Car
{
    public string make;
    public string model;
    public string color;
}

I found two examples of building an ExpandoObject from XML. Both were close, but neither was what I needed. Both made assumptions about the XML being parsed. One example assumed that attributes could be added to the XML tags to change the behavior of the creation of the ExpandoObject. I was a consumer of the XML. I didn’t have control over what the publisher provided. The other example assumed that list nodes always preceded non-list nodes. That was not the case in my source XML. In short, both potential solutions made assumptions about the XML being read that were bad assumptions in my problem space. I needed a generic and more robust solution.

Start With What Works Best

Of the two examples if found to convert XML to an ExpandoObject instance, the one that was closest to being what I needed was posted on ITDevSpace.com. I make no attempt to hide that my solution originated from using this code. In fact, looking closely at both, there are only a few differences. I give 90% credit to them for what was, for me, a 90% solution. This article describes my 10%.

Requirement 1

The original class did a really good job of handling lists of objects. It, however, assumed that the list was the first child element in the XML. For example, according to the ITDevSpace.com solution, this XML is “good”:

XML
<car>
  <owners>
    <owner>Bob Jones</owner>
    <owner>Betty Jones</owner>
  </owners>
  <make>Ford</make>
  <model>Explorer</model>
  <color>Silver</color>
</car>

This XML is “bad”:

XML
<car>
  <make>Ford</make>
  <model>Explorer</model>
  <color>Silver</color>
  <owners>
    <owner>Bob Jones</owner>
    <owner>Betty Jones</owner>
  </owners>
</car>

This is unfortunate, as they are syntactically identical XML. My XML looked like the “bad” XML, so that wasn’t going to work. I needed a way to generically handle lists, regardless of their placement in the XML.

Requirement 2

Although the original class did a good job of handling lists, there was a problem if the list only contained on item. For example, if parsing the “good” XML above, the original would produce this class:

C#
public class Car
{ 
  public string make; 
  public string model;
  public string color;
  public dynamic owners; // Containing a List<object> member with the <owner> elements
}

However, if there were only one owner. That is, this XML:

XML
<car>
  <make>Ford</make>
  <model>Explorer</model>
  <color>Silver</color>
  <owners>
    <owner>Bob Jones</owner>
  </owners>
</car>

Would produce this class:

C#
public class Car
{
  public string make;
  public string model;
  public string color;
  public dynamic owner; // Containing a dynamic member of a single <owner> element
}

Compared to the output of the “good” XML above, when using the Car.owners.owner member, I wouldn’t know if I was looking at a List<> or a single object without investigation through reflection. I wanted (i.e., not “needed”) a standard interface for items that I knew were going to be lists.

Requirement 3

Attributes! What about attributes? The original class processed intermediate nodes without regard for their attributes. For example, this XML:

XML
<car>
  <make>Ford</make>
  <model>Explorer</model>
  <color>Silver</color>
  <owners type="Current">
    <owner>Bob Jones</owner>
  </owners>
</car>

... would lose the data contained in the type attribute. I needed all attributes! I needed something like this result:

C#
public class Car
{
  public string make;
  public string model;
  public string color;
  public dynamic owners;
}
public class owners
{
  public string type;
  public <List>dynamic owner;  // A representation of dynamic owners in the Car class
};

The Code

Here is the most recent version. This class is also included in the sample project provided with this article for download.  

C#
public static class ExpandoObjectHelper
{
    private static List<string> KnownLists;
    public static void Parse(dynamic parent, XElement node, List<string> knownLists = null)
    {
        if (knownLists != null)
        {
            KnownLists = knownLists;
        }
        IEnumerable<xelement> sorted = from XElement elt in node.Elements() 
              orderby node.Elements(elt.Name.LocalName).Count() descending select elt;

        if (node.HasElements)
        {
            int nodeCount = node.Elements(sorted.First().Name.LocalName).Count();
            bool foundNode = false;
            if (KnownLists != null && KnownLists.Count > 0)
            {
                foundNode = (from XElement el in node.Elements() 
                  where KnownLists.Contains(el.Name.LocalName) select el).Count() > 0;
            }

            if (nodeCount>1 || foundNode==true)
            {
                // At least one of the child elements is a list
                var item = new ExpandoObject();
                List<dynamic> list = null;
                string elementName = string.Empty;
                foreach (var element in sorted)
                {
                    if (element.Name.LocalName != elementName)
                    {
                        list = new List<dynamic>;
                        elementName = elementName.LocalName;
                    }
                    if (element.HasElements ||
                        (KnownLists != null && KnownLists.Contains(element.Name.LocalName)))
                    {
                        Parse(list, element);
                        AddProperty(item, element.Name.LocalName, list);
                    }
                    else
                    {
                        Parse(item, element);
                    }
                }

                foreach (var attribute in node.Attributes())
                {
                    AddProperty(item, attribute.Name.ToString(), attribute.Value.Trim());
                }

                AddProperty(parent, node.Name.ToString(), item);
            }
            else
            {
                var item = new ExpandoObject();

                foreach (var attribute in node.Attributes())
                {
                    AddProperty(item, attribute.Name.ToString(), attribute.Value.Trim());
                }

                //element
                foreach (var element in sorted)
                {
                    Parse(item, element);
                }
                AddProperty(parent, node.Name.ToString(), item);
            }
        }
        else
        {
            AddProperty(parent, node.Name.ToString(), node.Value.Trim());
        }
    }

    private static void AddProperty(dynamic parent, string name, object value)
    {
        if (parent is List<dynamic>)
        {
            (parent as List<dynamic>).Add(value);
        }
        else
        {
            (parent as IDictionary<string, object>)[name] = value;
        }
    }
} 

Usage

The following section of code is taken directly from the sample project provided with this article for download. The first line of code loads an XML file from disk. The next section creates a list of XML node names that are known to contain lists of items. Once the XML is loaded and the list of nodes is prepared, creation of the dynamic object can begin by calling ExpandoObjectHelper.Parse. Once the parsing is complete, the parsed data may then be used. 

C#
// Load an XML document.
var xmlDocument = XDocument.Load("test.xml");

// Convert the XML document in to a dynamic C# object.
List<string> listNodes = new List<string>() {"owners"};
dynamic xmlContent = new ExpandoObject();
ExpandoObjectHelper.Parse(xmlContent, xmlDocument.Root, listNodes);

Console.WriteLine("Make: {0}", xmlContent.car.make); 

Known Issues

Everything has a drawback, right? I’ve noticed that compared to my previous XML parsing method, using an ExpandoObject is slow. I haven’t run benchmarks to know how much slower, but it is noticeable. I know part of the difference is that my original parsing method used XPath. I was able to target exactly the nodes I wanted before I realized that the data files were all different. When creating an ExpandoObject, the code has to slog through the entire file – every element and every attribute. If the file is large, this can take some time. Since my project is a scheduled job that runs overnight without a human watching it, I don’t expect this to be a problem for me. Your mileage may vary.

References  

History 

25 October 2012

  • Bug fix. An issue with multiple lists of objects at the same node level was discovered (see forum post below) whereby the properties of one list would be mixed with the properties of the sibling list. 

19 September 2012 

  • Reformatted mangled text, including fixing code broken during reformatting.
  • Updated the usage code.
  • Added sample project.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Chief Technology Officer Bytewerx
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralMy vote of 5 Pin
stixoffire12-Oct-18 5:55
stixoffire12-Oct-18 5:55 
Really like the fact that not only did you utilize the code from the other website giving credit, but you fixed the code to be really flexible AND posted your code here and made a note on the original website pointing back to here!!!! Like to give you 50 stars for that... I think you are in my hall of fame ..
QuestionWhy use knownList and foundnode? It seems like nodeCount is enough. Pin
AsinChen21-Jan-15 23:14
AsinChen21-Jan-15 23:14 
AnswerRe: Why use knownList and foundnode? It seems like nodeCount is enough. Pin
Kevin Yochum6-Nov-18 5:29
Kevin Yochum6-Nov-18 5:29 
QuestionTag confusion? Pin
steinard11-Mar-14 13:25
steinard11-Mar-14 13:25 
QuestionElements with attributes Pin
Tri Q Tran10-Nov-13 18:02
Tri Q Tran10-Nov-13 18:02 
BugLocalName should be used Pin
jackssss12331-Nov-13 20:03
jackssss12331-Nov-13 20:03 
QuestionReplace a portion of the dynamic object? Pin
jtproj27-Sep-13 19:41
jtproj27-Sep-13 19:41 
AnswerRe: Replace a portion of the dynamic object? Pin
Kevin Yochum30-Sep-13 2:54
Kevin Yochum30-Sep-13 2:54 
QuestionPossible error? Pin
Terrance Addison7-Jan-13 7:05
Terrance Addison7-Jan-13 7:05 
AnswerRe: Possible error? Pin
Kevin Yochum19-Feb-13 2:38
Kevin Yochum19-Feb-13 2:38 
GeneralRe: Possible error? Pin
jtproj27-Sep-13 19:43
jtproj27-Sep-13 19:43 
GeneralRe: Possible error? Pin
Kevin Yochum30-Sep-13 2:56
Kevin Yochum30-Sep-13 2:56 
QuestionEverything is added to everything Pin
amulroney24-Oct-12 10:54
amulroney24-Oct-12 10:54 
AnswerRe: Everything is added to everything Pin
amulroney24-Oct-12 10:55
amulroney24-Oct-12 10:55 
GeneralRe: Everything is added to everything Pin
Kevin Yochum25-Oct-12 1:15
Kevin Yochum25-Oct-12 1:15 
GeneralRe: Everything is added to everything Pin
Kevin Yochum25-Oct-12 1:43
Kevin Yochum25-Oct-12 1:43 
GeneralMy vote of 5 Pin
Kanasz Robert19-Sep-12 4:30
professionalKanasz Robert19-Sep-12 4:30 
GeneralRe: My vote of 5 Pin
Kevin Yochum19-Sep-12 5:17
Kevin Yochum19-Sep-12 5:17 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.