Tuesday, February 28, 2012

Loquacious XML builder

Let's try to make use of loquacious interface patterns I've shown in the previous post to build something simple but useful - an API to construct an arbitrary XML document with simple, readable and elegant piece of C# code. If you find it helpful, fell free to use it - for convenience, I've put it on GitHub.

We'll start with an utility class wrapping the standard cumbersome XML API. Nothing really interesting here, just few methods to add attributes, nested elements or inner content to a given XmlNode object.

internal class NodeBuilder
{
private readonly XmlDocument _doc;
private readonly XmlNode _node;

public NodeBuilder(XmlDocument doc, XmlNode node)
{
_doc = doc;
_node = node;
}

public void SetAttribute(string name, string value)
{
var attribute = _doc.CreateAttribute(name);
attribute.Value = value;
_node.Attributes.Append(attribute);
}

public XmlNode AddNode(string name)
{
var newNode = _doc.CreateElement(name);
_node.AppendChild(newNode);
return newNode;
}

public void AddContent(string content)
{
_node.AppendChild(_doc.CreateTextNode(content));
}
}

Now we'll create an entry point for our loquacious XML API - it'll be a static method that creates an instance of XmlDocument, uses NodeBuilder to initialize the document with a root element, runs a loquacious Action<INode> for the root node and finally, returns the XmlDocument content as a string.

public static class Xml
{
public static string Node(string name, Action<INode> action)
{
using (var stringWriter = new StringWriter())
{
var doc = new XmlDocument();
var root = new NodeBuilder(doc, doc).AddNode(name);
action(new NodeImpl(doc, root));

doc.WriteTo(new XmlTextWriter(stringWriter));
return stringWriter.ToString();
}
}
}

What do we need in the INode interface, used within Action<T> parameter? As always with loquacious interfaces, it should resemble one level of our object structure - an XML node in this case. So we'll have two simple methods to add an attribute and an inner content and another Action<INode>-parametrized method to add a new node at the next level in the XML structure.

public interface INode
{
void Attr(string name, string value);
void Node(string name, Action<INode> action);
void Content(string content);
}

The implementation of INode interface is pretty straightforward, following the patterns I've described previously.

internal class NodeImpl : INode
{
private readonly XmlDocument _doc;
private readonly NodeBuilder _nb;

public NodeImpl(XmlDocument doc, XmlNode node, string name)
{
_doc = doc;
_nb = new NodeBuilder(doc, node, name);
}

public void Attr(string name, string value)
{
_nb.SetAttribute(name, value);
}

public void Node(string name, Action<INode> action)
{
action(new NodeImpl(_doc, _nb.AddNode(name)));
}

public void Content(string content)
{
_nb.AddContent(content);
}
}

And that's it! We can use this three-class implementation to create any XML we need. For example, here is the code that builds a simple NuGet package manifest:

var package = Xml.Node("package", x => x.Node("metadata", m =>
{
m.Attr("xmlns", "http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd");
m.Node("id", id => id.Content("Foo.Bar"));
m.Node("version", version => version.Content("1.2.3"));
m.Node("authors", authors => authors.Content("NOtherDev"));
m.Node("description", desc => desc.Content("An example"));
m.Node("dependencies", deps =>
{
deps.Node("dependency", d =>
{
d.Attr("id", "First.Dependency");
d.Attr("version", "3.2.1");
});
deps.Node("dependency", d =>
{
d.Attr("id", "Another.Dependency");
d.Attr("version", "3.2.1");
});
});
}));

Of course this is the simple case - we could construct XML like this using StringBuilder pretty easily, too. But the flexibility this kind of API gives is very convenient for more complex scenarios. I'm going to show something more complicated next time.

3 comments:

  1. There are errors in the code. NodeImpl's Constructor and Node method. :)

    ReplyDelete
    Replies
    1. Hah, I've missed one refactoring done in the last minute :) Fixed, thanks.

      Delete

Note: Only a member of this blog may post a comment.