I recently implemented Domain Events as a way of organising domain logic in our application. To summarise how they work:
- Every event in your application is modelled as a class which implements the empty
IDomainEventinterface IDomainEvents are raised by a domain object calling a staticDomainEvents.Raise<T>(T domainEvent) where T : IDomainEventmethod- The
DomainEventsclass passes raisedIDomainEventobjects to theHandle<T>(T domainEvent)method of classes which implement theIDomainEventHandler<T>interface for the type ofIDomainEventraised
One task more or less left to the reader in the blog above is how the DomainEvents class finds all
the IDomainEventHandlers to which it has to pass IDomainEvents. I (as usual) didn’t want to
bother registering handlers, so I made some changes to have them register themselves.
So as an example, here’s an IDomainEvent which signals that an Order has been placed:
public class OrderPlaced : IDomainEvent
{
private readonly Order _order;
public OrderPlaced(Order order)
{
_order = order;
}
public Order PlacedOrder
{
get { return _placedOrder; }
}
}
…and an IDomainEventHandler which sends a confirmation email when the event is raised:
public class NotificationEmailDomainEventHandler :
IDomainEventHandler<OrderPlaced>
{
public void Handle(OrderPlaced domainEvent)
{
// send an email using domainEvent.PlacedOrder;
{
}
The static DomainEvents class is a singleton
which uses an IDomainEventHandlerLibrary to access the IDomainEventHandlers to which it
should pass raised IDomainEvents. The singleton instance is created by the InjectionService;
a static class which wraps the application’s dependency injection container and supplies the
IDomainEventHandlerLibrary. The IDomainEventHandlerLibrary interface is as follows:
public interface IDomainEventHandlerLibrary
{
IEnumerable<IDomainEventHandler<T>> GetEventHandlers<T>(
T domainEvent);
}
…and the DomainEvents class looks like this:
public class DomainEvents
{
private static readonly DomainEvents _instance =
InjectionService.Resolve<DomainEvents>();
private readonly IDomainEventHandlerLibrary _handlerLibrary;
public DomainEvents(IDomainEventHandlerLibrary handlerLibrary)
{
_handlerLibrary = handlerLibrary;
}
public static void Raise<T>(T domainEvent)
where T : IDomainEvent
{
_instance.InstanceRaise(domainEvent);
}
private void InstanceRaise<T>(T domainEvent)
where T : IDomainEvent
{
IEnumerable<IDomainEventHandler<T>> eventHandlers =
_handlerLibrary.GetEventHandlers(domainEvent);
if (eventHandlers != null)
{
foreach (IDomainEventHandler<T> handler in eventHandlers)
{
handler.Handle(domainEvent);
}
}
}
}
A self-configuring implementation of IDomainEventHandlerLibrary then looks like this:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
public class SelfConfiguredDomainEventHandlerLibrary :
IDomainEventHandlerLibrary
{
private static Dictionary<Type, List<IDomainEventHandler>>
_eventHandlerCache = CreateEventHandlerCache();
public IEnumerable<IDomainEventHandler<T>> GetEventHandlers<T>(
T domainEvent)
where T : IDomainEvent
{
if (!_eventHandlerCache.ContainsKey(typeof(T)))
{
return null;
}
return _eventHandlerCache[typeof(T)]
.Cast<IDomainEventHandler<T>>()
.ToArray();
}
private static Dictionary<Type, List<IDomainEventHandler>>
CreateEventHandlerCache()
{
var eventHandlerCache =
new Dictionary<Type, List<IDomainEventHandler>>();
Assembly
.GetExecutingAssembly()
.GetAvailableTypes(typeFilter: t =>
!t.IsInterface &&
typeof(IDomainEventHandler).IsAssignableFrom(t))
.Select(t =>
(IDomainEventHandler)InjectionService.Resolve(t))
.ForEach(eh =>
{
eh.GetType()
.GetInterfaces()
.Where(it =>
it.IsGenericType &&
typeof(IDomainEventHandler)
.IsAssignableFrom(it))
.ForEach(it =>
{
Type handledDomainEventType =
it.GetGenericArguments().First();
if (!eventHandlerCache
.ContainsKey(handledDomainEventType))
{
eventHandlerCache.Add(
handledDomainEventType,
new List<IDomainEventHandler>());
}
eventHandlerCache[handledDomainEventType]
.Add(eh);
});
});
return eventHandlerCache;
}
}
A couple of things to note:
- The generic
IDomainEventHandler<T>interface is derived from an empty, non-genericIDomainEventHandlerinterface in order for this to work - The
Assembly.GetAvailableTypes()method is an extension method of mine
Comments
3 comments