Sunday, May 02, 2010

C#: A Simple Pseudo-Serializable Generic <string, string> Dictionary

Today I was trying to find an easy way to convert XML string to a C# generic <string, string> dictionary and the other way around. I wasn't able to find anything easy and simple enough, so I decided to write my own code to do the job.

I knew the first option would be XML Serializer, but there are a couple of issues that I don’t appreciate using it. First, I want to keep the xml file really simple, I don't like a full-blown XML file, as the more complex the XML file is, the easier people make mistakes when making changes to the XML file. Second, I want to be able to customize the dictionary root node's name, item node's name, and also key/value attribute's name, which doesn't seem to be viable using XML serializer. Lastly, C# generic dictionary is not serializable out-of-box, so it has to be custom serialization code like this one.

To be more specific, what I want is actually very simple, I want my code to convert the following XML string to a C# generic dictionary, and other way around the other time.
<settings>
  <setting key="setting1" value="value1" />
  <setting key="setting2" value="value2" />
</settings>
Here is the class that I implemented based on C# Dictionary class.
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Schema;

public class SimpleDictionary : Dictionary<string, string>
{
    /**
     * A Simple pseudo-serializable generic <string, string> dictionary
     * @author Daniel Cai, http://danielcai.blogspot.com/
     */
    private readonly string xsdMarkup = @"
<xs:schema id='dictionary' xmlns='' xmlns:xs='http://www.w3.org/2001/XMLSchema'>
  <xs:element name='{0}'>
    <xs:complexType>
      <xs:choice minOccurs='0' maxOccurs='unbounded'>
        <xs:element name='{1}'>
          <xs:complexType>
            <xs:attribute name='{2}' type='xs:string' />
            <xs:attribute name='{3}' type='xs:string' />
          </xs:complexType>
        </xs:element>
      </xs:choice>
    </xs:complexType>
  </xs:element>
</xs:schema>";

    private string _rootNodeName;
    private string _itemNodeName;
    private string _keyAttributeName;
    private string _valueAttributeName;

    public SimpleDictionary()
        : this("dictionary", "item", "key", "value")
    {

    }

    public SimpleDictionary(string rootNodeName, string itemNodeName, string keyAttributeName, string valueAttributeName)
    {
        _rootNodeName = rootNodeName;
        _itemNodeName = itemNodeName;
        _keyAttributeName = keyAttributeName;
        _valueAttributeName = valueAttributeName;
        xsdMarkup = string.Format(xsdMarkup, rootNodeName, itemNodeName, keyAttributeName, valueAttributeName);
    }

    public void FromXml(string xml)
    {
        Clear();

        XDocument xdoc = XDocument.Parse(xml);

        ValidateXml(xdoc);

        var dictionaryItemQuery = from element in xdoc.Root.Elements()
                       where
                           element.Name == _itemNodeName &&
                           element.Attributes().Count() == 2 &&
                           element.FirstAttribute.Name == _keyAttributeName &&
                           element.LastAttribute.Name == _valueAttributeName

                       select element;

        foreach (XElement keyValuePair in dictionaryItemQuery)
        {
            Add(keyValuePair.Attribute(_keyAttributeName).Value,
                keyValuePair.Attribute(_valueAttributeName).Value);
        }
    }

    public string ToXml()
    {
        XElement xElement = new XElement(_rootNodeName,
                                         from key in this.Keys
                                         select new XElement(_itemNodeName,
                                                             new XAttribute(_keyAttributeName, key),
                                                             new XAttribute(_valueAttributeName, this[key]))
            );
        return xElement.ToString();
    }

    private void ValidateXml(XDocument xdoc)
    {
        bool isValid = true;
        string errorMessage = string.Empty;

        XmlSchemaSet schemas = new XmlSchemaSet();
        schemas.Add("", XmlReader.Create(new StringReader(xsdMarkup)));

        xdoc.Validate(schemas, (sender, e) =>
        {
            errorMessage = string.Format("Validation error: {0}", e.Message);
            isValid = false;
        }, true);

        if (!isValid)
        {
            throw new XmlSchemaValidationException(errorMessage);
        }
    }
}
To convert a XML string into a generic <string, string> dictionary, you can write your code as below:
SimpleDictionary simpleDictionary = new SimpleDictionary("settings", "setting", "key", "value");
simpleDictionary.FromXml(@"
<settings>
  <setting key='key1' value='value1' />
  <setting key='key2' value='value2' />
</settings>");

// Your dictionary is now ready for use. 
To convert a <string, string> dictionary into XML string, the code should be something like this:
SimpleDictionary simpleDictionary = new SimpleDictionary();
simpleDictionary.Add("key1", "value1");
simpleDictionary.Add("key2", "value2");

string xml = simpleDictionary.ToXml();
Console.WriteLine(xml);

/* Output:
<dictionary>
  <item key='key1' value='value1' />
  <item key='key2' value='value2' />
</dictionary>"
*/
It's worth noting that the class has two constructors, the default one will use a default set of names (dictionary as the root node name, item as the dictionary item node name, key as the item's key attribute name, and value as the item's value attribute name). If you want the nodes and attributes to be called differently, you may call the other constructor by providing specific names.

Hope this helps.

No comments:

Post a Comment