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

  1. Have a non-static DomainRaiser class that gets passed into the PayUsing 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.
  2. Have all entities contain an Events enumeration that is read by the UnitOfWork.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.
  3. 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?

Foi útil?

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.

Licenciado em: CC-BY-SA com atribuição
scroll top