Merging domain events before dispatching
This post describes a common problem: how to deal with multiple domain events if raising of one of them must negate the others.
Domain events
I assume you already know what a domain event is. If not, read this post first. I’ll rehash it here but only briefly.
A domain event describes an event that is significant to your domain. There are normally 3 parties involved: event producers, event consumers, and the event dispatcher:
-
Event producer is a domain entity (an aggregate root, to be precise). Each entity can generate one or more domain events during a business transaction. The business transaction is usually orchestrated by a SQL transaction.
-
When the business transaction successfully finishes, event dispatcher picks up all domain events generated by each entity and dispatches them to event consumers.
-
Event consumers are classes that subscribe to domain events in advance. The subscription can be done similarly to how command and query handlers subscribe to handling commands and queries. To do that, create an
IEventHandler<T>
interface and use reflection to match events with event handlers:
public sealed class Messages
{
private readonly IServiceProvider _provider;
public Messages(IServiceProvider provider)
{
_provider = provider;
}
public void Dispatch(IDomainEvent domainEvent)
{
Type type = typeof(IDomainEventHandler<>);
Type[] typeArgs = { domainEvent.GetType() };
Type handlerType = type.MakeGenericType(typeArgs);
dynamic handler = _provider.GetService(handlerType);
handler.Handle((dynamic)domainEvent);
}
}
There are different opinions on whether you can use domain events for inter-application communication only (communication with external systems) or for both inter- and inner-application communications (communications between classes inside your app). I don’t recommend using events for inner-application communications because it complicates the program flow and makes it harder to reason about. Interactions between classes are best done using regular techniques, like returning a result of the operation and passing it to the next method.
When you use domain events for inter-application communications only, all your domain event consumers do is they call external applications to inform them about important changes in your app. These calls normally take the form of putting messages on a message bus but you can also send an email or call an external application directly, using that application’s API.
Alright, enough of the rehash. Again, read this article to learn more about domain events.
Merging domain events
Sometimes, you can’t dispatch events generated by your domain model as is and need to merge them first.
For example, let’s say you have the following event producer that generates order canceled domain events:
public class Order
{
public void Cancel()
{
/* ... */
AddDomainEvent(new OrderCanceledEvent(Id));
}
}
public class OrderCanceledEvent : IDomainEvent
{
public int OrderId { get; }
public OrderCanceledEvent(int orderId)
{
OrderId = orderId;
}
}
And a consumer that converts domain events into messages on a bus:
public class OrderCanceledEventHandler : IDomainEventHandler<OrderCanceledEvent>
{
private readonly MessageBusGateway _gateway;
public OrderCanceledEventHandler(MessageBusGateway gateway)
{
_gateway = gateway;
}
public void Handle(OrderCanceledEvent domainEvent)
{
_gateway.SendOrderCanceledMessage(domainEvent.OrderId);
}
}
So far so good. Now, let’s say you are to implement another use case. The admin needs to be able to deactivate the customer account. Such deactivation must automatically cancel all open orders and send a separate (just one) message to the message bus.
How would you do that? The problem with the straightforward approach like
public class Customer
{
public void Deactivate()
{
foreach (Order order in Orders)
{
order.Cancel();
}
AddDomainEvent(new CustomerDeactivatedEvent(Id));
}
}
public class CustomerDeactivatedEventHandler : IDomainEventHandler<CustomerDeactivatedEvent>
{
public void Handle(CustomerDeactivatedEvent domainEvent)
{
_gateway.SendCustomerDeactivatedMessage(domainEvent.CustomerId);
}
}
is that the event consumers would generate several messages: one per each order and another one for the customer itself. You need to somehow cancel out the unnecessary domain events and leave just the customer deactivated one. In other words, merge the domain events.
One way to do that would be to introduce a flag argument to the order.Cancel()
method and pass either true
or false
depending on where the method is called from:
public class Order
{
public void Cancel(bool generateEvent)
{
/* ... */
if (generateEvent)
{
AddDomainEvent(new OrderCanceledEvent(Id));
}
}
}
That’s a bad way to implement the requirement. This flag doesn’t make sense from a domain modeling perspective: it introduces a technical concern that doesn’t have anything to do with the process of canceling an order.
Another way to do the merging is to define the relationship between the domain events in the form of one being a superset for the other and then check that relationship when producing a new event:
public abstract class DomainEvent
{
protected virtual Type SupersetFor => null;
public bool IsSupersetFor(DomainEvent domainEvent)
{
return domainEvent.GetType() == SupersetFor;
}
}
public class CustomerDeactivatedEvent : DomainEvent
{
protected override Type SupersetFor => typeof(OrderCanceledEvent);
}
public abstract class AggregateRoot
{
private readonly List<DomainEvent> _domainEvents = new List<DomainEvent>();
public IReadOnlyList<DomainEvent> DomainEvents => _domainEvents;
protected virtual void AddDomainEvent(DomainEvent newEvent)
{
if (_domainEvents.Any(existing => existing.IsSupersetFor(newEvent)))
return;
_domainEvents.Add(newEvent);
}
}
This implementation looks much nicer. It provides a declarative way to define which domain event should be canceled out (or, rather, not produced in the first place) and doesn’t require any additional logic in the entities. Now, when an order tries to produce an order cancellation event, it won’t be added to the collection if that collection already contains a customer deactivation event.
This approach completely misses the point, though. Order cancellation must always be accompanied by producing a domain event. The fact that the cancellation takes place during the customer account deactivation doesn’t make that cancellation less important. An event is an event, no matter the circumstances.
In fact, the whole concept of domain event merging is flawed. Event producers and event consumers are decoupled from each other for a reason - so that you process those events differently depending on your business requirements. The current business requirement is not to send order cancellation messages when the customer is deactivated. The best way to handle it is on the dispatcher side.
For that, you need to:
-
gather all events related to the customer aggregate in that aggregate’s event collection,
-
modify the event dispatcher such that it reduces the aggregate’s event collection before dispatching.
internal class EventDispatcher :
IPostInsertEventListener,
IPostDeleteEventListener,
IPostUpdateEventListener,
IPostCollectionUpdateEventListener
{
public void OnPostUpdate(PostUpdateEvent ev)
{
DispatchEvents(ev.Entity as AggregateRoot);
}
public void OnPostDelete(PostDeleteEvent ev)
{
DispatchEvents(ev.Entity as AggregateRoot);
}
public void OnPostInsert(PostInsertEvent ev)
{
DispatchEvents(ev.Entity as AggregateRoot);
}
public void OnPostUpdateCollection(PostCollectionUpdateEvent ev)
{
DispatchEvents(ev.AffectedOwnerOrNull as AggregateRoot);
}
private void DispatchEvents(AggregateRoot aggregateRoot)
{
if (aggregateRoot == null)
return;
// New functionality
IDomainEvent[] reduced = EventReducer.ReduceEvents(aggregateRoot.DomainEvents);
foreach (IDomainEvent domainEvent in reduced)
{
Messages.Dispatch(domainEvent);
}
aggregateRoot.ClearEvents();
}
}
Now you have a new EventReducer
class that is responsible for the reduction of the domain event collection in aggregates. It is purely functional and thus can be easily tested. And you can easily modify it if needed too. No need for the convoluted logic on the producer side anymore.
Summary
-
Entities are producers of domain events.
-
Event consumers convert domain events into calls to external applications.
-
Event dispatcher is a mediator between event producers and event consumers.
-
Don’t merge domain events on the producer side.
-
Instead, reduce the domain events in the event dispatcher before dispatching them.
Subscribe
Comments
comments powered by Disqus