Ayende @ Rahien

It's a girl

Support dynamic fields with NHibernate and .NET 4.0

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

Comments

Franck
03/08/2011 06:22 PM by
Franck

Very nice indeed!

Fred
03/08/2011 09:13 PM by
Fred

Yeah, thats really cool and easy.

And btw it is good to read some interesting stuff again after a long time... :-)

Andrey Shchekin
03/08/2011 09:14 PM by
Andrey Shchekin

I would say using some kind of IUserType (I do not remember if <dynamic-component allows that) to create the HashtableDynamicObject would be better.

Otherwise anyone who refactors the code to virtual dynamic Attributes { get; private set; } will break it, because it is not obvious that this field is used externally.

hazzik
03/09/2011 06:14 AM by
hazzik

So, I need to define each of dynamic field in the mapping? I think it is ugly.

Daniel Lang
03/09/2011 06:39 AM by
Daniel Lang

I think the main advantage of this approach is the huge performance benefit over an EAV model. Unfortunately I don't see a way to get this working in a multi-tenant architecture, where different tenants need to have individual fields, while still using on the same DB.

Boris Drajer
03/09/2011 09:58 AM by
Boris Drajer

Great stuff, in v3.0 NHibernate has really outgrown the "ORM" moniker and is on its way to becoming a platform.

@hazzik: I believe you could use ConfORM or Fluent NHibernate to do the mapping dynamically.

Frank Quednau
03/09/2011 10:06 AM by
Frank Quednau

Daniel, I guess supporting different customers with different schemas on the same DB schema will always be an interesting proposition.

Ayende Rahien
03/09/2011 12:55 PM by
Ayende Rahien

Hazzik,

Since the data storage in a RDBMS, you really have no choice.

Daniel,

You should use different schemas / databases for different tenants.

Daniel Lang
03/09/2011 01:30 PM by
Daniel Lang

@Frank:

Whether different databases and schemas for different tenants are a good choice depends on the kind of software. Let's say you have a SaaS - based CRM-application and you want your customers to choose which fields make up a single business-contact. You need to find a way, to dynamically allow to customize the contact-entity.

In such a case it would not only be a waste of memory (each db-instance can take up to hundreds of mb) but also a unnecessary overhead while managing updates, backups, etc.

@Oren:

Would you say that RavenDB would be a good choice in such a situation?

janis
03/11/2011 10:42 PM by
janis

looks nice.. but i don't like it that you need to write in the xml file properties of a dynamic field..

why... because it isn't dynamic any more.. if there is a new field you have to go in the code to add it..

that's why this is better to use it with mongodb because of the json solution. you don't need to use xml..or get in the code for adding a new field

add field detail page on asp.net and it add this field in the document at the monogdb.. you can even search these dynamic fields..

that's the real dynamic..

gr.

Ayende Rahien
03/12/2011 08:46 AM by
Ayende Rahien

Janis,

I guess you missed the part where I recommended doing just that, then...

janis
03/12/2011 09:40 AM by
janis

uuu..? you still using a xml mapper and need to edited the xml file for adding new fields...

i don't see that you are getting a collection from the database..

<property
has only one propertie.. it isn't a collection..

so if i need to add Eyesize i need to put

<property
in it :s....

oke.. you don't need to add it in the .net code.. but you have to edit in backend the xml file.. :S..

Ayende Rahien
03/12/2011 04:35 PM by
Ayende Rahien

Janis,

I was actually referring to:

In general, I would recommend anyone that needs dynamic fields to work with a data storage solution that supports it (like RavenDB , for example).

But in response to your other questions:

  • You can put a collection in a dynamic property.

  • You can edit the configuration programatically

Jake Scott
03/17/2011 10:07 AM by
Jake Scott

Is there any way to combine this with NHibernate Lucene Full text search?

Tattu
03/21/2011 01:16 PM by
Tattu

This is great for basic types.But can I add a many to one relation problematically.

Ephraim
03/30/2011 07:09 AM by
Ephraim

How are the newcolumns created in customer_extensions.Is it Nhibernate doing update schema on the database.

Comments have been closed on this topic.