项目

本地事件总线

本地事件总线允许服务发布和订阅进程内事件。这意味着当两个服务(发布者和订阅者)运行在同一进程内时,此机制非常适合。

发布事件

以下章节将介绍两种发布本地事件的方法。

使用ILocalEventBus发布事件

可通过依赖注入获取ILocalEventBus实例,并用于发布本地事件。

示例:当商品库存数量变化时发布本地事件

using System;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus.Local;

namespace AbpDemo
{
    public class MyService : ITransientDependency
    {
        private readonly ILocalEventBus _localEventBus;

        public MyService(ILocalEventBus localEventBus)
        {
            _localEventBus = localEventBus;
        }
        
        public virtual async Task ChangeStockCountAsync(Guid productId, int newCount)
        {
            //TODO: 实现业务逻辑...
            
            //发布事件
            await _localEventBus.PublishAsync(
                new StockCountChangedEvent
                {
                    ProductId = productId,
                    NewCount = newCount
                }
            );
        }
    }
}

PublishAsync方法接收单个参数:事件对象,该对象负责保存与事件相关的数据。它是一个简单的普通类:

using System;

namespace AbpDemo
{
    public class StockCountChangedEvent
    {
        public Guid ProductId { get; set; }
        
        public int NewCount { get; set; }
    }
}

即使不需要传输任何数据,也需要创建一个类(此时为一个空类)。

在实体/聚合根类内部发布事件

实体无法通过依赖注入获取服务,但在实体/聚合根类内部发布本地事件是非常常见的需求。

示例:在聚合根方法中发布本地事件

using System;
using Volo.Abp.Domain.Entities;

namespace AbpDemo
{
    public class Product : AggregateRoot<Guid>
    {
        public string Name { get; set; }
        
        public int StockCount { get; private set; }

        private Product() { }

        public Product(Guid id, string name)
            : base(id)
        {
            Name = name;
        }

        public void ChangeStockCount(int newCount)
        {
            StockCount = newCount;
            
            //添加待发布的事件
            AddLocalEvent(
                new StockCountChangedEvent
                {
                    ProductId = Id,
                    NewCount = newCount
                }
            );
        }
    }
}

AggregateRoot类定义了AddLocalEvent方法用于添加新的本地事件,该事件将在聚合根对象保存到数据库时(创建、更新或删除操作后)被发布。

提示:如果实体发布此类事件,最佳实践是以受控方式修改相关属性,正如上例所示——StockCount只能通过ChangeStockCount方法修改,这确保了事件的发布。

IGeneratesDomainEvents接口

实际上,添加本地事件并非AggregateRoot类独有。任何实体类均可实现IGeneratesDomainEvents接口。但AggregateRoot默认实现了该接口,为您提供了便利。

不建议为非聚合根的实体实现此接口,因为某些数据库提供程序可能不支持此类实体的该功能。例如,EF Core支持,但MongoDB不支持。

实现机制

调用AddLocalEvent不会立即发布事件。事件将在保存数据库变更时发布:

  • 对于EF Core,在DbContext.SaveChanges时发布。
  • 对于MongoDB,在调用存储库的InsertAsyncUpdateAsyncDeleteAsync方法时发布(因为MongoDB没有变更追踪系统)。

订阅事件

服务可通过实现ILocalEventHandler<TEvent>接口来处理事件。

示例:处理上述定义的StockCountChangedEvent

using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus;

namespace AbpDemo
{
    public class MyHandler
        : ILocalEventHandler<StockCountChangedEvent>,
          ITransientDependency
    {
        public async Task HandleEventAsync(StockCountChangedEvent eventData)
        {
            //TODO: 编写事件触发时执行的业务代码
        }
    }
}

至此完成。ABP会自动发现MyHandler,每当StockCountChangedEvent发生时就会调用HandleEventAsync方法。您可以在处理程序中注入任何服务并执行所需逻辑。

  • 一个或多个处理程序可以订阅同一事件。
  • 单个事件处理程序类可通过为每种事件类型实现ILocalEventHandler<TEvent>接口来订阅多个事件

如果在事件处理程序中执行数据库操作并使用存储库,可能需要创建一个工作单元,因为某些存储库方法需要在活动的工作单元中运行。将处理方法标记为virtual并为方法添加[UnitOfWork]特性,或手动使用IUnitOfWorkManager创建工作单元范围。

处理程序类必须注册到依赖注入(DI)容器。上例使用ITransientDependency实现此目的。更多选项请参阅DI文档

LocalEventHandlerOrder特性

LocalEventHandlerOrder特性可用于设置事件处理程序的执行顺序,这在需要按特定顺序处理事件时非常有用。

[LocalEventHandlerOrder(-1)]
public class MyHandler
    : ILocalEventHandler<StockCountChangedEvent>,
      ITransientDependency
{
    public async Task HandleEventAsync(StockCountChangedEvent eventData)
    {
        //TODO: 编写事件触发时执行的业务代码
    }
}

默认情况下,所有事件处理程序的顺序值为0。因此,如果希望某些事件处理程序先于其他处理程序执行,可将顺序值设置为负数。

LocalEventHandlerOrderAttribute属性

  • Order (int): 用于设置特定事件处理程序的执行顺序。

事务与异常行为

事件处理程序始终在与发布事件的代码相同的工作单元范围内执行,这意味着它们处于同一数据库事务中。如果事件处理程序抛出异常,工作单元(数据库事务)将回滚。因此,如果希望隐藏错误,请在事件处理程序中自行使用try-catch

调用ILocalEventBus.PublishAsync时,事件处理程序不会立即执行。相反,它们会在当前工作单元完成前执行(处理程序中的未处理异常仍会导致当前工作单元回滚)。如果需要立即执行处理程序,可将可选的onUnitOfWorkComplete参数设为false

除非有特殊需求,否则建议保持默认行为。在实体/聚合根类内部发布事件时(参见在实体/聚合根类内部发布事件章节),onUnitOfWorkComplete选项不可用。

预构建事件

在实体创建、更新和删除操作时发布事件是非常常见的需求。ABP自动为所有实体发布这些事件。您只需订阅相关事件即可。

示例:订阅用户创建时发布的事件

using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Entities.Events;
using Volo.Abp.EventBus;

namespace AbpDemo
{
    public class MyHandler
        : ILocalEventHandler<EntityCreatedEventData<IdentityUser>>,
          ITransientDependency
    {
        public async Task HandleEventAsync(
            EntityCreatedEventData<IdentityUser> eventData)
        {
            var userName = eventData.Entity.UserName;
            var email = eventData.Entity.Email;
            //...
        }
    }
}

此类订阅了EntityCreatedEventData<IdentityUser>事件,该事件在用户创建后(但在当前事务完成前)发布。例如,您可能希望向新用户发送"欢迎"邮件。

预构建事件类型包括:

  • EntityCreatedEventData<T>:在实体成功创建后发布。
  • EntityUpdatedEventData<T>:在实体成功更新后发布。
  • EntityDeletedEventData<T>:在实体成功删除后发布。
  • EntityChangedEventData<T>:在实体成功创建、更新或删除后发布。如果需要监听任何类型的变更(而不是单独订阅每个事件),这是一个快捷方式。

实现机制

预构建事件在保存数据库变更时发布:

  • 对于EF Core,在DbContext.SaveChanges时发布。
  • 对于MongoDB,在调用存储库的InsertAsyncUpdateAsyncDeleteAsync方法时发布(因为MongoDB没有变更追踪系统)。

AbpEntityChangeOptions

AbpEntityChangeOptions类中有一个PublishEntityUpdatedEventWhenNavigationChanges选项,默认值为true。 如果将其设置为false,当导航属性变更时不会发布EntityUpdatedEventData<T>

此选项仅用于EF Core。

另请参阅

在本文档中