Disclaimer: This isn't something I've thought up. It's taken directly from Udi Dahan's Domain Events. This post is about me understanding what Udi is describing.
In the system I’m refactoring to a richer domain model (from a more “dumb” domain, where most logic is in the service layer) I want to raise domain events when something important happens in the domain. Prior to actually implementing this change in my codebase I needed to understand how the framework will work. To that end I've made the following dummy application with one event that is raised and below I’ll attempt to explain the how and why it works.
Business objects wanting to raise domain events call the Raise method on the DomainEventDispatcher to notify the system that an event of interest has occurred. The business object passes a instance of IDomainEvent in the Raise method.
The DomainEventDispatcher uses the Castle Windsor IoC container to get all registered handlers of the event being raised and calls the Handle method on each.
To wire up events you implement the IHandles interface for each different action you want to occur when the the business object raises the event. Registering each IHandles implementation with IoC allows the event to be subscribed to. DomainEventDispatcher will query IoC to get all handlers for the raised event and call their Handle method.
In this simple exmaple I have an invoice that raises an event when the invoice is paid. In the real world this could be used to send out a thankyou email, updating other accouting software, or do whatever other action needs to be performed in this case.
public class Invoice
{
public string InvoiceNumber { get; set; }
public decimal AmountOwing { get; set;}
public void MakePayment(decimal amount)
{
AmountOwing += amount;
DomainEventDispatcher.Raise(new InvoiceWasPaid() { InvoiceNumber = InvoiceNumber, PaymentAmount = amount});
}
}
IDomainEvent doesn’t have any actions and is used to identify and restrict class to only those that are domain events.
public interface IDomainEvent
{
}
InvoiceWasPaid is the event that occurs. It’s good form to name event as verbs past tense. This also indicates when the event took place. In thise case the invoice has already been paid.
public class InvoiceWasPaid : IDomainEvent
{
public string InvoiceNumber { get; set; }
public decimal PaymentAmount { get; set; }
}
We now need to implement IHandles to perform the action that should occur once the Invoice has been paid.
public class InvoiceWasPaidHandler : IHandles<InvoiceWasPaid>
{
public void Handle(InvoiceWasPaid args)
{
Console.WriteLine("{0:c} was paid on invoice '{1}'", args.PaymentAmount, args.InvoiceNumber);
}
}
Finally we need to hook it all up with the DomainEventDispatcher class
public static class DomainEventDispatcher
{
private static IWindsorContainer Container;
// Call this in your App startup, such as main() or in the Application_Start event of your webapp,
// after you have configured your IoC container
public static void SetContainer(IWindsorContainer container)
{
Container = container;
}
public static void Raise<T>(T args) where T : IDomainEvent
{
foreach(var handler in Container.GetAllInstances<IHandles<T>>()) {
handler.Handle(args);
}
}
}
Last but not least in this sample, we have the stub to test it all.
class Program
{
static void Main(string[] args)
{
// configure IoC container
IWindsorContainer container = new Container();
container.Register(Component.For<IHandles<InvoiceWasPaid>>().ImplementedBy<InvoiceWasPaidHandler>());
DomainEventDispatcher.SetContainer(container);
var invoice = new Invoice() {InvoiceNumber = "70034", AmountOwing = 100M};
Console.WriteLine("Invoice '{0}' with amount owing of {1:c}", invoice.InvoiceNumber, invoice.AmountOwing);
Console.WriteLine("About to make payment of $50.00 on invoice '{0}'", invoice.InvoiceNumber);
invoice.MakePayment(50M);
Console.ReadLine();
}
}
When executed, this code will output the following:
You can see the in last line above that the event handler InvoiceWasPaidHandler.Handle was raised and executed when the call invoice.MakePayment(50M) method was called.
We can't test the above because DomainEventDispatcher is a static class. To get around this The DomainEventDisaptcher allows registering for callbacks. The callbacks are fired just after the event. This allows us to create a delegate in our test method. The updated version of DomainEventDispatcher looks like:
public static class DomainEventDispatcher
{
[ThreadStatic]
private static List<Delegate> actions;
private static IWindsorContainer Container { get; set; }
public static void SetContainer(IWindsorContainer container)
{
Container = container;
}
public static void Register<T>(Action<T> callback) where T : IDomainEvent
{
if (actions == null) {
actions = new List<Delegate>();
}
actions.Add(callback);
}
public static void ClearCallbacks()
{
actions = null;
}
public static void Raise<T>(T args) where T : IDomainEvent
{
if (Container != null) {
foreach (var handler in Container.ResolveAll<IHandles<T>>()) {
handler.Handle(args);
}
}
if (actions != null) {
foreach (var action in actions)
if (action is Action<T>)
((Action<T>) action)(args);
}
}
}
Testing
Now when an event is raised any attached callbacks are also called. This allows for a unit test that looks like the following:
[TestFixture]
public class InvoiceFixture
{
[SetUp]
public void SetUp()
{
IWindsorContainer container = new WindsorContainer();
// Add event handlers to the container
container.Register(Component.For<IHandles<InvoiceWasPaid>>().ImplementedBy<InvoiceWasPaidHandler>());
DomainEventDispatcher.SetContainer(container);
DomainEventDispatcher.ClearCallbacks();
}
[Test]
public void Invoice_payment_raises_event()
{
var invoice = new Invoice() {AmountOwing = 50, InvoiceNumber = "70043"};
string invoiceNumber = "";
decimal amount = 0;
DomainEventDispatcher.Register<InvoiceWasPaid>(x =>
{
invoiceNumber = x.InvoiceNumber;
amount = x.PaymentAmount;
});
invoice.MakePayment(50M);
Assert.AreEqual(50M, amount);
Assert.AreEqual("70043", invoiceNumber);
}
}
I'm not convinced its fantastic to have test only code in there, as I'm not sure when we'd use the callback in production. Regardless, my test passes, showing the event is being raised from the MakePayment() call.
A little more on IoC
My container of choise is Castle Windsor. I prefer to use an installer to register my event handlers with the container, meaning I don't have explicity register each handler and I won't run the risk of forgetting to register one.
public class DomainEventsInstaller : IWindsorInstaller
{
public void Install(IWindsorContainer container, IConfigurationStore store)
{
container.Register(Classes.FromThisAssembly()
.BasedOn(typeof(IHandles<>))
.WithService.Base()
.LifestyleTransient());
}
}
I also like to keep my domain event handlers in a separate assembly to my domain model and my assembly for domain services. My model assembly contains the infrastructure to raise the domain event as well as the events themselves. But the subscribers to the event, the model doesn't really care about who else cares about the event, and to make the event handlers easiest to find, I keep then in their own assembly (usually something like Domain.Events)