问题
I've been studying on domain driven design in conjunction with domain events. I really like the separations of concerns those events provide. I ran into an issue with the order of persisting a domain object and raising domain events. I would like to raise events in the domain objects, yet I want them to be persistence ignorant.
I've created a basic ShoppingCartService
, with this Checkout
method:
public void Checkout(IEnumerable<ShoppingCartItem> cart, Customer customer)
{
var order = new Order(cart, customer);
_orderRepositorty.Add(order);
_unitOfWork.Commit();
}
In this example, the constructor of Order
would raise an OrderCreated
event which can be handled by certain handlers. However, I don't want those events to be raised when the entity is not yet persisted or when persisting somehow fails.
To solve this issue I have figured several solutions:
1. Raise events in the service:
Instead of raising the event in the domain object, I could raise events in the service. In this case, the Checkout
method would raise the OrderCreated
event. One of the downsides of this approach is that by at looking the Order
domain object, it isn't clear which events are raised by what methods. Also, a developer has to remember to raise the event when an order is created elsewhere. It doesn't feel right.
2. Queue domain events
Another option is to queue domain events and raise them when persisting succeeded. This could be achieved by a using
statement for example:
using (DomainEvents.QueueEvents<OrderCreated>())
{
var order = new Order(cart, customer);
_orderRepositorty.Add(order);
_unitOfWork.Commit();
}
The QueueEvents<T>
method would set a boolean to true
and the DomainEvents.Raise<T>
method would queue the event rather than executing it directly. In the dispose callback of QueueEvent<T>
, the queued events are executed which makes sure that the persisting already happened. This seems rather tricky and it required the service to know which event is being raised in the domain object. In the example I provided it also only supports one type of event to be raised, however, this could be worked around.
3. Persist in domain event
I could persist the object using a domain event. This seems okay, except for the fact that the event handler persisting the object should execute first, however I read somewhere that domain events should not rely on a specific order of execution. Perhaps that is not that important and domain events could somehow know in which order the handlers should execute. For example: suppose I have an interface defining a domain event handler, an implementation would look like this:
public class NotifyCustomer : IDomainEventHandler<OrderCreated>
{
public void Handle(OrderCreated args)
{
// ...
}
}
When I want to handle persisting in using an event handler too, I would create another handler, deriving from the same interface:
public class PersistOrder : IDomainEventHandler<OrderCreated>
{
public void Handle(OrderCreated args)
{
// ...
}
}
}
Now NotifyCustomer
behaviour depends on the order being saved in the database, so the PersistOrder
event handler should execute first. Is it acceptable that these handlers introduce a property for example that indicates the order of their execution? A snap from the implementation of the DomainEvents.Raise<OrderCreated>()
method:
foreach (var handler in Container.ResolveAll<IDomainEventHandler<OrderCreated>>().OrderBy(h => h.Order))
{
handler.Handle(args);
}
Now my question is, do I have any other options? Am I missing something? And what do you think of the solutions I proposed?
回答1:
Either your (transactional) event handlers enlist in the (potentially distributed) transaction, or you publish/handle the events after the transaction committed. Your "QueueEvents" solution gets the basic idea right, but there are more elegant solutions, like publishing via the repository or the event store. For an example have a look at SimpleCQRS
You might also find these questions and answers useful:
CQRS: Storing events and publishing them - how do I do this in a safe way?
Event Aggregator Error Handling With Rollback
Update on point 3:
... however I read somewhere that domain events should not rely on a specific order of execution.
Regardless of your way of persisting, the order of events absolutely matters (within an aggregate).
Now NotifyCustomer behaviour depends on the order being saved in the database, so the PersistOrder event handler should execute first. Is it acceptable that these handlers introduce a property for example that indicates the order of their execution?
Persisting and handling events are separate concerns - don't persist using an event handler. First persist, then handle.
回答2:
Disclaimer: I don't know what I am talking about. ⚠️
As an alternative to raising events in the ShoppingCartService
you could raise events in the OrderRepository
.
As an alternative to queuing domain events in the ShoppingCartService
you could queue them in the Order
aggregate by inheriting from a base class that provides a AddDomainEvent
method.
public class Order : Aggregate
{
public Order(IEnumerable<ShoppingCartItem> cart, Customer customer)
{
AddDomainEvent(new OrderCreated(cart, customer))
}
}
public abstract class Aggregate
{
private List<IDomainEvent> _domainEvents = new List<IDomainEvent>();
public IReadOnlyCollection<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();
public void AddDomainEvent(IDomainEvent domainEvent)
{
_domainEvents.Add(domainEvent);
}
}
来源:https://stackoverflow.com/questions/22194403/persistence-and-domain-events-with-persistence-ignorant-objects