ABP - 本地事件总线
1. 事件总线
在我们的一个应用中,经常会出现一个逻辑执行之后要跟随执行另一个逻辑的情况,例如一个用户创建了后续还需要发送邮件进行通知,或者需要初始化相应的权限等。面对这样的情况,我们当然可以顺序进行相应的逻辑代码的编写,但这样会导致各种业务逻辑全部集中耦合在一个类中,违背了 "单一职责原则"。
在 ABP 框架中,对于上面的业务场景的处理,我们可以通过事件总线来解耦,使得代码逻辑实现更加清晰。事件总线的本质就是中介者模式,利用一个中介角色在发送方和接受方之间进行消息的传递,接受方单独实现关注的独立的小功能点,从而达到各块业务逻辑清晰,代码松散耦合的目的。
ABP 框架中的事件总线分为 本地事件总线 和 分布式事件总线 两种,两种使用的方式基本类似,只是分布式事件总线需要借助 RabbitMQ、Kafaka 等第三方消息队列中间件。本章先讲本地事件总线相关知识点。
2. 本地事件总线
本地事件总线实现进程内的事件的发布订阅功能,通常运用于单体应用架构或微服务架构中的一个服务内部。使用方式比较简单,以下是演示,也将通过控制台程序来进行。
2.1. 事件总线基本使用
本地事件总线的实现包含在 Volo.Abp.EventBus Nuget 包中,我们可以通过以下方式来集成。
通过以下命令创建一个控制台项目:
abp new AbpEventBus -t console
在 AbpEventBusSample.csproj 执行以下命令:
Abp add-package Volo.Abp.EventBus
如果是 Web 应用的话,在通过 ABP CLI 初始化启动模板的时候就已经集成了事件总线模块,无须再自己进行集成。
2.1.1 发布
ABP 提供了 ILocalEventBus 接口来满足我们对本地事件总线的使用。我们只需要在要进行事件发布的类注入该接口即可,之后就能通过以下的方式进行事件的发布了。
public class HelloWorldService : ITransientDependency
{
private readonly ILocalEventBus _localEventBus;
public HelloWorldService(ILocalEventBus localEventBus)
{
_localEventBus = localEventBus;
}
public Task SayHelloAsync()
{
// 当前业务逻辑
Console.WriteLine("Hello Jerry!");
// 关联业务逻辑
_localEventBus.PublishAsync(new HelloEventData
{
Who = "Tom",
ToWho = "Jerry",
Where = "广州天河正佳广场",
When = DateTime.Now
});
return Task.CompletedTask;
}
}
在进行事件发布之前需要先定义一个事件对象,这是一个普通类,是事件相关的各种数据的一个包装类,例如上面使用到的 HelloEventData 。
public class HelloEventData
{
public string Who { get; set; }
public string ToWho { get; set; }
public string Where { get; set; }
public DateTime When { get; set; }
}
就算在事件发布过程中不需要传输任何数据也需要创建一个类,在这种情况下为空类,这是因为事件总线是通过这个事件对象的类型来确定其对于的订阅者,从而执行相应的处理方法的。



通过源码可以看到,当我们调用 PublishAsync 方法时,最终时调用了 TriggerHandlersAsync 方法,该方法中从 HandlerFactories 中找到相应的的 HandlerFactory,然后通过 IEventHandlerInvoker 执行相应的事件执行器。


最终,就是反射创建了对于的 IEventHandlerMethodExecutor 对象,传入执行器和事件对象,再通过委托调用执行器的 HandleEventAsync 方法。
那么 HandlerFactories 是怎么来的呢?它实际上就是一个事件对象和执行器对应的集合。这里的工厂实际上并不是执行器工厂,它不负责执行器的创建,而是通过执行器生成执行器的包装类(为了类型对象能够释放销毁)。

在我们通过 PublishAsync 方法发布事件的时候,还可以通过 onUnitOfWorkComplete 参数设置事件发布是否和工作单元挂钩,实现事件和其他业务的原子性。其实这也很简单,就是结合工作单元的时候,只是将事件存储起来,没有立刻触发。



等到工作单元提交了,再通过 IUnitOfWorkEventPublisher 对象发布,而该接口在事件总线模块中有对于的实现类UnitOfWorkEventPublisher,其实就是工作单元提交时再次发布一次不结合工作单元的事件而已。


2.1.2 订阅
事件的订阅有多种方式。
(1) 实现 ILocalEventHandler<TEvent> 接口,并配置到容器
public class HelloEventHandler : ILocalEventHandler<HelloEventData>, ITransientDependency
{
public Task HandleEventAsync(HelloEventData eventData)
{
Console.WriteLine($"{eventData.Who} Say Hello To { eventData.ToWho } in { eventData.When } at { eventData.When }");
return Task.CompletedTask;
}
}

这种方式是最简便的方式,ABP 框架中的事件总线模块会在服务配置到容器时自动方向这些订阅者执行器。
从源码中可以看到,事件总线模块中注册了容器中依赖关系配置的拦截器(auto Ioc 容器的功能),在应用启动向容器中配置依赖关系的时候,这里的事件会触发,对每一个配置进行检查,通过接口类型查找到相应的实现类之后,会被保存到 选项 当中。

在 事件总线 构造函数会根据选项中保存的执行器注册订阅


实际上这里就是通过执行器实例创建了一个工厂类,并且将其添加到上面讲到的事件对象和执行器对应的集合 HandlerFactories 中,维护好事件对象类型与执行器的对于关系。


(2) 手动调用 ILocalEventBus 接口进行订阅
public Task SayHelloAsync()
{
// 当前业务逻辑
Console.WriteLine("Hello Jerry!");
_localEventBus.Subscribe<HelloEventData, HelloLogEventHandler>();
//_localEventBus.Subscribe(new HelloLogEventHandler());
//_localEventBus.Subscribe<HelloEventData>((eventData) => { return Task.CompletedTask; });
// 关联业务逻辑
_localEventBus.PublishAsync(new HelloEventData
{
Who = "Tom",
ToWho = "Jerry",
Where = "广州天河正佳广场",
When = DateTime.Now
});
return Task.CompletedTask;
}
public class HelloLogEventHandler : ILocalEventHandler<HelloEventData>
{
public Task HandleEventAsync(HelloEventData eventData)
{
Console.WriteLine($"Log: {eventData.Who} Say Hello To { eventData.ToWho } in { eventData.When } at { eventData.When }");
return Task.CompletedTask;
}
}

手动注册的订阅者会在调用注册代码之后全局生效,应该保存只有一次的订阅注册。
手动注册订阅者的时候,其实和上面自动注册的方式没太大区别,事件总线会根据我们注册订阅者的方式进行一定的包装,最终也是添加到事件对象和执行器对照的集合。

这些订阅者会在我们调用 ILocalEventBus 的 PublishAsync 方法对相关的事件进行发布之后触发。事件的发布订阅有以下特点:
事件可以由0个或多个处理程序订阅.
一个事件处理程序可以订阅多个事件,但是需要为每个事件实现 ILocalEventHandler 接口.
如果需要在订阅者执行器中执行数据库操作并且使用到仓储,那可能需要使用工作单元。因为一些存储库方法需要在活动的工作单元中工作。应确保处理方法设置为 virtual,并为该方法添加一个 [UnitOfWork] 特性,或者手动使用 IUnitOfWorkManager 创建一个工作单元范围。
当一个事件发布,订阅的事件处理程序将立即执行,而同时 PublishAsync 如果通过 await 关键字转同步的话,它将阻塞,直到事件处理程序执行完成。换句话说,本地事件总线事件发布与处理实际是立即触发,顺序执行的。
这意味着如果处理程序抛出一个异常,它会影响发布该事件的代码,我们可以在 PublishAsync 调用上捕捉异常。 如果想要隐藏错误,可以在事件处理程序中使用 try-catch。 如果在一个工作单元范围内进行事件发布,那么相应的事件处理程序也会被工作单元覆盖. 这意味着,如果你的 UOW 是事务和处理程序抛出一个异常,事务会回滚。
这从上面列出来的源码中也可以看出来,本地事件总线本质上就还是通过在维护好事件对象类型与执行器对照集合中通过事件对象查找执行器,然后调用执行器中的方法的过程。
2.2 预定义事件
实体的增、删、改是非常常见的操作,有些时候一些实体的增、删、改之后需要关联一些其他的业务逻辑,这时候我们可以通过事件总线来解决。ABP框架会为所有的实体自动发布这些事件,我们只需要订阅相关的事件。
sing 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;
//...
}
}
}
上面的例子是 ABP 官方的示例,订阅了 EntityCreatedEventData<Entity> 接口的事件处理程序会在相应的实体创建之后触发。这种和领域对象(实体、聚合根)操作相关的预定义事件有两类:
用过去时态事件
当相关工作单元完成且实体更改成功保存到数据库时,将发布带有过去时态的事件. 如果在这些事件处理程序上抛出异常,则无法回滚事务,因为事务已经提交.
事件类型;
EntityCreatedEventData<T>当实体创建成功后发布.EntityUpdatedEventData<T>当实体更新成功后发布.EntityDeletedEventData<T>当实体删除成功后发布.EntityChangedEventData<T>当实体创建,更新,删除后发布. 如果你需要监听任何类型的更改,它是一种快捷方式 - 而不是订阅单个事件.
用于进行时态事件(6.0版本可用,7.0版本已移除)
带有进行时态的事件在完成事务之前发布(如果数据库事务由所使用的数据库提供程序支持). 如果在这些事件处理程序上抛出异常,它会回滚事务,因为事务还没有完成,更改也没有保存到数据库中.
事件类型;
EntityCreatingEventData<T>当新实体保存到数据库前发布.EntityUpdatingEventData<T>当已存在实体更新到数据库前发布.EntityDeletingEventData<T>删除实体前发布.EntityChangingEventData<T>当实体创建,更新,删除前发布. 如果你需要监听任何类型的更改,它是一种快捷方式 - 而不是订阅单个事件.
它们是在将更改保存到数据库时发布预构建事件;
- 对于 EF Core, 他们在 DbContext.SaveChanges 发布.
- 对于 MongoDB, 在你调用仓储的 InsertAsync, UpdateAsync 或 DeleteAsync 方法发布(因为MongoDB没有更改追踪系统).
领域对象中是不能够通过依赖注入注入服务,在聚合根类中我们可以通过 AddLocalEvent 添加本地事件,实体类中则不行,这里添加的事件将在聚合根对象持久化操作的时候发布。
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;
//ADD an EVENT TO BE PUBLISHED
AddLocalEvent(
new StockCountChangedEvent
{
ProductId = Id,
NewCount = newCount
}
);
}
}
}
聚合根中可以通过 AddLocalEvent 方法添加事件,是因为 ABP 框架中的聚合根基类实现了 IGeneratesDomainEvents 接口,如果我们的实体类中也需要发布事件,也可以实现 IGeneratesDomainEvents 接口。但是 ABP 并不建议随意地普通的实体类实现该接口,因为基于 IGeneratesDomainEvents 的事件发布是基于特定的数据库提供程序的,目前 ABP 框架中仅支持 EF Core 、MongoDB 的实现。
通过源码可以看到,这些事件的发布是重写了 SaveChangeAsync 等数据持久化的方法,在其中根据 IGeneratesDomainEvents 接口添加了相应的领域对象的更改操作事件。


最终还是和上面的发布事件时的工作单元操作一样,先添加到工作单元中,在工作单元提交的时候由 IUnitOfWorkEventPublisher 对象发布。

参考文档:
ABP 官方文档 - 本地事件总线
ABP 系列总结:
目录:ABP 系列总结
上一篇:ABP - 缓存模块(2)
ABP - 本地事件总线的更多相关文章
- [Abp vNext 源码分析] - 13. 本地事件总线与分布式事件总线 (Rabbit MQ)
一.简要介绍 ABP vNext 封装了两种事件总线结构,第一种是 ABP vNext 自己实现的本地事件总线,这种事件总线无法跨项目发布和订阅.第二种则是分布式事件总线,ABP vNext 自己封装 ...
- ABP之事件总线(5)
前面已经对Castle Windsor的基本使用进行了学习,有了这个基础,接下来我们将把我们的事件总线再次向ABP中定义的事件总线靠近.从源码中可以知道在ABP中定义了Dictionary,存放三种类 ...
- ABP之事件总线(4)
在上一篇的随笔中,我们已经初步完成了EventBus,但是EventBus中还有诸多的问题存在,那么到底有什么问题呢,接下来我们需要看一看ABP中的源码是如何定义EventBus的. 1.第一个点 在 ...
- ABP之事件总线(3)
承接上一篇时间总线的学习,在上一篇中我们实现了取消显式注册事件的方式,采用使用反射的方式.这样的好处可以解除Publisher和Scriber的显式依赖,但是问题又来了,因为我们只有Publisher ...
- ABP之事件总线(1)
什么是事件总线呢?官方的文档说,它是一个单例对象,由其他的类共同拥有,可以用来触发和处理事件.这个东西确实比较陌生,为什么要使用事件总线,或者说事件总线的优势是什么???首先我们可以明确的是,事件总线 ...
- ABP的事件总线和领域事件(EventBus & Domain Events)
http://www.aspnetboilerplate.com/Pages/Documents/EventBus-Domain-Events EventBus EventBus是个单例,获得Even ...
- ABP之事件总线(2)
在上一篇文章中,我们复习了一下事件的经典的发布订阅模式,同时对是事件源和时间处理逻辑进行抽象统一,用起来也没有问题.但是还是有很多的问题,比如说我们Handle方法其实是违背了单一性的原则的,里面混杂 ...
- 浅入 ABP 系列(4):事件总线
浅入 ABP 系列(4):事件总线 版权护体作者:痴者工良,微信公众号转载文章需要 <NCC开源社区>同意. 目录 浅入 ABP 系列(4):事件总线 事件总线 关于事件总线 为什么需要这 ...
- 源码解析-Abp vNext丨分布式事件总线DistributedEventBus
前言 上一节咱们讲了LocalEventBus,本节来讲本地事件总线(DistributedEventBus),采用的RabbitMQ进行实现. Volo.Abp.EventBus.RabbitMQ模 ...
- [Abp 源码分析]九、事件总线
0.简介 事件总线就是订阅/发布模式的一种实现,本质上事件总线的存在是为了降低耦合而存在的. 从上图可以看到事件由发布者发布到事件总线处理器当中,然后经由事件总线处理器调用订阅者的处理方法,而发布者和 ...
随机推荐
- 经GitHub将kubernetes镜像推送到阿里云
背景 在安装kubernetes时会出现无法访问镜像站的情况,通过GitHub将kubernetes镜像推送到阿里云之后,即可使用阿里云地址引用所需镜像,现已同步镜像5000+,当前还在陆续同步.仓库 ...
- [数据库/MySQL]数据类型:enum 枚举类型
1 需求描述 场景 性别(gender) :男 / 女 / 保密 2 基本语法 enum(枚举值 1,枚举值 2...); 枚举值列表在 255 个以内,使用 1 个字节来存储 枚举值列表超过 255 ...
- [数据库/MySQL]数据库备份与升级:MySQL Percona(RPM) 5.7.24-27 升级到 5.7.31-34
1 数据库升级方式:RPM包方式升级 [亲测有效] 环境 OS: CENTOS 7 DB: MYSQL 5.7.24-27 1.1 数据库备份 备份以防止升级失败 备份数据库的2个主要方法: 1)用M ...
- blog图片资源
- 【Azure Developer】Azure AD 注册应用的 OAuth 2.0 v2 终结点获取的 Token 解析出来依旧为v1.0, 这是什么情况!
问题描述 使用 Azure AD 注册应用 Oauth2 v2.0的终结点(OAuth 2.0 token endpoint (v2):https://login.partner.microsofto ...
- CSS绘制虚线的方案
一.实现效果 二.代码实现 <div class="line"></div> .line { width: 1px; /* 虚线宽度 */ backgrou ...
- 【Python基础】数据类型与类型转换
五种基本数据类型 在 Python 中,基本数据类型是指不可变对象的数据类型.以下是 Python 中的基本数据类型: 整数类型(int):表示整数,例如 1.2.3 等等. 浮点数类型(float) ...
- 使用NineData定制企业级数据库规范
1. 为什么需要数据库规范? 在企业级应用中,数据库是非常重要的一部分,它们存储着公司的核心数据,包括客户信息.订单.产品信息等等.如果这些数据没有得到妥善的管理,那么就会导致数据不一致.数据丢失.数 ...
- 2022-07-30:以下go语言代码输出什么?A:[]byte{} []byte;B:[]byte{} []uint8;C:[]uint8{} []byte;D:[]uin8{} []uint8。
2022-07-30:以下go语言代码输出什么?A:[]byte{} []byte:B:[]byte{} []uint8:C:[]uint8{} []byte:D:[]uin8{} []uint8. ...
- 2021-04-01:给定一个正方形矩阵matrix,原地调整成顺时针90度转动的样子。[[a,b,c],[d,e,f],[g,h,i]]变成[[g,d,a],[h,e,b],[i,f,c]]。
2021-04-01:给定一个正方形矩阵matrix,原地调整成顺时针90度转动的样子.[[a,b,c],[d,e,f],[g,h,i]]变成[[g,d,a],[h,e,b],[i,f,c]]. 福大 ...