Wednesday 7 September 2011

Dynamic XML

One of the newcomer in C# 4.0 - great newcomer is new keyword 'dynamic'. Dynamic variables are run-time binded which is very helpful many times e.g. it simplify access to COM objects. I don't want to explain in more details about 'dynamic'. I want to show useful usage of this. For more details about dynamic keyword I send here - http://msdn.microsoft.com/en-us/library/dd264741.aspx.
Recently, I must have read complex xml and I thought that maybe I can use dynamic and I mustn't read xml by LINQ or other technics.
Take this xml as example:
<root>
<shop>
<book>
<author>John Savacki</author>
<title>E.G.Title</title>
<price>20.50</price>
</book>
<book>
<author>Tom Curly</author>
<title>E.G.Title 2</title>
</book>
</shop>
</root>
I wanted to read it like that:
dynamic xml = ......
string author = xml.shop.book[0].author

Is not pretty?
It is - but unfortunately such library doesn't exist.
So, I was finding existing solutions but none of these was sufficient for me so I decided to write own DynamicXML class and I wanna share with you my solution and by the way show usage of new 'dynamic' feature.

Cause I will be use exactly this base class - DynamicObject. It is base class for new type which we can bind to dynamic variable and thanks for this we can capture every references to our variable like properties, like methods etc.
So this is header of my class:

public class DynamicXml : DynamicObject, IEnumerable
{
}

I leverage IEnumerable interace to give possibility of going through collection but about this will be later.

To read XML I override only two methods of our base class:
TryGetMember and TryGetIndex.
As you expected first method is call when client write sth like this dynamicXML.Something and second when client type sth like this dynamicXML.Something[i].
Simply Idea is that: when client demand property(node) we find if actual node include this. If yes then we check if this node include children or not. If include children then we should return new DynamicXML with changed current node, else we should return string with value. When actual node doesn't include finding property we can return null - but here appear problem. What with optional fields. Look on our sample xml - when client type xml.shop.book[1].price we should return null - of course, it is good, but what if price can also include optional fields? eg. 'discount'.
Now, when client call xml.shop.book[1].price.discount he get NullPointerException. We must avoid this but we haven't possibility to check if client type sth after actual checking property(in TryGetMember), so I decided to return always new DynamicXml object and carry additional method like eval() which return finally result.
So to get author from second book we must type xml.shop.book[1].author.eval().
Next option is when we retrive more than one element eg. we type xml.shop.book.eval() - this should return DynamicXml with list of book elements. To attend to this all option I use list of XElement and and string variable of actual value.
Our class look like this now:

public class DynamicXml : DynamicObject, IEnumerable
{
private string value = null;

public IList Elements { get; set; }
}

Now we need some constructors - public and private. Public for client to create our DynamicXml from few source like filename or from stream. Private will be used for returning new DynamicXml in reply of property demand(it simple recursion).

public class DynamicXml : DynamicObject, IEnumerable
{
private string value = null;

public IList Elements { get; set; }

public DynamicXml(Stream input) { Elements = new List { XDocument.Load(input).Root }; }

public DynamicXml(TextReader input) { Elements = new
List { XDocument.Load(input).Root }; }

public DynamicXml(string input) { Elements = new
List { XDocument.Load(input).Root }; }

private DynamicXml() { Elements = null; }

private DynamicXml(XElement input) { Elements = new
List { input }; }

private DynamicXml(IEnumerable<XElement> input) { Elements =
new List(input); }

private DynamicXml CreateValueDynamicXml(string value)
{
DynamicXml result = new DynamicXml();
result.value = value;

return result;
}
}

Last method is sth like pseudo-constructor - I write it as method to avoid conflict with previous string-parameter constructor.

So, almost last step is attend to client demands for properties. Firstly, I comment TryGetValue:

public override bool TryGetMember(GetMemberBinder binder, out
object result)
{
if (Elements != null)
{
var elements = Elements.Descendants().Where(q =>
q.Name == binder.Name);

if (elements.Count() == 1)
{
if (elements.First().HasElements)
{
result = new DynamicXml(elements.First());
}
else
{
result = CreateValueDynamicXml(elements.First().Value);
}
}
else if (elements.Count() > 1)
{
result = new DynamicXml(elements);
}
else
{
result =
CreateValueDynamicXml(null);
return false;
}

return true;
}
else
{
result = CreateValueDynamicXml(null);;
return false;
}
}


If list of actual elements is empty we return next DynamicXml object with null value - it's our protection for optional nodes as I mentioned earlier. If list has elements then we find every elements of this list of node name equal name of demanding properties which is carry through GetMemberBinder object. If we find one element so we check if this element has children if yes then we return new DynamicXml with Elements list including one found element(next we can retrive child elements from this). If found element doesn't include any child we know that this is 'leaf' so we return new DynamicXml with empty Elements list but with proper value. If we found more than one matching element we return new DynamicXml with Elements filled of all previously found elements. If we didn't find any matching elements is the same to case of null Elements property.

public override bool TryGetIndex(GetIndexBinder binder,
object[] indexes, out object result)
{
int index = 0;
if (int.TryParse(indexes[0].ToString(), out index) && Elements != null)
{
if (Elements[index].HasElements)
{
result = new DynamicXml(Elements[index]);
}
else
{
result = CreateValueDynamicXml(Elements[index].Value);
}
return true;
}
else
{
result =
CreateValueDynamicXml(null);
return false;
}
}

This method is always calling afterTryGetMember so must keeping result from TryGetMember to service this method. Obviously, we keep our previous result in Elements list, so now we check if this list is not null and if we can get back number from index element, cause it not must be number, we can type sth like this: dynamicVar.property["key"]. Indexes is table cause we can demand more than one dimension e.g.dynamicVar.property[0,1,"key"]. I assume that we demand only number-one dimension index, so if we has not null Elements and we can retrive index we return new DynamicXml with value or with new actual node.

Eval() method look like this:

public string Eval()
{
return string.IsNullOrEmpty(value) ? null : value;
}

I think that it's so easy and don't need any comment.

Last method I attend to is GetEnumerator() as implementation of IEnumerable interface:
public IEnumerator GetEnumerator()
{
foreach (var element in Elements)
{
if (element.HasElements)
{
yield return new DynamicXml(element);
}
else
{
yield return CreateValueDynamicXml(element.Value);
}
}
}

I wrote this method to service demand like this:
foreach (var book in xml.shop.book.eval())
{
Console.WriteLine(book.author.eval());
}

I think that above method is also easy to understand. Exception may be yield keyword. To understand this I suggest this article:

Below I present whole ready to use code of DynamicXml class:

public class DynamicXml : DynamicObject, IEnumerable
{
private string value = null;

public IList Elements { get; set; }

public DynamicXml(Stream input) { Elements = new
List { XDocument.Load(input).Root }; }

public DynamicXml(TextReader input) { Elements = new
List { XDocument.Load(input).Root }; }

public DynamicXml(string input) { Elements = new
List { XDocument.Load(input).Root }; }

private DynamicXml() { Elements = null; }

private DynamicXml(XElement input) { Elements = new
List { input }; }

private DynamicXml(IEnumerable<XElement> input) { Elements =
new List(input); }

private DynamicXml CreateValueDynamicXml(string value)
{
DynamicXml result = new DynamicXml();
result.value = value;

return result;
}

public override bool TryGetMember(GetMemberBinder binder, out
object result)
{
if (Elements != null)
{
var elements = Elements.Descendants().Where(q =>
q.Name == binder.Name);

if (elements.Count() == 1)
{
if (elements.First().HasElements)
{
result = new DynamicXml(elements.First());
}
else
{
result = CreateValueDynamicXml(elements.First().Value);
}
}
else if (elements.Count() > 1)
{
result = new DynamicXml(elements);
}
else
{
result =
CreateValueDynamicXml(null);
return false;
}

return true;
}
else
{
result =
CreateValueDynamicXml(null);
return false;
}
}

public override bool TryGetIndex(GetIndexBinder binder,
object[] indexes, out object result)
{
int index = 0;
if (int.TryParse(indexes[0].ToString(), out index) &&
Elements != null)
{
if (Elements[index].HasElements)
{
result = new DynamicXml(Elements[index]);
}
else
{
result = CreateValueDynamicXml(Elements[index].Value);
}
return true;
}
else
{
result =
CreateValueDynamicXml(null)
;
return false;
}
}

public IEnumerator GetEnumerator()
{
foreach (var element in Elements)
{
if (element.HasElements)
{
yield return new DynamicXml(element);
}
else
{
yield return CreateValueDynamicXml(element.Value);
}
}
}

public string Eval()
{
return string.IsNullOrEmpty(value) ? null : value;
}
}

At the end I wanna sorry for my english and for not good formatted but tool for writing posts here is very nagging by writing code.

I hope that this class can be helpfully for you.

No comments:

Post a Comment