Fire domain events after transaction completes
https://softwareengineering.stackexchange.com/questions/255307
-
05-10-2020 - |
Pergunta
I am trying to implement a domain event system that only fires its events when the associated unit-of-work commits successfully.
The main reason why I want to do this is because I have other sub-systems that read the database that expect those changes to be in place when handling the event.
I also don't want event handlers to do their work (send out emails, etc.) if the commit fails.
The system is multiple ASP.NET applications (WebForms, MVC, and WebAPI).
public class Order
{
private Payment _payment;
public int ID { get; private set; }
public decimal Amount { get; private set; }
public void PayUsing(IPaymentProcessor processor)
{
if (_payment != null)
{
throw new InvalidOperationException(
"You can't pay for an order twice");
}
// the Process method may also raise domain events
_payment = processor.Process(Amount);
// Raise saves the event into a static Queue<T>
DomainEvents.Raise(new OrderPaidEvent(this));
}
}
public class OrderFulfillmentService
{
private readonly IUnitOfWorkFactory _unitOfWorkFactory;
private readonly IPaymentProcessor _paymentProcessor;
public OrderFulfillmentService(
IUnitOfWorkFactory unitOfWorkFactory,
IPaymentProcessor paymentProcessor)
{
_unitOfWorkFactory = unitOfWorkFactory;
_paymentProcessor = paymentProcessor;
}
public void Fulfill(int orderId)
{
using (var unitOfWork = _unitOfWorkFactory.Create())
{
var order = unitOfWork.OrdersRepository.GetById(orderId);
order.PayUsing(_paymentProcessor);
unitOfWork.Commit();
// I only want the events to be raised if the Commit succeeds
}
}
}
With the current implementation, it's problematic because right now the domain raising must put it into a queue as it must not fire immediately. The queue is static which causes problems for multi-user systems. I do not want to accidentally fire events for another users, which is a definite problem as all users access the same queue.
This is the list of solutions I have investigated, and why they won't work in my scenario
Have a non-static
DomainRaiser
class that gets passed into thePayUsing
method.- Passing a
DomainRaiser
is very difficult to do from the domain objects, sometimes events can be raised by changing properties and passing this object around is cumbersome.
- Passing a
Have all entities contain an
Events
enumeration that is read by theUnitOfWork.Commit
call as discussed on http://lostechies.com/jimmybogard/2014/05/13/a-better-domain-events-pattern/- Tracking every single entity that has possibly changed is very difficult, I am off-loading this change tracking to the ORM. This also clutters up the domain objects.
Using
HttpContext.Current.Items
to store user-specific events- This is currently the best suggestion, however, it's not possible to unit-test, and it locks my domain to using asp.net, which I have plans in the future to release a desktop app.
My question is, how do I queue and dispatch these events up in a multi-user environment reliably while taking into consideration that I only want to fire events if the overall unit-of-work succeeds?
Solução
First, I would ensure each entity has reference to DomainRaiser
. It would be best if this was set when the entity is created or materialized in repository. Each user/request context would have it's own instance which would then be injected into all entities, that are worked by the context. I don't know what Repository/UnitOfWork implementation are you using, so it might be impossible to do. But I think it might be automated if you can give your entities an interface which can be called during construction.
This would also solve your second problem of only executing the events if UnitOfWork
succeeds. If DomainRaiser
and UnitOfWork
have single instance in one context, they can interact with each other, which would make this simple method call.
Last thing that comes to my mind is that you could separate some events into two parts: prepare
and execute
. Prepare would be called in same UnitOfWork
as the event was raised from. This would allow you to read relevant data for the event, without waiting for commit. Execution would then be called outside with a flag if UnitOfWork
committed successfully and would use the prepared data.