1. Dotnet9首页
  2. .NET
  3. ASP.NET Core

30:领域事件:提升业务内聚,实现模块解耦-开发实战

.NET Core开发实战前文传送门:

30 | 领域事件:提升业务内聚,实现模块解耦

我们在领域的抽象层定义了领域事件和领域事件处理的接口

IDomainEvent

namespace GeekTime.Domain
{
    public interface IDomainEvent : INotification
    {
    }
}

这是一个空接口,它只是标记出来某一个对象是否是领域事件,INotification 也是一个空接口,它是 MediatR 框架的一个接口,是用来实现事件传递用的

namespace MediatR
{
    public interface INotification
    {
    }
}

接着是 IDomainEventHandler

namespace GeekTime.Domain
{
    public interface IDomainEventHandler<TDomainEvent> : INotificationHandler<TDomainEvent>
        where TDomainEvent : IDomainEvent
    {
        //这里我们使用了INotificationHandler的Handle方法来作为处理方法的定义
        //Task Handle(TDomainEvent domainEvent, CancellationToken cancellationToken);
    }
}

同样这个接口也是继承了 IDomainEventHandler 接口,它有一个泛型参数是 TDomainEvent,这个 TDomainEvent 约束必须为 IDomainEvent,也就是说处理程序只处理 IDomainEvent 作为入参

实际上该方法已经在 INotificationHandler 中定义好了,所以这里不需要重新定义,只是告诉大家它的定义是什么样子的

在 Entity 中对领域事件代码的处理

private List<IDomainEvent> _domainEvents;
public IReadOnlyCollection<IDomainEvent> DomainEvents => _domainEvents?.AsReadOnly();

public void AddDomainEvent(IDomainEvent eventItem)
{
    _domainEvents = _domainEvents ?? new List<IDomainEvent>();
    _domainEvents.Add(eventItem);
}

public void RemoveDomainEvent(IDomainEvent eventItem)
{
    _domainEvents?.Remove(eventItem);
}

public void ClearDomainEvents()
{
    _domainEvents?.Clear();
}

将领域事件做一个实体的属性存储进来,它应该是一个列表,因为在一个实体操作过程中间可能会发生多件事情,领域事件应该是可以被实体模型之外的代码读到,所以暴露一个 ReadOnly 的 Collection

这里还提供几个方法:添加领域事件,移除领域事件,清除领域事件

这些方法都是在领域模型内部进行调用的

可以看一下之前定义的 Order

public Order(string userId, string userName, int itemCount, Address address)
{
    this.UserId = userId;
    this.UserName = userName;
    this.Address = address;
    this.ItemCount = itemCount;

    this.AddDomainEvent(new OrderCreatedDomainEvent(this));
}

public void ChangeAddress(Address address)
{
    this.Address = address;
    //this.AddDomainEvent(new OrderAddressChangedDomainEvent(this));
}

当我们构造一个全新的 Order 的时候,实际上这里可以定义一个事件叫做 OrderCreatedDomainEvent,这个领域事件它的构造函数的入参就是一个 Order,当我们调用 Order 的构造函数时,实际上我们的行为就是在创建一个全新的 Order,所以在这里添加一个事件 AddDomainEvent

同理的比如说 ChangeAddress 被调用了,我们在这里实际上可以定义一个 OrderAddressChangedDomainEvent 类似这样子的领域事件出来

大家可以看到领域事件的构造和添加都应该是在领域模型的方法内完成的,而不应该是被外界的代码去调用创建,因为这些事件都是领域模型内部发生的事件

接着看看 OrderCreatedDomainEvent 的定义

namespace GeekTime.Domain.Events
{
    public class OrderCreatedDomainEvent : IDomainEvent
    {
        public Order Order { get; private set; }
        public OrderCreatedDomainEvent(Order order)
        {
            this.Order = order;
        }
    }
}

那我们如何处理我们的领域事件,接收领域事件的处理应该定义在应用层

namespace GeekTime.API.Application.DomainEventHandlers
{
    public class OrderCreatedDomainEventHandler : IDomainEventHandler<OrderCreatedDomainEvent>
    {
        ICapPublisher _capPublisher;
        public OrderCreatedDomainEventHandler(ICapPublisher capPublisher)
        {
            _capPublisher = capPublisher;
        }

        public async Task Handle(OrderCreatedDomainEvent notification, CancellationToken cancellationToken)
        {
            await _capPublisher.PublishAsync("OrderCreated", new OrderCreatedIntegrationEvent(notification.Order.Id));
        }
    }
}

它继承了 IDomainEventHandler,这个接口是上面讲到的领域事件处理器的接口,它的泛型入参就是要处理的事件的类型 OrderCreatedDomainEvent

为了简单演示起见,这里的逻辑是当我们创建一个新的订单时,我们向 EventBus 发布一条事件,叫做 OrderCreated 这个事件

我们在 OrderController 的 CreateOrder 定义了一个 CreateOrderCommand

[HttpPost]
public async Task<long> CreateOrder([FromBody]CreateOrderCommand cmd)
{
    return await _mediator.Send(cmd, HttpContext.RequestAborted);
}

CreateOrderCommand

namespace GeekTime.API.Application.Commands
{
    public class CreateOrderCommand : IRequest<long>
    {

        //ublic CreateOrderCommand() { }
        public CreateOrderCommand(int itemCount)
        {
            ItemCount = itemCount;
        }

        public long ItemCount { get; private set; }
    }
}

CreateOrderCommandHandler

public async Task<long> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
{

    var address = new Address("wen san lu", "hangzhou", "310000");
    var order = new Order("xiaohong1999", "xiaohong", 25, address);

    _orderRepository.Add(order);
    await _orderRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
    return order.Id;
}

我们在 CreateOrderCommandHandler 里面创建了一个 Order,然后保存进仓储,调用了 UnitOfWork 的 SaveEntitiesAsync

启动程序,直接执行,调用我们的方法,可以看到我们先进入到了创建订单的处理系统(CreateOrderCommandHandler),接着进入到了领域事件发布的 Publish 的代码(MediatorExtension),当仓储存储完毕之后,进入到了 OrderCreatedDomainEventHandler,也就是说我们在创建完我们的领域模型并将其保存之后,我们的领域事件的处理程序才触发

在之前讲解实现 UnitOfWork 的时候(EFContext),我们的 SaveEntitiesAsync 里面只有一行代码是 SaveChangesAsync,这里添加了一行代码,是发送领域事件的代码 DispatchDomainEventsAsync

public async Task<bool> SaveEntitiesAsync(CancellationToken cancellationToken = default)
{
    var result = await base.SaveChangesAsync(cancellationToken);
    //await _mediator.DispatchDomainEventsAsync(this);
    return true;
}

这就是 MediatorExtension 中看到的 DispatchDomainEventsAsync

namespace GeekTime.Infrastructure.Core.Extensions
{
    static class MediatorExtension
    {
        public static async Task DispatchDomainEventsAsync(this IMediator mediator, DbContext ctx)
        {
            var domainEntities = ctx.ChangeTracker
                .Entries<Entity>()
                .Where(x => x.Entity.DomainEvents != null && x.Entity.DomainEvents.Any());

            var domainEvents = domainEntities
                .SelectMany(x => x.Entity.DomainEvents)
                .ToList();

            domainEntities.ToList()
                .ForEach(entity => entity.Entity.ClearDomainEvents());

            foreach (var domainEvent in domainEvents)
                await mediator.Publish(domainEvent);
        }
    }
}

大家可以看到我们发送领域事件实际上是这么一个过程:我们从当前要保存的 EntityContext 里面去跟踪我们的实体,然后从跟踪到的实体的对象中获取到我们当前的 Event,如果 Event 是存在的,就把它取出来,然后将实体内的 Event 进行清除,再然后将这些 Event 逐条地通过中间件发送出去,并且找到对应的 Handler 处理

定义领域事件实际上也非常简单,只需要在领域模型创建一个 Events 的目录,然后将领域事件都定义在这里,领域事件需要继承 IDomainEvent,领域事件的处理器都定义在 DomainEventHandler,在应用层这个目录下面,我们可以为每一个事件都定义我们的处理程序

总结一下

领域模型内创建事件:我们不要在领域模型的外面去构造事件,然后传递给领域模型,因为整个领域事件是由领域的业务逻辑触发的,而不是说外面的对模型的操作触发的

另外就是针对领域事件应该定义专有的领域事件处理类,就像我们刚才演示的,在一个特定的目录,对每一个事件进行定义处理类

还有一个就是在同一个事务里面去处理我们的领域事件,实际上我们也可以选择在不同的事务里面处理,如果需要在不同的事务里面去处理领域事件的时候,我们就需要考虑一致性的问题,考虑中间出错,消息丢失的问题

0:领域事件:提升业务内聚,实现模块解耦-开发实战"
△扫码免费试看课程
0:领域事件:提升业务内聚,实现模块解耦-开发实战"

原文出处:微信公众号【郑子铭 DotNet NB】

原文链接:https://mp.weixin.qq.com/s/3560xWZkJvp3FNqvuYQlCg

本文观点不代表Dotnet9立场,转载请联系原作者。

发表评论

登录后才能评论