Extending LinqToXml for better XML interaction

by Rob Galanakis on 25/07/2010

Many tools programmers or tech artists work with XML on a daily basis.  For .NET developers, there are three ways to work with XML.  You can use XML serialization (built in or custom), the System.Xml namespace, or LinqToXml (System.Xml.Linq).  I usually advocate XML serialization (with custom serialization routines!), but there are times when that isn’t practical.

In our case, we had a few hand-maintaned xml files that supported our animation system, which was undergoing a heavy rewrite by the gameplay team. They don’t maintain the animation tools code, though, so the serializing logic we wrote for the xml could easily break. Even worse, they could break our entire system logic if they make a logical change! So it was really impossible to maintain a fully logical system with serializing classes, as we always desire.

So we wrote ‘utility’ methods to do what we needed to do, and only what we needed to do, using System.Xml and XPath expressions. However, as anyone that has had to decipher tools written this way will tell you, this is a maintenance nightmare. You need to understand all the nuance of the underlying code to write, and each time you write something you can easily break the xml, since you’re working really, truly with raw xml. So we developed these ‘LinqToXml’ extensions to create a very light logical layer between underlying XML, representative classes, and procedural (utility) code.

Let’s take a look at an example XML file (vastly simplified):

<metadata>
  <actions>
    <input name="Stance">
      <value name="Standing" />
      <value name="Sitting" />
    </input>
    <input name="Weapon">
      <value name="Saber" />
      <value name="Gun" />
    </input>
  </inputs>
  <actions>
    <action name="Attack" blendInTime="0.1" blendOutTime="0.2">
      <path value="action|attack" />
      <input name="Stance" value="Standing" />
      <input name="Weapon" value="Saber" />
    </action>
    <action name="Heal" blendInTime="0.05" blendOutTime="0.03">
      <path value="action|heal" />
      <input name="Stance" value="Sitting" />
    </action>
  </actions>
</metadata>

Looking at the file, the reasons for a full logical system are obvious- validating the ‘input’ element on an action, for example.  But like I said, we needed to trade what’s right for what will work (we wrote the ‘right’ system for the xml 6 months ago, and the metadata system was changed right after that, invalidating some amount of work and resulting in a stagnation of the logical and serializing system for metadata).  Below is an example of the classes we would create to represent that XML.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;

namespace FluentXml
{
    public class MetadataAction : XElement
    {
        public string ActionName
        {
            get { return this.Attribute("name").Value; }
            set { this.Attribute("name").Value = value; }
        }
        public decimal BlendInTime
        {
            get { return decimal.Parse(this.Attribute("blendInTime").Value); }
            set { this.Attribute("blendInTime").Value = value.ToString(); }
        }
        public string NetworkPath
        {
            get { return this.Element("path").Attribute("value").Value; }
            set { this.Element("path").Attribute("value").Value = value; }
        }
        public Dictionary Inputs
        {
            get { return this.Elements("input").ToDictionary(input => input.Attribute("name").Value, input => input.Attribute("value").Value); }
            set
            {
                this.Elements("input").Remove();
                foreach (KeyValuePair kvp in value)
                {
                    this.Add(new XElement("input", new XAttribute("name", kvp.Key), new XAttribute("value", kvp.Value)));
                }
            }
        }

        public MetadataAction(XElement other)
            : base(other)
        {
        }
    }
    public class MetadataInput : XElement
    {
        public string InputName
        {
            get { return this.Attribute("name").Value; }
            set { this.Attribute("name").SetValue(value); }
        }
        public IEnumerable InputValues
        {
            get
            {
                foreach (XElement inputVal in this.Elements("value"))
                {
                    yield return inputVal.Attribute("name").Value;
                }
            }
            set
            {
                this.Elements("value").Remove();
                foreach (string valname in value)
                {
                    this.Add(new XElement("value", new XAttribute("name", valname)));
                }
            }
        }

        public MetadataInput(XElement other)
            : base(other)
        {
        }
    }

    public class MetadataFile : XElement
    {
        public MetadataFile(XElement other)
            : base(other)
        {
        }

        public IEnumerable Inputs()
        {
            foreach (XElement child in this.Element("inputs").Elements().ToList())
            {
                MetadataInput result = child as MetadataInput;
                if (result == null)
                {
                    result = new MetadataInput(child);
                    child.ReplaceWith(result);
                }
                yield return result;
            }
        }
        public IEnumerable Actions()
        {
            foreach (XElement child in this.Element("actions").Elements().ToList())
            {
                MetadataAction result = child as MetadataAction;
                if (result == null)
                {
                    result = new MetadataAction(child);
                    child.ReplaceWith(result);
                }
                yield return result;
            }
        }
        public MetadataAction Action(string actionName)
        {
            foreach (MetadataAction action in this.Actions())
            {
                if (action.ActionName.Equals(actionName))
                {
                    return action;
                }
            }
            return null;
        }
        public MetadataInput Input(string inputName)
        {
            foreach (MetadataInput item in this.Inputs())
            {
                if (item.InputName.Equals(inputName))
                {
                    return item;
                }
            }
            return null;
        }
    }
}

This is really just ‘glue’ code that works with the underlying XML directly, but in a much more easily maintained format; instead of difficult-to-decipher XPath expressions, the classes make an easy-to-understand interface to the xml. In addition, we can write very fluent and concise utility expressions, for querying or processing, such as:

MetadataFile aam = new MetadataFile(XElement.Load("XMLFile1.xml"));
IEnumerable allActionNames = aam.Actions().Select(a => a.ActionName);
aam.Input("Weapon").InputValues = new string[] { "test1", "test2" };
aam.Actions().Where(a => a.BlendInTime < 0.5m).ToList().ForEach(a => a.BlendInTime = 0.5m);
var inputsAndActionsUsing = aam.Inputs().Select(input => new { Input = input, Actions = from a in aam.Actions() where a.Inputs.ContainsKey(input.InputName) select a });

This is a bit of an advanced topic but for people who are used to LINQ (and especially Linq2Xml), it should make sense, and is very, very powerful. We replaced all of our XPath utility methods for Metadata with this system in a couple hours, and boy was it worth it. In a future post I’ll go over a few improvements we can make, such as using a base ‘MetadataElement’ class for validation and better reuse, improving our IEnumerable yield methods, and improving MetadataFile by making it an XDocument to preserve top-level comments and provide more intuitive use.

rob.galanakis@gmail.com

There are 2 comments in this article:

  1. 30/08/2010Varuna Bamunusinghe says:

    This is a quite nice method to work with XML files. We’ve been using LinqToXml in one of our project. But, without using LINQ functionality that much, we used XPath queries.

    It seems your method is lot easier to maintain and to work with. Btw, did you check performance against executing XPath queries?

  2. 4/09/2010Rob Galanakis says:

    I haven’t but there are some discussions regarding its usage:
    http://stackoverflow.com/questions/856215/linq-to-xml-vs-dom
    http://stackoverflow.com/questions/182976

    We’ve been expanding the use of this system over the past several weeks and, after some frustrating initial startup debugging, it has been well worth it. The main ‘bug’ was being caused by some deferred execution issues which we still haven’t figured out (we have just been putting ‘ToArray’ when querying one of the problematic IEnumerables). I still need to get a good repro and further debug it.

Write a comment:


+ four = 8

Switch to our mobile site