NHibernate – Automatic change tracking for aggregate roots in DDD scenarios

time to read 7 min | 1391 words

Recently I had a long discussion with Greg Young about the need of this feature, and after spending some time talking to him face to face we were able to reach an understanding on what exactly is required to make this work. Basically, the feature that Greg would like to see is to write code like this and have NHibernate take care of optimistically locking the aggregate.

using (var s = sf.OpenSession())
using (var tx = s.BeginTransaction())
{
    var post = s.Get<Post>(postId);
    post.AddComment(new Comment
    {
        Email = "foo@bar.z",
        Text = "first!",
        Name = "foo",
    });
    tx.Commit();
}

using (var s = sf.OpenSession())
using (var tx = s.BeginTransaction())
{
    var post = s.Get<Post>(postId);
    var comment = post.Comments.First();
    comment.Text += " some change";

    tx.Commit();
}

In this case, Post is the aggregate and Comment is a contained entity. As it turned out, the solution (assuming that you are willing to accept some intentional limitations) is ridiculously easy. Those limitations are more or less inherit to the way people use DDD in the first place, so that doesn’t add additional restrictions on you. The limitations are:

  • The relation between aggregates and contained entities must be explicit.
  • The association between a contained entity and its aggregate must be direct. That is, any contained entity must have a direct reference to its aggregate, not going via intermediaries.
  • You never access a contained entity except by traversing the object graph from its aggregate.

Given all of that, the only thing that we are left with is to formalize the association between aggregates and contained entities:

public interface ICanFindMyAggregateRoot
{
    IAggregateRoot MyRoot { get;  }
}

public interface IAggregateRoot
{
}

This implies that all you entities must have implemented either IAggregateRoot or ICanFindMyAggregateRoot, following on Greg’s allow-no-errors policy, we verify this using:

private static void ProtectMyDomainModelFromDomainDrivenDesignIgnorance(Configuration configuration)
{
    foreach (var clazz in configuration.ClassMappings)
    {
        if(typeof(IAggregateRoot).IsAssignableFrom(clazz.MappedClass) == false &&
            typeof(ICanFindMyAggregateRoot).IsAssignableFrom(clazz.MappedClass) == false)
            throw new InvalidOperationException("DDD Violation " + clazz.MappedClass);
    }
}

And now that we have finished setting everything up, what are we left with?

public class ForceRootAggregateUpdateListener : IPreUpdateEventListener, IPreInsertEventListener
{
    public bool OnPreUpdate(PreUpdateEvent updateEvent)
    {
        var rootFinder = updateEvent.Entity as ICanFindMyAggregateRoot;
        if (rootFinder == null)
            return false;
        
        updateEvent.Session.Lock(rootFinder.MyRoot, LockMode.Force);

        return false;

    }

    public bool OnPreInsert(PreInsertEvent insertEvent)
    {
        var rootFinder = insertEvent.Entity as ICanFindMyAggregateRoot;
        if (rootFinder == null)
            return false;
        
        insertEvent.Session.Lock(rootFinder.MyRoot, LockMode.Force);

        return false;
    }
}

I have spoken about Listeners in the past, and they are the preferred way to extend NHibernate. In this case, we need to register them, and then forget about it:

<listener type='pre-update' class='Ex1_Querying.ForceRootAggregateUpdateListener, Ex1-Querying'/>
<listener type='pre-insert' class='Ex1_Querying.ForceRootAggregateUpdateListener, Ex1-Querying'/>

As you can see, this is really pretty simple, we check if we are currently update a contained entity and then force an update in the versioned entity version. There is a slight problem here that we may generate several updates per transaction here, but I am not worried about that overly much, it is fairly simple to resolve (by keeping track of the entity and not updating if we already updated in the current transaction), so I’ll leave it up to you.

The end result is that this code:

using (var s = sf.OpenSession())
using (var tx = s.BeginTransaction())
{
    var post = s.Get<Post>(postId);
    var comment = post.Comments.First();
    comment.Text += " some change";

    tx.Commit();
}

Results in:

image

And this code:

using (var s = sf.OpenSession())
using (var tx = s.BeginTransaction())
{
    var post = s.Get<Post>(postId);
    post.AddComment(new Comment
    {
        Email = "foo@bar.z",
        Text = "first!",
        Name = "foo",
    });
    tx.Commit();
}

Will result in:

image

As you can see, we generate two update statements for Post here. The first is for the change in the associated Comments collection, the second is for the change (insert) of a contained entity. We can avoid that duplicate update by adding the additional constraint that all contained entities must be directly attached to the aggregate root (so it contain a reference to anything that it uses), but I feel that this is a too heavy limitation, and that a single superfluous update is just all right with me.

So here you go, fully automated aggregate change tracking in any part of the graph.