概述

事件总线是一种事件发布/订阅结构,通过发布订阅模式可以解耦不同架构层级,同样它也可以来解决业务之间的耦合,它有以下优点

  • 松耦合
  • 横切关注点
  • 可测试性
  • 事件驱动

发布订阅模式

通过下图我们可以快速了解发布订阅模式的本质

  1. 订阅者将自己关心的事件在调度中心进行注册
  2. 事件的发布者通过调度中心把事件发布出去
  3. 订阅者收到自己关心的事件变更并执行相对应业务

其中发布者无需知道订阅者是谁,订阅者彼此之间也互不认识,彼此之间互不干扰

事件总线类型

在Masa Framework中,将事件划分为

本地事件,它的发布与订阅需要在同一个进程中,订阅方与发布方需要在同一个项目中

集成事件,它的发布与订阅一定不在同一个进程中,订阅方与发布方可以在同一个项目中,也可以在不同的项目中

下面我们会用一个注册用户的例子来说明如何使用本地事件

入门

  1. 新建ASP.NET Core 空项目Assignment.InProcessEventBus,并安装Masa.Contrib.Dispatcher.Events
dotnet new web -o Assignment.InProcessEventBus
cd Assignment.InProcessEventBus
dotnet add package Masa.Contrib.Dispatcher.Events --version 0.7.0-preview.7
  1. 注册EventBus (用于发布本地事件), 修改Program.cs
builder.Services.AddEventBus();
  1. 新增RegisterUserEvent类并继承Event,用于发布注册用户事件
public record RegisterEvent : Event
{
public string Account { get; set; } public string Email { get; set; } public string Password { get; set; }
}
  1. 新增注册用户处理程序

在指定事件处理程序方法上增加特性 EventHandler,并在方法中增加参数 RegisterUserEvent

public class UserHandler
{
private readonly ILogger<UserHandler>? _logger; public UserHandler(ILogger<UserHandler>? logger = null)
{
//todo: 根据需要可在构造函数中注入其它服务 (需支持从DI获取)
_logger = logger;
} [EventHandler]
public void RegisterUser(RegisterUserEvent @event)
{
//todo: 1. 编写注册用户业务
_logger?.LogDebug("-----------{Message}-----------", "检测用户是否存在并注册用户"); //todo: 2. 编写发送注册通知等
_logger?.LogDebug("-----------{Account} 注册成功 {Message}-----------", @event.Account, "发送邮件提示注册成功");
}
}

注册用户的处理程序可以放到任意一个类中,但其构造函数参数必须支持从DI获取,且处理程序的方法仅支持 TaskVoid 两种, 不支持其它类型

  1. 发送注册用户事件,修改Program.cs
app.MapPost("/register", async (RegisterUserEvent @event, IEventBus eventBus) =>
{
await eventBus.PublishAsync(@event);
});

进阶

处理流程

EventBus的 请求管道包含一系列请求委托,依次调用。 它们与ASP.NET Core中间件有异曲同工之妙,区别点在于中间件的执行顺序与注册顺序相反,最先注册的最后执行

每个委托均可在下一个委托前后执行操作,其中TransactionMiddleware是EventBus发布后第一个要进入的中间件 (默认提供),并且它是不支持多次嵌套的。

EventBus 支持嵌套,这意味着我们可以在Handler中重新发布一个新的Event,但TransactionMiddleware仅会在最外层进入时被触发一次

自定义中间件

根据需要我们可以自定义中间件,并注册到EventBus的请求管道中,比如通过增加FluentValidation, 将参数验证从业务代码中剥离开来,从而使得处理程序更专注于业务

  1. 注册FluentValidation, 修改Program.cs
builder.Services.AddValidatorsFromAssembly(Assembly.GetEntryAssembly());
  1. 自定义验证中间件ValidatorMiddleware.cs,用于验证参数
public class ValidatorMiddleware<TEvent> : Middleware<TEvent>
where TEvent : IEvent
{
private readonly ILogger<ValidatorMiddleware<TEvent>>? _logger;
private readonly IEnumerable<IValidator<TEvent>> _validators; public ValidatorMiddleware(IEnumerable<IValidator<TEvent>> validators, ILogger<ValidatorMiddleware<TEvent>>? logger = null)
{
_validators = validators;
_logger = logger;
} public override async Task HandleAsync(TEvent @event, EventHandlerDelegate next)
{
var typeName = @event.GetType().FullName; _logger?.LogDebug("----- Validating command {CommandType}", typeName); var failures = _validators
.Select(v => v.Validate(@event))
.SelectMany(result => result.Errors)
.Where(error => error != null)
.ToList(); if (failures.Any())
{
_logger?.LogError("Validation errors - {CommandType} - Event: {@Command} - Errors: {@ValidationErrors}",
typeName,
@event,
failures); throw new ValidationException("Validation exception", failures);
} await next();
}
}
  1. 注册EventBus并使用验证中间件ValidatorMiddleware
builder.Services.AddEventBus(eventBusBuilder=>eventBusBuilder.UseMiddleware(typeof(ValidatorMiddleware<>)));
  1. 添加注册用户验证类RegisterUserEventValidator.cs
public class RegisterUserEventValidator : AbstractValidator<RegisterUserEvent>
{
public RegisterUserEventValidator()
{
RuleFor(e => e.Account).NotNull().WithMessage("用户名不能为空");
RuleFor(e => e.Email).NotNull().WithMessage("邮箱不能为空");
RuleFor(e => e.Password)
.NotNull().WithMessage("密码不能为空")
.MinimumLength(6)
.WithMessage("密码必须大于6位")
.MaximumLength(20)
.WithMessage("密码必须小于20位");
}
}

编排

EventBus 支持事件编排,它们可以用来处理一些对执行顺序有要求的业务,比如: 注册用户必须成功之后才可以发送注册邮件通知,发送奖励等等,那我们可以这样做

将注册用户业务拆分为三个Handler,并通过指定Order的值来对执行事件排序

public class UserHandler
{
private readonly ILogger<UserHandler>? _logger; public UserHandler(ILogger<UserHandler>? logger = null)
{
_logger = logger;
} [EventHandler(1)]
public void RegisterUser(RegisterUserEvent @event)
{
_logger?.LogDebug("-----------{Message}-----------", "检测用户是否存在并注册用户");
//todo: 编写注册用户业务
} [EventHandler(2)]
public void SendAwardByRegister(RegisterUserEvent @event)
{
_logger?.LogDebug("-----------{Account} 注册成功 {Message}-----------", @event.Account, "发送注册奖励");
//todo: 编写发送奖励等
} [EventHandler(3)]
public void SendNoticeByRegister(RegisterUserEvent @event)
{
_logger?.LogDebug("-----------{Account} 注册成功 {Message}-----------", @event.Account, "发送注册成功邮件");
//todo: 编写发送注册通知等
}
}

Saga

EventBus支持Saga模式

具体是怎么做呢?

[EventHandler(1, IsCancel = true)]
public void CancelSendAwardByRegister(RegisterUserEvent @event)
{
_logger?.LogDebug("-----------{Account} 注册成功,发放奖励失败 {Message}-----------", @event.Account, "发放奖励补偿");
}

当发送奖励出现异常时,则执行补偿机制,执行顺序为 (2 - 1) > 0,由于目前仅存在一个Order为1的Handler,则执行奖励补偿后退出

但对于部分不需要执行失败但不需要执行回退的方法,我们可以修改 FailureLevels 确保不会因为当前方法的异常而导致执行补偿机制

[EventHandler(3, FailureLevels = FailureLevels.Ignore)]
public void SendNoticeByRegister(RegisterUserEvent @event)
{
_logger?.LogDebug("-----------{Account} 注册成功 {Message}-----------", @event.Account, "发送邮件提示注册成功");
//todo: 编写发送注册通知等
}

源码解读

EventHandler

  • FailureLevels: 失败级别, 默认: Throw

    • Throw:发生异常后,依次执行Order小于当前Handler的Order的取消动作,比如:Handler顺序为 1、2、3,CancelHandler为 1、2、3,如果执行 Handler3 异常,则依次执行 2、1
    • ThrowAndCancel:发生异常后,依次执行Order小于等于当前Handler的Order的取消动作,比如:Handler顺序为 1、2、3,CancelHandler为 1、2、3,如果执行 Handler3 异常,则依次执行 3、2、1
    • Ignore:发生异常后,忽略当前异常(不执行取消动作),继续执行其他Handler
  • Order: 执行顺序,默认: int.MaxValue,用于控制当前方法的执行顺序
  • EnableRetry: 当Handler异常后是否启用重试, 默认: false
  • RetryTimes: 重试次数,当出现异常后执行多少次重试, 需开启重试配置
  • IsCancel: 是否是补偿机制,默认: false

Middleware

  • SupportRecursive: 是否支持递归 (嵌套), 默认: true

    • 部分中间件仅在最外层被触发一次,像TransactionMiddleware 就是如此,但也有很多中间件是需要被多次执行的,比如ValidatorMiddleware,每次发布事件时都需要验证参数是否正确
  • HandleAsync(TEvent @event, EventHandlerDelegate next): 处理程序,通过调用 next() 使得请求进入下一个Handler

IEventHandler 与 ISagaEventHandler

  • HandleAsync(TEvent @event): 提供事件的Handler
  • CancelAsync(TEvent @event): 提供事件的补偿Handler

EventHandler功能类似,提供基本的Handler以及补偿Handler,推荐使用EventHandler的方式使用

TransactionMiddleware

提供事务中间件,当EventBusUoW以及Masa提供的Repository来使用时,当存在待提交的数据时,会自动执行保存并提交,当出现异常后,会执行事务回滚,无需担心脏数据入库

性能测试

与市面上使用较多的MeidatR作了对比,结果如下图所示:

BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19043.1023 (21H1/May2021Update)

11th Gen Intel Core i7-11700 2.50GHz, 1 CPU, 16 logical and 8 physical cores

.NET SDK=7.0.100-preview.4.22252.9

[Host] : .NET 6.0.6 (6.0.622.26707), X64 RyuJIT DEBUG

Job-MHJZJL : .NET 6.0.6 (6.0.622.26707), X64 RyuJIT

Runtime=.NET 6.0 IterationCount=100 RunStrategy=ColdStart

Method Mean Error StdDev Median Min Max
AddShoppingCartByEventBusAsync 124.80 us 346.93 us 1,022.94 us 8.650 us 6.500 us 10,202.4 us
AddShoppingCartByMediatRAsync 110.57 us 306.47 us 903.64 us 7.500 us 5.300 us 9,000.1 us

根据性能测试我们发现,EventBus与MediatR性能差距很小,但EventBus提供的功能却要强大的多

常见问题

  1. 按照文档操作,通过EventBus发布事件后,对应的Handler并没有执行,也没有发现错误?

①. EventBus.PublishAsync(@event) 是异步方法,确保等待方法调用成功,检查是否出现同步方法调用异步方法的情况

②. 注册EventBus时指定程序集集合, Assembly被用于注册时获取并保存事件与Handler的对应关系

var assemblies = new[]
{
typeof(UserHandler).Assembly
};
builder.Services.AddEventBus(assemblies);

程序集: 手动指定Assembly集合 -> MasaApp.GetAssemblies() -> AppDomain.CurrentDomain.GetAssemblies()

但由于NetCore按需加载,未使用的程序集在当前域中不存在,因此可能会导致部分事件以及Handler的对应关系未正确保存,因此可通过手动指定Assembly集合或者修改全局配置中的Assembly集合来修复这个问题

  1. 通过EventBus发布事件,Handler出错,但数据依然保存到数据库中

①. 检查是否禁用事务

  1. DisableRollbackOnFailure是否为true (是否失败时禁止回滚)
  2. UseTransaction是否为false (禁止使用事务)

②. 检查当前数据库是否支持回滚。例如: 使用的是Mysql数据库,但回滚数据失败,请查看

本章源码

Assignment11

https://github.com/zhenlei520/MasaFramework.Practice

开源地址

MASA.Framework:https://github.com/masastack/MASA.Framework

MASA.EShop:https://github.com/masalabs/MASA.EShop

MASA.Blazor:https://github.com/BlazorComponent/MASA.Blazor

如果你对我们的 MASA Framework 感兴趣,无论是代码贡献、使用、提 Issue,欢迎联系我们

MASA Framework -- EventBus入门与设计的更多相关文章

  1. MASA Framework - EventBus设计

    目录 MASA Framework - 整体设计思路 MASA Framework - EventBus设计 概述 利用发布订阅模式来解耦不同架构层级,亦可用于解决隔离业务之间的交互 优点: 松耦合 ...

  2. MASA Framework - DDD设计(1)

    目录 MASA Framework - 整体设计思路 MASA Framework - EventBus设计 MASA Framework - MASA Framework - DDD设计(1) DD ...

  3. MASA Framework - DDD设计(2)

    目录 MASA Framework - 整体设计思路 MASA Framework - EventBus设计 MASA Framework - MASA Framework - DDD设计(1) MA ...

  4. MASA Framework - 整体设计思路

    源起 年初我们在找一款框架,希望它有如下几个特点: 学习成本低 只需要学.Net每年主推的技术栈和业务特性必须支持的中间件,给开发同学减负,只需要专注业务就好 个人见解:一款好用的框架应该是补充,而不 ...

  5. MASA Auth - SSO与Identity设计

    AAAA AAAA即认证.授权.审计.账号(Authentication.Authorization.Audit.Account).在安全领域我们绕不开的两个问题: 授权过程可靠:让第三方程序能够访问 ...

  6. MasaFramework -- 缓存入门与设计

    概念 什么是缓存,在项目中,为了提高数据的读取速度,我们会对不经常变更但访问频繁的数据做缓存处理,我们常用的缓存有: 本地缓存 内存缓存:IMemoryCache 分布式缓存 Redis: Stack ...

  7. Entity Framework 程序设计入门二 对数据进行CRUD操作和查询

    前一篇文章介绍了应用LLBL Gen生成Entity Framework所需要的类型定义,用一行代码完成数据资料的读取, <LLBL Gen + Entity Framework 程序设计入门& ...

  8. Entity Framework实体模型 入门视频教程

    Entity Framework实体模型 入门视频教程 恢复内容开始--- 第一步 创建一个 控制台应用程序 第二步 创建一个ADO.NET 数据实体模型 DbModel.edmx 需要跟数据库进行连 ...

  9. Entity Framework快速入门--ModelFirst

    Entity Framework带给我们的不仅仅是操作上的方便,而且使用上也很是考虑了用户的友好交互,EF4.0与vs2010的完美融合也是我们选择它的一个理由吧.相比Nhibernate微软这方面做 ...

随机推荐

  1. Apache DolphinScheduler 简单任务定义及复杂的跨节点传参

    ​ 点亮 ️ Star · 照亮开源之路 GitHub:https://github.com/apache/dolphinscheduler Apache DolphinScheduler是一款非常不 ...

  2. 应用性能监控:SkyWalking

    目录 SkyWalking 简介 SkyWalking 搭建 平台后端(Backend) 平台前端(UI) Java Agent(Java 应用监控) Java Agent 下载 Java 演练项目 ...

  3. QT的字符编码

    QString编码:UTF-16 QString内部保存的数据就是QChar数组,是Unicode编码(utf16),在字符显示,操作的时候都是基于Unicode. QString构造时默认采用Lat ...

  4. 深度学习库 SynapseML for .NET 发布0.1 版本

    2021年11月 微软开源一款简单的.多语言的.大规模并行的机器学习库 SynapseML(以前称为 MMLSpark),以帮助开发人员简化机器学习管道的创建.具体参见[1]微软深度学习库 Synap ...

  5. Skype for Business server 数据库安装

    之前安装了SFB 2015标准版,但是没有安装归档据库,现在打算重新安装.环境中安装的是默认自带的SQL EXPRESS. 继续安装向导,安装SQL数据库.但是在最后的时候遇到了问题. 安装向导报错 ...

  6. G&GH01 注册/安装/设置

    注意事项与声明 平台: Windows 10 作者: JamesNULLiu 邮箱: jamesnulliu@outlook.com 博客: https://www.cnblogs.com/james ...

  7. 单机部署minio,设置Nginx代理,配置https(TLS)访问

    安装 下载地址:https://dl.min.io/ # 创建目录 mkdir -p /usr/local/minio/{data,bin,etc} # 下载minio wget https://dl ...

  8. X-Pack:创建阈值检查警报

    简单的事情应该简单(Simple things should be simple),这是Elastic {ON} '17的主题之一,Elastics收到了许多关于使用简单易用的UI创建警报的请求.事实 ...

  9. KVM命令参数

    # virt-install --help usage: virt-install --name NAME --memory MB STORAGE INSTALL [options] 从指定安装源创建 ...

  10. 第二章:视图层 - 9:动态生成CSV文件

    CSV (Comma Separated Values),以纯文本形式存储数字和文本数据的存储方式.纯文本意味着该文件是一个字符序列,不含必须像二进制数字那样的数据.CSV文件由任意数目的记录组成,记 ...