Support dynamic fields with NHibernate and .NET 4.0

time to read 8 min | 1403 words

A common theme in many application is the need to support custom / dynamic fields. In other words, the system admin may decide that the Customer needs to have a few additional fields that aren’t part of the mainline development.

In general, there are a few ways of handling that:

  • DateField1, DateField2, StringField1, StringField2, etc, etc – And heaven helps if you need more than 2 string fields.
  • Entity Attribute Value – store everything in a n EAV model, which basically means that you are going to have tables named: Tables, Rows, Attributes and Values.
  • Dynamically updating the schema.

In general, I would recommend anyone that needs dynamic fields to work with a data storage solution that supports it (like RavenDB Smile, for example). But sometimes you have to use a relational database, and NHibernate has some really sweet solution.

First, let us consider versioning. We are going to move all of the user’s custom fields to its own table. So we will have the Customers table and Customers_Extensions table. That way we are free to modify our own entity however we like it. Next, we want to allow nice syntax both for querying and for using it, even if there is custom code written against our code.

We can do it using the following code:

public class Customer
{
    private readonly IDictionary attributes = new Hashtable();
    public virtual int Id { get; set; }
    public virtual string Name { get; set; }

    public virtual dynamic Attributes
    {
        get { return new HashtableDynamicObject(attributes);}
    }
}

Where HashtableDynamicObject is implemented as:

public class HashtableDynamicObject : DynamicObject
{
    private readonly IDictionary dictionary;

    public HashtableDynamicObject(IDictionary dictionary)
    {
        this.dictionary = dictionary;
    }

    public override bool  TryGetMember(GetMemberBinder binder, out object result)
    {
        result = dictionary[binder.Name];
        return dictionary.Contains(binder.Name);
    }

    public override bool TrySetMember(SetMemberBinder binder, object value)
    {
        dictionary[binder.Name] = value;
        return true;
    }

    public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result)
    {
        if (indexes == null)
            throw new ArgumentNullException("indexes");
        if (indexes.Length != 1)
            throw new ArgumentException("Only support a single indexer parameter", "indexes");
        result = dictionary[indexes[0]];
        return dictionary.Contains(indexes[0]);
    }

    public override bool TrySetIndex(SetIndexBinder binder, object[] indexes, object value)
    {

        if (indexes == null)
            throw new ArgumentNullException("indexes");
        if (indexes.Length != 1)
            throw new ArgumentException("Only support a single indexer parameter", "indexes");
        dictionary[indexes[0]] = value;
        return true;
    }
}

This is fairly basic so far, and not really interesting. We expose a hashtable as the entry point for a dynamic object that exposes all the dynamic fields. The really interesting part happens in the NHibernate mapping:

<class name="Customer"
             table="Customers">

    <id name="Id">
        <generator class="identity"/>
    </id>
    <property name="Name" />

    <join table="Customers_Extensions" optional="false">
        <key column="CustomerId"/>
        <dynamic-component name="Attributes" access="field.lowercase">
            <property name="EyeColor" type="System.String"/>
        </dynamic-component>
    </join>
</class>

As you can see, we used both a <join/> and a <dynamic-component/> to do the work. We used the <join/> to move the fields to a separate table, and then mapped those fields via a <dynamic-component/>, which is exposed an IDictionary.

Since we want to allow nicer API usage, we don’t expose the IDictionary directly, but rather expose a dynamic object that provides us with a nicer syntax.

The following code:

using (var session = sessionFactory.OpenSession())
using (var tx = session.BeginTransaction())
{
    session.Save(
        new Customer
        {
            Name = "Ayende",
            Attributes =
            {
                EyeColor = "Brown"
            }
        });
    
    tx.Commit();
}

Will produce the following SQL:

image

And that is quite a nice solution all around Open-mouthed smile