Click here to Skip to main content
15,867,453 members
Articles / Programming Languages / C#

Domain Events with Convention-Based Registration and Deferred Execution Support

Rate me:
Please Sign up or sign in to vote.
5.00/5 (12 votes)
27 Mar 2017CPOL5 min read 20.4K   138   17   1
Domain Events registered by conventions with the container and fired only when the transaction is committed

In DDD, we establish bounded contexts and define how they will exchange information, which often occurs by raising events. According to Eric Evans, you should:

“Describe the points of contact between the models, outlining explicit translation for any communication and highlighting any sharing.”

These contexts will often communicate as part of a same database transaction. That's actually desired, since it will assure integrity between the whole process. However, in the real world, things can be tricky. Some actions might depend on committed transactions in order to get triggered. This happens, for instance, to services that run outside of the ongoing database transaction. You don’t want to send an email to notify the user that something changed and have the whole operation rolled back seconds later. Same thing happens when our applications consume distributed services that do not support distributed transactions.

What We Want To Achieve

Let's see our main goals. There are many good ways to implement domain event handlers. This implementation will use a dependency container to control the lifetime of the events raiser, it will register events with the container on a very transparent way and support delayed execution. Let's review:

Effortless Registration

In order to create a new handler for an event, we don't want to look for a distant class in another project that has hundreds of event-to-handler bindings and add new lines. To that end, we might want to consider to use a dependency container.

Deferred Execution Support

Handlers that invoke code that are not bound to a transaction should be fired only once the transaction is committed.

Practical Example : Paying an Order on an E-Commerce App

Let's take as example the payment of an order on an e-commerce application. Once the payment is processed, it raises an event. Then, the bounded context that processes customers orders handles the event and places the order for shipment. Furthermore, the bounded context that manages the inventory handles the same event to subtract from the stock the items that were ordered.

Image 1

Created with Balsamiq Mockups.

Firing the Event

PaymentService is instantiated by the dependency container. An event is raised on the last line of the PayOrder method.

C#
public class PaymentService : IPaymentService
{
    private readonly IDomainEventsRaiser _events;
    private readonly IRepository<Order> _orderRepository;
    private readonly IRepository<Payment> _paymentRepository;

    public PaymentService(IRepository<Payment> paymentRepository,
               IRepository<Order> orderRepository, IDomainEventsRaiser events)
    {
        _paymentRepository = paymentRepository;
        _orderRepository = orderRepository;
        _events = events;
    }

    public void PayOrder(int orderId, decimal amount)
    {
        Order order = _orderRepository.GetById(orderId);

        Payment payment = new Payment()
        {
            OrderId = orderId,
            Amount = amount
        };

        _paymentRepository.Insert(payment);

        _events.Raise(new OrderPaidEvent(payment, order.Items));
    }
}

IDomainEventsRaiser has the responsibility of raising events and it needs only one method to do so:

C#
public interface IDomainEventsRaiser
{
    /// <summary>
    /// Raises the given domain event
    /// </summary>
    /// <typeparam name="T">Domain event type</typeparam>
    /// <param name="domainEvent">Domain event</param>
    void Raise<T>(T domainEvent) where T : IDomainEvent;
}

What makes OrderPaidEvent an event is the fact that it implements the IDomainEvent interface. This interface actually does not expose any service. Its only purpose is to serve as a generic constraint to the Raise method of IDomainEventsRaiser to group classes that represent an event.

C#
public class OrderPaidEvent : IDomainEvent
{
    public OrderPaidEvent(Payment payment, IEnumerable<OrderItem> orderItems)
    {
        Payment = payment;
        OrderItems = new List<OrderItem>(orderItems);
    }

    public Payment Payment { get; private set; }

    public List<OrderItem> OrderItems { get; set; }
}

Handling the Event

As we said earlier, we want effortless registration. To that end, we will use some conventions. Every handler will have to implement the interface IHandles, as described below:

C#
/// <summary>
/// Handles an event. If there is a database transaction, the
/// execution is delayed until the transaction is complete.
/// </summary>
/// <typeparam name="T"></typeparam>
public interface IHandles<T> where T : IDomainEvent
{
    void Handle(T domainEvent);
}

Once the order is paid, its related items are subtracted from the stock. In other words, once OrderPaidEvent is raised, a SubtractInventaryWhenOrderPaidEventHandler will handle it. The IHandles interface makes the bridge between a domain event and its handlers.

C#
public class SubtractInventaryWhenOrderPaidEventHandler : IHandles<OrderPaidEvent>
{
    private readonly IInventoryService _inventory;

    public SubtractInventaryWhenOrderPaidEventHandler(IInventoryService inventory)
    {
        _inventory = inventory;
    }

    public void Handle(OrderPaidEvent domainEvent)
    {
        foreach (var item in domainEvent.OrderItems)
            _inventory.SubtractAvailability(item.InventoryItemId, item.Quantity);
    }
}

The handler is also created by the dependency container. That allows dependencies to be passed more easily.

We can have multiple handlers for the same event. That said, once the order is paid, it can be placed for delivery. That means that a PlaceOrderWhenPaidEventHandler will handle this same event (we can also have one single handler that handles multiples events).

C#
public class PlaceOrderWhenPaidEventHandler : IHandles<OrderPaidEvent>
{
    private readonly IOrderPlacementService _orderPlacement;

    public PlaceOrderWhenPaidEventHandler(IOrderPlacementService orderPlacement)
    {
        _orderPlacement = orderPlacement;
    }

    public void Handle(OrderPaidEvent domainEvent)
    {
        _orderPlacement.PlaceOrder(domainEvent.Payment.OrderId);
    }
}

The Domain Events Raiser

Now that we understand how events are fired and handled, let's see how the events raiser is implemented:

C#
/// <summary>
/// Simple domain events raiser that is functional, but doesn't support deferred execution
/// </summary>
class DomainEventsRaiser : IDomainEventsRaiser
{
    /// <summary>
    /// Locator of event handlers
    /// </summary>
    private readonly IServiceProvider _locator;

    internal DomainEventsRaiser(IServiceProvider locator)
    {
        _locator = locator;
    }

    /// <summary>
    /// Raises the given domain event
    /// </summary>
    /// <typeparam name="T">Domain event type</typeparam>
    /// <param name="domainEvent">Domain event</param>
    public void Raise<T>(T domainEvent) where T : IDomainEvent
    {
        //Get all the handlers that handle events of type T
        IHandles<T>[] allHandlers = (IHandles<T>[])_locator.GetService(typeof(IHandles<T>[]));

        if (allHandlers != null && allHandlers.Length > 0)
            foreach (var handler in allHandlers)
                handler.Handle(domainEvent);
    }
}

As we can see, a locator finds all the handlers that handle a specific event, then fires each handler sequentially. The domain events raiser is also created by the dependency container. It allows services to have its reference on their constructors.

These same principles could be used for other needs, like CQRS to handle commands.

Convention-Based Registration

You might want to use convention-based registration to services that are a part of the same concept, have similar structure and follow a same pattern of registration and resolving. A more "manual" registration can be more interesting for services that don't fall on the first rule and require more specific needs.

We will be using Unity on this section, but any decent container should give you similar capabilities.

Container.RegisterTypes(new EventHandlersConvention());

This will make the container register all classes that implement IHandles<T>. That allows the DomainEventsRaiser to use a locator that find the handlers for a specific event.

C#
    /// <summary>
    /// Register the conventions that allows domain events to be raised and handled
    /// </summary>
    class EventHandlersConvention : RegistrationConvention
    {
        public override Func<Type, IEnumerable<Type>> GetFromTypes()
        {
            return WithMappings.FromAllInterfaces;
        }
 
        public override Func<Type, IEnumerable<InjectionMember>> GetInjectionMembers()
        {
            return (t) => new InjectionMember[0];
        }
 
        public override Func<Type, LifetimeManager> GetLifetimeManager()
        {
            return WithLifetime.Transient;
        }
 
        public override Func<Type, string> GetName()
        {
            return WithName.TypeName;
        }
 
        public override IEnumerable<Type> GetTypes()
        {
            Type handlerType = typeof(IHandles<>);
 
            return AllClasses.FromLoadedAssemblies(skipOnError: false).
                        Where(t => !t.IsAbstract &&
                            t.GetInterfaces().Any(i => i.IsGenericType && 
                            i.GetGenericTypeDefinition().Equals(handlerType)));
        }
    }

Important: The lifetime of DomainEventsRaiser is not transient. It can be singleton, but I suggest scoped to the duration of a full transaction, since the state of this object (the events queue that we will see on the next section) is not reusable between one transaction and another.

Adding Deferred Execution Support

We already have a full working example of domain events that are registered using conventions. Now let's add a teaspoon of real life.

Let's say, once the order is paid, an e-mail is sent to the customer to inform that his payment has been processed. This is what our process now looks like:

Image 2

Created with Balsamiq Mockups.

We will be handling e-mail sending using the domain events raiser that we proposed for domain events. However, the e-mail should be sent only after the transaction is committed.

So, in our IHandles interface, we add a Deferred property:

C#
/// <summary>
/// Handles an event. If there is a database transaction, the
/// execution is delayed until the transaction is complete.
/// </summary>
/// <typeparam name="T"></typeparam>
public interface IHandles<T> where T : IDomainEvent
{
    void Handle(T domainEvent);

    bool Deferred { get; }
}

One single class can handle many events. NotificationsHandler demonstrates how this behavior could be achieved:

C#
public class NotificationsHandler : IHandles<OrderPaidEvent>,
          IHandles<OrderShippedEvent> //<- multiple events handled by a single handler
{
    private readonly IUserNotifier _notifier;

    public NotificationsHandler
              (IUserNotifier notifier) // <-- instantiated by the dependencies container
    {
        _notifier = notifier;
    }

    public bool Deferred { get { return true; } } //<- 'true' here indicates that
       //all these events should be invoked only after the transaction is committed

    public void Handle(OrderPaidEvent domainEvent)
    {
        _notifier.Notify("Yay! Your payment has been processed.");
    }

    public void Handle(OrderShippedEvent domainEvent)
    {
        _notifier.Notify(string.Format("Your order has finally been shipped.
                         Address : \"{0}\"", domainEvent.Order.ShipmentAddress));
    }
}

DeferredDomainEventsRaiser

And DeferredDomainEventsRaiser replaces the simple DomainEventsRaiser that I mentioned earlier. It adds deferred handlers in a Queue and dispatches them once the transaction is committed. Please notice that:

  • Not all events are deferred. Only those that have IHandles.Deferred = true.
  • In addition, events are never deferred if there is no transaction in progress.
C#
/// <summary>
/// Domain events handler that supports deferred execution
/// </summary>
class DeferredDomainEventsRaiser : IDomainEventsRaiser
{
    /// <summary>
    /// Locator of event handlers
    /// </summary>
    private readonly IServiceProvider _resolver;
    /// <summary>
    /// Collection of events queued for later execution
    /// </summary>
    private readonly ConcurrentQueue<Action> _pendingHandlers = new ConcurrentQueue<Action>();
    /// <summary>
    /// Data access state manager
    /// </summary>
    private readonly IDbStateTracker _dbState;

    public DeferredDomainEventsRaiser(IServiceProvider resolver, IDbStateTracker dbState)
    {
        _resolver = resolver;
        _dbState = dbState;

        _dbState.TransactionComplete += this.Flush;
        _dbState.Disposing += this.FlushOrClear;
    }

    /// <summary>
    /// Raises the given domain event
    /// </summary>
    /// <typeparam name="T">Domain event type</typeparam>
    /// <param name="domainEvent">Domain event</param>
    public void Raise<T>(T domainEvent) where T : IDomainEvent
    {
        //Get all the handlers that handle events of type T
        IHandles<T>[] allHandlers =
        (IHandles<T>[])_resolver.GetService(typeof(IHandles<T>[]));

        if (allHandlers != null && allHandlers.Length > 0)
        {
            IHandles<T>[] handlersToEnqueue = null;
            IHandles<T>[] handlersToFire = allHandlers;

            if (_dbState.HasPendingChanges())
            {
                //if there is a transaction in progress, events are enqueued to be executed later

                handlersToEnqueue = allHandlers.Where(h => h.Deferred).ToArray();

                if (handlersToEnqueue.Length > 0)
                {
                    lock (_pendingHandlers)
                        foreach (var handler in handlersToEnqueue)
                            _pendingHandlers.Enqueue(() => handler.Handle(domainEvent));

                    handlersToFire = allHandlers.Except(handlersToEnqueue).ToArray();
                }
            }

            foreach (var handler in handlersToFire)
                handler.Handle(domainEvent);
        }
    }

    /// <summary>
    /// Fire all the events in the queue
    /// </summary>
    private void Flush()
    {
        Action dispatch;
        lock (_pendingHandlers)
            while (_pendingHandlers.TryDequeue(out dispatch))
                dispatch();
    }

    /// <summary>
    /// Execute all pending events if there is no open transaction.
    /// Otherwise, clears the queue without executing them
    /// </summary>
    private void FlushOrClear()
    {
        if (!_dbState.HasPendingChanges())
            Flush();
        else
        {
            //If the state manager was disposed with a transaction in progress, we clear
            //the queue without firing the events because this flow is pretty inconsistent
            //(it could be caused, for instance, by an unhandled exception)
            Clear();
        }
    }

    /// <summary>
    /// Clear the pending events without firing them
    /// </summary>
    private void Clear()
    {
        Action dispatch;
        lock (_pendingHandlers)
            while (_pendingHandlers.TryDequeue(out dispatch)) ;
    }
}

This new interface IDbStateTracker simply exposes some events about the state of the database and it could be implemented by a UnitOfWork (see the source code attached to this post, also available on GitHub).

C#
/// <summary>
/// Tracks the database's state
/// </summary>
public interface IDbStateTracker : IDisposable
{
    /// <summary>
    /// Triggered when disposing
    /// </summary>
    event Action Disposing;

    /// <summary>
    /// Triggered when the transaction is completed
    /// </summary>
    event Action TransactionComplete;

    /// <summary>
    /// Returns true if there are uncommitted pending changes.
    /// Otherwise, returns false.
    /// </summary>
    bool HasPendingChanges();
}

Putting It All Together

This approach allows event handlers to be registered automatically by using conventions. Also, the same design allows event handlers to be fired online or have their execution delayed until the transaction is over. The source code attached to this article (also available on GitHub) contains a full working example of the principles discussed here.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Canada Canada
Software architect, Machine learning engineer, full stack cloud developer, MSc in Machine Learning, Microsoft Certified Solutions Developer (MCSD).

Comments and Discussions

 
QuestionHow Rollback for all if one Handler failed ? Pin
jpanel@hotmail.com26-Oct-19 7:25
jpanel@hotmail.com26-Oct-19 7:25 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.