Ayende @ Rahien

Hi!
My name is Oren Eini
Founder of Hibernating Rhinos LTD and RavenDB.
You can reach me by phone or email:

ayende@ayende.com

+972 52-548-6969

, @ Q c

Posts: 5,968 | Comments: 44,488

filter by tags archive

NHibernate – Automatic change tracking for aggregate roots in DDD scenarios


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.


Comments

Remco Ros

var words = File.ReadAllLines("words.txt");

so... is that a left-over from your previous post? :-)

Frans Bouma

If another thread has already updated Post, the version is different, so the update fails. However, semantically one could argue that it should have been version+1, instead of a harcoded value.

Ayende Rahien

Frans,

It is not hard coded value, it is a parameter that is set to version+1

Bryan

Very interesting... I have to stew on this a bit, but we were essentially moving down the path of trying to solve the same problem. We were doing it by having the repository refuse to save an aggregate unless you explicitly locked it and keeping track of which aggregates were locked internally. Then, we built dirty tracking into our model. Our strategy works, but it still requires too much code in the model to track the changes and you have to manually acquire the lock which is yet more code to maintain.

Jo&#227;o P. Bragan&#231;a

private static void ProtectMyDomainModelFromDomainDrivenDesignIgnorance...

I would have spit coffee all over my monitor had I been drinking coffee at the time!

pete w

Very slick.

Works provided youve got a single aggregate root and not many, but maybe thats just me being a "DDD-ignoramus".

Lucio Assis

Do you really write the below repeatedly all throughout your code?

using (var s = sf.OpenSession())

using (var tx = s.BeginTransaction())

Ayende Rahien

Lucio,

For demos, forsure.

Anything else, not realy.

Stephen

In your example, is this really a place where you would care about locking the aggregate..

Perhaps I've not built large enough systems, or just ignorant with domain driven design.. but, the intent is to add a comment to a specific post.. why would you care that its changed since you loaded it?

In this scenario wouldn't this simply cause many 'add comment' actions to be rejected on what would be an extremely busy blog.. whilst really serving no purpose..

I can understand it for other things like.. your scenario where a person has multiple phones and the contact had changed since you loaded it.

dkl
dkl

I guess it would be easy to support not only members referenced directly from aggregate root, but the whole subtree, including intermediaries. You just have to implement both ICanFindMyAggregateRoot and IAggregateRoot on the object and add some while cycles to ForceRootAggregateUpdateListener methods, right? (It would make sense to use three interfaces in this case, like IAggregateRoot, IAggregateChild, IAggregateIntermediary)

Bryan

@Stephen

In a very large system you would probably make Comment it's own aggregate root independent of Post. The additional complexity of having to query for Comments separately from Posts is a trade off against scalability of the system.

In a simpler system, you might have Post be an aggregate root that contains all Comments. It's easier to reason about what is happening inside the database, you'll write less code, but scaling will be more difficult.

It all depends on what you are a building. Are you building Slashdot, or are you building your own person blog?

Michael Hart

What about value objects? I have plenty of value objects in my aggregates, but there's no way I'm introducing a reference back to the aggregate in them.

Roger

That's pretty much the way we've solved it in our project - with one difference. The part you skipped, no duplicated lockmode.force for the same root, we found it hard solving nicely with event listeners because of their stateless nature. Quite doable but it became quite messy for us when we went that path (most probably because of lack of knowledge about listeners from ourside).

We used an iinterceptor instead where we found it it easier to keep data "per session" (or here rather - per tran).

Peter Morris

In my opinion there are two parts to the "Aggregate root" pattern, which really should be two different patterns.

Part 1: Lifetime management.

An OrderLine is part of an Order, so it must live and die as a composite part of that Order.

Part 2: Validation

When updating an OrderLine you must validate via the Order so that you can check validation as a whole. Such as "Is the total order value < 1000" etc.

NH already handles part 1 quite nicely so I won't bother mentioning it any further. This post is attempting to address Part 2, however it is too simple to work.

The "validation root" depends entirely on what you are doing.

Example 1: Raising a new order

Requirement:

I need to ensure that the total value of unpaid orders for the customer does not exceed the customer's credit limit.

DDD solution:

You ensure a version upgrade is performed on Customer.

Example 2: Creating a shipping document from an Order

Requirement:

The order is only considered closed for modification once a shipping order has been completed. When creating the shipping document from the Order we need to ensure that it accurately represents the content of the Order.

DDD solution:

Ensure a version upgrade on Order when creating a shipping document for it. There is no need to do a lock on Customer because we are not changing the value of the order, nor are we adjusting the Customer's outstanding balance.

So here you see we have 2 different operations on an Order which both require a different "validation root". Personally I think the DDD agg-root pattern is a mix of two different patterns. The solution you have outlined will

A: Not work for all scenarios

B: Will muddy the model if I have an agg-root structure 5 classes deep because each needs a reference to the root.

I maintain that only the app layer knows the context in order to determine what the validation root is.

s_tristan

I resolved the problem by defining two custom collections:

  1. ManyToOneCollection <parenttype,childtype>

  2. ManyToManyCollection <type1,type2>

This collections automatically manages the relationships on both sides.

Ayende Rahien

Value objects are part of the entity as far as NH is concerened

Colin Jack

"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."

This would be the bit I've been unhappy with when considering this sort of solution in the end.

Ayende Rahien

Colin,

That is part of DDD anyway

Comment preview

Comments have been closed on this topic.

FUTURE POSTS

No future posts left, oh my!

RECENT SERIES

  1. Career planning (6):
    24 Jul 2015 - The immortal choices aren't
  2. Production postmortem (4):
    23 Jul 2015 - The case of the native memory leak
  3. API Design (7):
    20 Jul 2015 - We’ll let the users sort it out
  4. What is new in RavenDB 3.5 (3):
    15 Jul 2015 - Exploring data in the dark
  5. The RavenDB Comic Strip (3):
    28 May 2015 - Part III – High availability & sleeping soundly
View all series

RECENT COMMENTS

Syndication

Main feed Feed Stats
Comments feed   Comments Feed Stats