I've been using the Domain Events pattern for some time - it enables us to encapsulate as much behaviour in our domain layer as possible and provides a nice way for other parts of our application to subscribe to domain events.
Currently we're using a static class that our domain objects can call to raise events:
static class DomainEvents
{
public static IEventDispatcher Dispatcher { get; set; }
public static void Raise<TEvent>(TEvent e)
{
if (e != null)
{
Dispatcher.Dispatch(e);
}
}
}
As you can see, this is little more than a shim to an IEventDispatcher
that actually does the work of dispatching or publishing the events.
Our dispatcher implementation simply uses our IoC container (StructureMap) to locate event handlers for the specified type of event.
public void Dispatch<TEvent>(TEvent e)
{
foreach (var handler in container.GetAllInstances<IHandler<TEvent>>())
{
handler.Handle(e);
}
}
This works okay in most cases. However, there are a few problems with this approach:
Events should only be dispatched if the entity is successfully persisted
Take the following class:
public class Order
{
public string Id { get; private set; }
public decimal Amount { get; private set; }
public Order(decimal amount)
{
Amount = amount;
DomainEvents.Raise(new OrderRaisedEvent { OrderId = Id });
}
}
In the Order
constructor we raise an OrderRaisedEvent
. In our application layer we'd likely create the order instance, add it to our database "session" and then commit/save the changes:
var order = new Order(amount: 10);
session.Store(order);
session.SaveChanges();
The problem here is that the domain event is raised before we have successfully saved our Order entity (committing the transaction). If the save failed we would have still dispatched the events.
A better approach would be to queue the events until the entity is persisted. However, I'm unsure how best to implement this whilst maintaining strongly typed event handlers.
Events should not be created until the entity is persisted
Another issue I'm facing is that our entity identifiers are not set/assigned until store the entity (RavenDB - session.Store
). This means that in the above example, the order identifier passed to the event is actually null
.
Since I'm not sure how can actually generate RavenDB identifiers upfront, one solution could be to delay the creation of the events until the entity is actually saved but again I'm not how best to implement this - perhaps queuing a collection of Func<TEntity, TEvent>
?
One solution (as suggested by @synhershko) is to move the dispatching of the domain events outside of the domain. This way we can ensure our entity is persisted before we raise any events.
However, we're now moving behaviour out of the domain (where it belongs) into our application just to work around our persistence technology - which I'm not that happy about.
My solution to Events should only be dispatched if the entity is successfully persisted was to create a deferred event dispatcher that queues the events. We then inject the dispatcher into our unit of work ensuring that we first persist/save our entity and then emit the domain events:
public class DeferredEventDispatcher : IEventDispatcher
{
private readonly IEventDispatcher inner;
private readonly ConcurrentQueue<Action> events = new ConcurrentQueue<Action>();
public DeferredEventDispatcher(IEventDispatcher inner)
{
this.inner = inner;
}
public void Dispatch<TEvent>(TEvent e)
{
events.Enqueue(() => inner.Dispatch(e));
}
public void Resolve()
{
Action dispatch;
while (events.TryDequeue(out dispatch))
{
dispatch();
}
}
}
public class UnitOfWork
{
public void Commit()
{
session.SaveChanges();
dispatcher.Resolve(); // raise events
}
}
Essentially this achieves the same thing as suggested by @synhershko but keeps the "raising" of events within my domain.
As for Events should not be created until the entity is persisted the main issue was that entity identifiers were being set externally by RavenDB. A solution that keeps my domain persistent ignorant and easy to test is to simply pass the id as a constructor parameter. This is what I would have done if using a SQL database (usually passing a Guid).
Fortunately RavenDB does provide a way for you to generate identifiers using the hilo strategy (so we can keep RESTful identifiers). This is from the RavenDB Contrib project:
public static string GenerateIdFor<T>(this IAdvancedDocumentSessionOperations session)
{
// An entity instance is required to generate a key, but we only have a type.
// We might not have a public constructor, so we must use reflection.
var entity = Activator.CreateInstance(typeof(T), true);
// Generate an ID using the commands and conventions from the current session
var conventions = session.DocumentStore.Conventions;
var databaseName = session.GetDatabaseName();
var databaseCommands = session.GetDatabaseCommands();
return conventions.GenerateDocumentKey(databaseName, databaseCommands, entity);
}
I can then use this to generate an ID and pass it in my entity constructors:
var orderId = session.GenerateIdFor<Order>();
var order = new Order(orderId, 1.99M);
Following our discussion on Twitter, instead of doing this from the Order constructor I will just do this:
var order = new Order(amount: 10);
session.Store(order);
DomainEvents.Raise(new OrderRaisedEvent { OrderId = order.Id });
session.SaveChanges();
Even better - you can create a IDocumentStoreListener and do it from there depending on the type being persisted. I see no reason for doing this from within the constructor - you want to raise an event when the order was persisted (or sent to be persisted), not when creating an in-memory representation of it.
Since this will use the HiLo generator internally, you are guaranteed this will not access the DB for every call to Store(), and that the IDs will be unique. So if anything goes wrong the message bearing the order ID will be received, and when no order will be found with that ID after several retries you can assume something went wrong with persisting it. Or you can try and catch exceptions and raise another event with the exception details bearing that order ID.
Trying to contain both message dispatching and storing documents in RavenDB within the same transaction is an overkill really. You should plan and build for failures instead.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With