How to publish and handle domain events

Original link

introduction

Domain Event s are one of the building blocks of Domain Driven Design. It captures the memory of something happening in a particular domain. We create domain events to notify other parts of the same domain that something interesting happened, and these other parts may react to it.

Domain events are usually immutable data container classes named in the past tense. For example:

public class OrderPlaced
{
    public Order Order { get; }

    public OrderPlaced(Order order)
    {
        this.Order = order;
    }
}

Three ways to publish domain events

1. Use the static DomainEvents class

This approach was proposed by Udi Dahan in his Domain Events Salvation post. In short, there is a static class called DomainEvents with a method Raise that is called as soon as something interesting happens during the processing of the aggregate method. It's worth emphasizing the word immediately, because all domain event handlers also start processing immediately (even aggregate methods don't finish processing).

public class Shop
{
    public void PlaceOrder(Order order)
    {
        // business logic....

        DomainEvents.Raise(new OrderPlaced(order));
    }
}

2. Raise the event returned from the aggregate method

This is where aggregate methods return domain events directly to the ApplicationService. ApplicationService decides when and how to raise events. You can read Jan Kronquist's Don’t publish Domain Events, return them! (Don’t publish Domain Events, return them!) post to familiarize yourself with this way of raising events.

public List<IDomainEvent> PlaceOrder(Order order)
{
    // business logic....

    List<IDomainEvent> events = new List<IDomainEvent>();
    events.Add(new OrderPlaced(order));

    return events;
}
public class ShopApplicationService
{
    private readonly IOrderRepository orderRepository;
    private readonly IShopRepository shopRepository;
    private readonly IEventsPublisher eventsPublisher;
        
    public void PlaceOrder(int shopId, int orderId)
    {
        Shop shop = this.shopRepository.GetById(shopId);
        Order order = this.orderRepository.GetById(orderId);

        List<IDomainEvent> events = shop.PlaceOrder(order);

        eventsPublisher.Publish(events);
    }
}

3. Add the event to the event entity collection

This way, there is an event collection on every entity that creates domain events. During the execution of the aggregate method, each domain event instance is added to this collection. After execution, the ApplicationService (or other component) reads all event collections from all entities and publishes them. Jimmy Bogard at A better domain events pattern (a better domain event pattern) This method is described in detail in .

public abstract class EntityBase
{
    ICollection<IDomainEvent> Events { get; }
}

public class Shop : EntityBase
{
    public List<IDomainEvent> PlaceOrder(Order order)
    {
        // business logic....

        Events.Add(new OrderPlaced(order));
    }
}
public class ShopApplicationService
{
    private readonly IOrderRepository orderRepository;
    private readonly IShopRepository shopRepository;
    private readonly IEventsPublisher eventsPublisher;
        
    public void PlaceOrder(int shopId, int orderId)
    {
        Shop shop = this.shopRepository.GetById(shopId);
        Order order = this.orderRepository.GetById(orderId);

        shop.PlaceOrder(order);

        eventsPublisher.Publish();
    }
}
public class EventsPublisher : IEventsPublisher
{
    public void Publish()
    {
        List<IDomainEvent> events = this.GetEvents(); // for example from EF DbContext

        foreach (IDomainEvent @event in events)
        {
            this.Publish(@event);
        }
    }
}

Handle Domain Events

The way domain events are handled depends indirectly on the publishing method. If you use the DomainEvents static class, you must handle the event immediately. In the other two cases, you can control when the event is published and the execution of the handler - inside or outside of an existing transaction.

Better practice, in my opinion, is to always handle domain events within an existing transaction and treat aggregate method execution and handler processing as atomic operations. Because if you have a lot of events and handlers, you don't have to think about which ones need to initialize connections, transactions, and should be handled in an "all-or-nothing" way, and which ones don't.

However, sometimes there is a need to communicate with third-party services such as email or Web services based on domain events. As we know, communication with third-party services is usually not transactional, so we need some additional generic mechanisms to handle these types of scenarios. So I created Realm Event Notification.

Domain Event Notification

There is no domain event notification in DDD terms. I use this name because I think it's the most appropriate - it's posting notifications of domain events.

The mechanics are simple. If I want to notify my application that a domain event has been published, I create notification classes for it and as many handlers as possible for this notification. I always post my notifications after the transaction commits. The complete process looks like this:

  1. Create database transaction
  2. get aggregation
  3. call aggregate method
  4. Add domain events to event collection
  5. Publish domain events and handle them
  6. Save changes to DB and commit transaction
  7. Publish domain event notifications and handle them

How do I know that a specific domain event has been published?

First, I had to define notifications for domain events using generics:

public interface IDomainEventNotification<out TEventType> where TEventType:IDomainEvent
{
    TEventType DomainEvent { get; }
}

public class DomainNotificationBase<T> : IDomainEventNotification<T> where T:IDomainEvent
{
    public T DomainEvent { get; }

    public DomainNotificationBase(T domainEvent)
    {
        this.DomainEvent = domainEvent;
    }
}

public class OrderPlacedNotification : DomainNotificationBase<OrderPlaced>
{
    public OrderPlacedNotification(OrderPlaced orderPlaced) : base(domainEvent)
    {
    }
}

All notifications are registered in the IoC container:

protected override void Load(ContainerBuilder builder)
{
    builder.RegisterAssemblyTypes(typeof(IDomainEvent).GetTypeInfo().Assembly)
    .AsClosedTypesOf(typeof(IDomainEventNotification<>));
}

In EventsPublisher, we use the IoC container to resolve the defined notifications, and after our unit of work completes, all notifications are published:

var domainEventNotifications = new List<IDomainEventNotification<IDomainEvent>>();
foreach (var domainEvent in domainEvents)
{
    Type domainEvenNotificationType = typeof(IDomainEventNotification<>);
    var domainNotificationWithGenericType = domainEvenNotificationType.MakeGenericType(domainEvent.GetType());
    var domainNotification = _scope.ResolveOptional(domainNotificationWithGenericType, new List<Parameter>
    {
        new NamedParameter("domainEvent", domainEvent)
    });

    if (domainNotification != null)
    {
        domainEventNotifications.Add(domainNotification as IDomainEventNotification<IDomainEvent>);
    }             
}

var tasks = domainEventNotifications
    .Select(async (notification) =>
    {
        await _mediator.Publish(notification, cancellationToken);
    });

await Task.WhenAll(tasks);

This is how the whole process looks like in a UML sequence diagram:

You can think that there are a lot of things to keep in mind, and you are right! But as you can see, the whole process is very simple, and we can simplify this solution using IoC interceptors, which I will discuss in another article describe.

Summarize

  1. Domain events are past event information in the modeling domain, which is an important part of the DDD method.
  2. There are many ways to publish and handle domain events - through static classes, returning them, exposing through collections.
  3. Domain events should be handled within an existing transaction (my suggestion)
  4. For non-transactional operations, domain event notifications are introduced

Tags: C#

Posted by ifm1989 on Mon, 23 Jan 2023 21:47:41 +0300