如何发布和处理领域事件

原文链接

系列文章目录

一、简单的CQRS实现与原始SQL和DDD
二、使用EF的领域模型的封装和持久化透明(PI)
三、REST API数据验证
四、领域模型验证
五、如何发布和处理领域事件
六、处理领域事件:缺失的部分
七、发件箱模式(The Outbox Pattern)
八、.NET Core中的旁路缓存模式(Cache-Aside Pattern)

引言

领域事件(Domain Event)是领域驱动设计的构建块之一。它捕获了特定领域中发生的一些事情的内存。我们创建领域事件来通知相同领域的其他部分发生了一些有趣的事情,这些其他部分可能会对此做出反应。

领域事件通常是用过去时态命名的不可变数据容器类。例如:

public class OrderPlaced
{
    public Order Order { get; }

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

发布领域事件的三种方式

1 . 使用静态的DomainEvents类

这种方法是由Udi Dahan在他的Domain Events Salvation帖子中提出的。简而言之,有一个名为DomainEvents的静态类,其方法为Raise,当聚合方法处理过程中发生一些有趣的事情时,立即调用它。值得强调一下单词 immediately,因为所有领域事件处理程序也会立即开始处理(甚至聚合方法也没有完成处理)。

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

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

2. 引发从聚合方法返回的事件

这是聚合方法直接将领域事件返回给ApplicationService的方法。ApplicationService决定何时以及如何引发事件。你可以阅读Jan Kronquist的Don’t publish Domain Events, return them!(不要发布域事件,返回它们!)帖子来熟悉这种引发事件的方式。

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 events = shop.PlaceOrder(order);

        eventsPublisher.Publish(events);
    }
}

3. 将事件添加到事件实体集合

这样,在创建领域事件的每个实体上都存在事件集合。在聚合方法执行期间,将每个领域事件实例添加到此集合。执行后,ApplicationService(或其他组件)从所有实体中读取所有事件集合并发布它们。Jimmy Bogard在A better domain events pattern(一个更好的领域事件模式)中详细描述了这种方法。

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 events = this.GetEvents(); // for example from EF DbContext

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

处理领域事件

处理域事件的方式间接依赖于发布方法。如果使用DomainEvents静态类,则必须立即处理事件。在另外两种情况下,您可以控制事件何时发布以及处理程序的执行——在现有事务内部或外部。

如何发布和处理领域事件_第1张图片

在我看来,比较好的做法是,总是在现有事务中处理领域事件,并将聚合方法执行和处理程序处理视为原子操作。因为如果你有很多事件和处理程序,你就不必考虑哪些需要初始化连接、事务以及应该以“全有或全无(all-or-nothing)”的方式处理,哪些又不需要这样处理。

但是,有时需要基于领域事件与第三方服务(例如电子邮件或Web服务)进行通信。正如我们所知,与第三方服务的通信通常不是事务性的,因此我们需要一些额外的通用机制来处理这些类型的场景。所以我创建了领域事件通知。

领域事件通知

DDD术语中没有领域事件通知。我用这个名字是因为我认为它最合适——它是发布领域事件的通知。

机制很简单。如果我想通知我的应用程序领域事件已经发布,我为它创建通知类,并为这个通知创建尽可能多的处理程序。我总是在事务提交后发布我的通知。完整的过程如下所示:

  1. 创建数据库事务
  2. 获取聚合
  3. 调用聚合方法
  4. 将领域事件添加到事件集合
  5. 发布领域事件并处理它们
  6. 将更改保存到DB并提交事务
  7. 发布领域事件通知并处理它们

我如何知道特定的域事件已发布?

首先,我必须使用泛型定义域事件的通知:

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)
    {
    }
}

所有通知都注册在IoC容器中:

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

在EventsPublisher中,我们使用IoC容器解析已定义的通知,在我们的工作单元完成后,所有通知都会被发布:

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);

这是整个过程在UML时序图中呈现的样子:

如何发布和处理领域事件_第2张图片

你可以认为有很多东西要记住,你是对的!但正如你所看到的,整个过程非常简单,我们可以使用IoC拦截器(interceptors)简化这个解决方案,我将在另一篇文章中描述。

总结

  1. 领域事件是建模领域中过去发生的事件信息,是DDD方法的重要组成部分。
  2. 发布和处理领域事件的方法有很多种——通过静态类、返回它们、通过集合公开。
  3. 领域事件应该在现有事务中处理(我的建议)
  4. 对于非事务性操作,引入了领域事件通知

你可能感兴趣的:(C#,DDD)