Domain Events - Take 2
Monday, August 25th, 2008.My previous post on how to create fully encapsulated domain models introduced the concept of events as a core pattern of communication from the domain back to the service layer. In that post, I put up enough code to get the idea across but didn’t address issues like memory leaks and multi-threading. This post will show the solution to those two critical points.

I’ve snipped out one of the events in the previous example for brevity.
Previous API
The previous API looked like this:
1: public static class DomainEvents
2: {
3: public static event EventHandler GameReportedLost;
4: public static void RaiseGameReportedLostEvent()
5: {
6: if (GameReportedLost != null)
7: GameReportedLost(null, null);
8: }
9:
10: public static event EventHandler CartIsFull;
11: public static void RaiseCartIsFull()
12: {
13: if (CartIsFull != null)
14: CartIsFull(null, null);
15: }
16: }
One thing that we want to keep in the solution is that all the code to define events, their names, and the parameters they bring will be in one place - in this case, the DomainEvents class. One thing that we’d like to fix is the amount of code needed to define an event.
Previous Service Layer
Here’s what our previous service layer code looked like:
1: public class AddGameToCartMessageHandler :
2: BaseMessageHandler<AddGameToCartMessage>
3: {
4: public override void Handle(AddGameToCartMessage m)
5: {
6: using (ISession session = SessionFactory.OpenSession())
7: using (ITransaction tx = session.BeginTransaction())
8: {
9: ICart cart = session.Get<ICart>(m.CartId);
10: IGame g = session.Get<IGame>(m.GameId);
11:
12: Domain.DomainEvents.GameReportedLost +=
13: gameReportedLost;
14: Domain.DomainEvents.CartIsFull +=
15: cartIsFull;
16:
17: cart.Add(g);
18:
19: Domain.DomainEvents.GameReportedLost -=
20: gameReportedLost;
21: Domain.DomainEvents.CartIsFull -=
22: cartIsFull;
23:
24: tx.Commit();
25: }
26: }
27:
28: private EventHandler gameReportedLost = delegate {
29: Bus.Return((int)ErrorCodes.GameReportedLost);
30: };
31:
32: private EventHandler cartIsFull = delegate {
33: Bus.Return((int)ErrorCodes.CartIsFull);
34: };
35: }
36: }
Another thing that should be improved is the amount of code needed in the service layer.
Raising an event, though, should still be fairly simple - one line of code similar to DomainEvents.RaiseGameReportedLost().
New API
Here’s what the new API looks like:
1: public static class DomainEvents
2: {
3: public static readonly DomainEvent<IGame> GameReportedLost =
4: new DomainEvent<IGame>;
5:
6: public static readonly DomainEvent<ICart> CartIsFull=
7: new DomainEvent<ICart>;
8: }
It looks like we’ve managed to bring down the complexity of defining an event.
Raising an event is slightly different, but still only one line of code (”this” refers to the Cart class that is calling this API): DomainEvents.CartIsFull.Raise(this);
New Service Layer
The advantage of having a disposable domain event allows us to use the “using” construct for cleanup.
1: public class AddGameToCartMessageHandler :
2: BaseMessageHandler<AddGameToCartMessage>
3: {
4: public override void Handle(AddGameToCartMessage m)
5: {
6: using (ISession session = SessionFactory.OpenSession())
7: using (ITransaction tx = session.BeginTransaction())
8: using (DomainEvents.GameReportedLost.Register(gameReportedLost))
9: using (DomainEvents.CartIsFull.Register(cartIsFull))
10: {
11: ICart cart = session.Get<ICart>(m.CartId);
12: IGame g = session.Get<IGame>(m.GameId);
13:
14: cart.Add(g);
15:
16: tx.Commit();
17: }
18: }
19:
20: private Action<IGame> gameReportedLost = delegate {
21: Bus.Return((int)ErrorCodes.GameReportedLost);
22: };
23:
24: private Action<ICart> cartIsFull = delegate {
25: Bus.Return((int)ErrorCodes.CartIsFull);
26: };
27: }
28: }
I also want to mention that you don’t necessarily have to have the same service layer object handle these events as that which calls the domain objects. In other words, we can have singleton objects handling these events for things like sending emails, notifying external systems, and auditing.
The Infrastructure
The infrastructure that makes all this possible (in a thread-safe way) is quite simple and made up of two parts, the DomainEvent that we saw being used above, and the DomainEventRegistrationRemover which handles the disposing:
1: using System;
2: using System.Collections.Generic;
3:
4: namespace DomainEventInfrastructure
5: {
6: public class DomainEvent<E>
7: {
8: [ThreadStatic]
9: private static List<Action<E>> _actions;
10:
11: protected List<Action<E>> actions
12: {
13: get {
14: if (_actions == null)
15: _actions = new List<Action<E>>();
16:
17: return _actions;
18: }
19: }
20:
21: public IDisposable Register(Action<E> callback)
22: {
23: actions.Add(callback);
24: return new DomainEventRegistrationRemover(delegate
25: {
26: actions.Remove(callback);
27: }
28: );
29: }
30:
31: public void Raise(E args)
32: {
33: foreach (Action<E> action in actions)
34: action.Invoke(args);
35: }
36: }
37: }
38:
Note that the invocation list of the domain event is thread static, meaning that each thread gets its own copy - even though they’re all working with the same instance of the domain event.
Here’s the DomainEventRegistrationRemover - even simpler:
1: using System;
2:
3: namespace DomainEventInfrastructure
4: {
5: public class DomainEventRegistrationRemover : IDisposable
6: {
7: private readonly Action CallOnDispose;
8:
9: public DomainEventRegistrationRemover(Action ToCall)
10: {
11: this.CallOnDispose = ToCall;
12: }
13:
14: public void Dispose()
15: {
16: this.CallOnDispose.DynamicInvoke();
17: }
18: }
19: }
For your convenience, I’ve made these available for download here.
I also want to add that if you haven’t looked at the comments on the original post - there’s some really good stuff there (36 comments so far). Take a look.
|
If you liked this article, you might also like articles in these categories:
If you've got a minute, you might enjoy taking a look at some of my best articles.I've gone through the hundreds of articles I've written over the past 4 years and put together a list of the best ones as ranked by my 2000+ readers. You won't be disappointed. If you'd like to get new articles sent to you when they're published, it's easy and free.Subscribe right here. Something on your mind? Got a question? I'd be thrilled to hear it. Leave a comment below or email me, whatever works for you. 14 CommentsYour comment... |







August 25th, 2008 at 8:15 am
You can change this:
this.CallOnDispose.DynamicInvoke();
to this:
this.CallOnDispose();
And get the same behavior with static invocation.
Same for the Raise() method.
Do you really need scoping in here? Why not support this syntax?
public class AddGameToCartMessageHandler :
BaseMessageHandler
{
public override void Handle(AddGameToCartMessage m)
{
DomainEvents.GameReportedLost+= delegate{
Bus.Return((int)ErrorCoes.GameReportedLost);
};
DomainEvents.CartIsFull += delegate{
Bus.Return((int)ErrorCodes.CartIsFull);
};
using (ISession session = SessionFactory.OpenSession())
using (ITransaction tx = session.BeginTransaction())
using (.Register(cartIsFull))
{
ICart cart = session.Get(m.CartId);
IGame g = session.Get(m.GameId);
Â
cart.Add(g);
Â
tx.Commit();
}
}
Â
}
August 25th, 2008 at 1:20 pm
Ayende,
The problem with weakrefs, it turns out, is that the thread may run fast enough that garbage colleciton doesn’t occur in time. As such, we may have a dangling delegate being activated from the handling of a previous message by the same thread.
That’s why I need scoping - to simplify cleanup.
August 25th, 2008 at 5:58 pm
But that is something that should be in the infrastructure.
You put “clean domain events” as part of the life cycle.
Putting it in the code means that you have concerns in your code that the infrastructure should handle.
August 25th, 2008 at 8:00 pm
I like this much better than the first.
Here’s a few things that come to mind that might be nice from a public api perspective
1) conventions++
using(DomainEvents.Register(cartIsFull, gameIsLost)) {
}
I /think/ we can parse out the variable name using the C# 3.0 Expression stuff.
2) using…. the ability of returning something like IDisposableAction is great, but unless there’s great readability to be gained I don’t think I’m in favor of using using this way, is it that much better than something like With.Events(() => { });
August 31st, 2008 at 6:54 pm
Udi,
Maybe you should inject policies into the Cart. The policies are services which perform the checking. Just because the services live outside the domain doesn’t mean the domain is anemic. After all, you are adding intelligence to the domain and can do so using your IoC container.
The service code would look something like:
cart.enforcePolicies(GameOrderPolicies);
if (cart.canAddGame(game))
cart.add(game);
else
Bus.return(cart.brokenPolicies[0].PolicyName)
Your domain only knows that there are certain policies which will be injected in, and those policies must be compliant before before adding a game.
September 1st, 2008 at 12:56 am
Tony,
I consider having the service layer use the result of one call to the domain to decide if another call to the domain is necessary to be taking domain logic out of the domain.
In your example, I would prefer a single call:
cart.Add(game, policies);
The question is about the connection between the domain and the policies - what are the dependencies between them, how did the service layer get an instance of it, etc.
September 1st, 2008 at 4:05 pm
Ayende,
It’s actually possible to have another message handler run at the end of the unit of work and do the clean-up for that thread.
Still thinking about the generality of that solution.
September 3rd, 2008 at 11:46 pm
You need to support the concept of IMessageModule, not just IMessageHandler.
September 6th, 2008 at 2:46 am
Ayende,
There is (kinda) support for that in the handling of the MessageReceived event of the transport - by setting the order of subscriptions you can do Before and After handling.
September 10th, 2008 at 2:19 pm
[…] Update: The new and improved solution is now available: Domain Events, Take 2. […]
October 29th, 2008 at 10:23 am
Not too keen on having the static global events, why not have the events defined as properties on the domain model? I know you would say “but what if a child object raised the event?” but I would say that’s the parent object’s responsibility to listen to child object events and either handle them or raise its own events accordingly (or simply delegate via a custom add and remove handler).
What would be the problem with that?
November 2nd, 2008 at 4:54 pm
Neil,
This creates extensibility and maintainability issues.
Suppose I write another domain object which acts as an aggregate root in a different scenario making use of the current parent as a child. I’d have to remember to redefine that event on my object.
Does that make sense?
December 23rd, 2008 at 3:44 am
@Udi: There is a problem with [ThreadStatic] attribute with Web applications.
I came across circumstances where the list of actions was null because I guess part of the HTTP request was handled by one thread and the rest by another one.
I’ve removed this attribute and it’s fine now.
December 23rd, 2008 at 11:07 pm
Daniel,
Removing the [ThreadStatic] attribute causes a bug - an action on one thread by user A can call be into a different action previously registered by a different thread by user B.
The overall guidance is that the domain model with its associated events should not be called within the web app itself. Rather, the domain model should be hosted in its own process, an application server if you will, which the web app sends messages to.
Hope that helps.