概述

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

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

发布订阅模式

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

  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. 美团组件化事件总线方案改进:ModularEventBus

    请点赞关注,你的支持对我意义重大. Hi,我是小彭.本文已收录到 GitHub · AndroidFamily 中.这里有 Android 进阶成长知识体系,有志同道合的朋友,关注公众号 [彭旭锐] ...

  2. 区块相隔虽一线,俱在支付同冶熔,Vue3.0+Tornado6前后端分离集成Web3.0之Metamask区块链虚拟三方支付功能

    最近几年区块链技术的使用外延持续扩展,去中心化的节点认证机制可以大幅度改进传统的支付结算模式的经营效率,降低交易者的成本并提高收益.但不能否认的是,区块链技术也存在着极大的风险,所谓身怀利器,杀心自起 ...

  3. C#实现HTTP访问类HttpHelper

    在项目开发过程中,我们经常会访问第三方接口,如我们需要接入的第三方接口是Web API,这时候我们就需要使用HttpHelper调用远程接口了.示例中的HttpHelper类使用Log4Net记录了每 ...

  4. KingbaseES 全局索引是否因为DDL操作而变为Unusable ?

    前言 Oracle 在对分区做DDL操作时,会使分区全局索引失效,需要加上关键字update global indexes.KingbaseES 同样支持全局索引.那么,如果对分区表进行DDL操作,那 ...

  5. session 总结

    session 总结(单节点场景) session 称作域对象,一般保存在当前服务器的内存中,如果有很多session也会部分不常用的session"钝化"到磁盘中,若磁盘中的se ...

  6. Kubernetes 配置管理

    ConfigMap(可变配置管理) 对于应用的可变配置在 Kubernetes 中是通过一个 ConfigMap 资源对象来实现的,我们知道许多应用经常会有从配置文件.命令行参数或者环境变量中读取一些 ...

  7. Elasticsearch:Index生命周期管理入门

    如果您要处理时间序列数据,则不想将所有内容连续转储到单个索引中. 取而代之的是,您可以定期将数据滚动到新索引,以防止数据过大而又缓慢又昂贵. 随着索引的老化和查询频率的降低,您可能会将其转移到价格较低 ...

  8. 在k8s中将nginx.conf文件内容创建为ConfigMap挂载到pod容器中

    将nginx.conf文件内容创建为ConfigMap user nginx; worker_processes auto; error_log /var/log/nginx/error.log er ...

  9. 5_SpringMVC

    一. 什么是MVC框架 MVC全名是Model View Controller, 是模型(model), 视图(view), 控制器(controller)的缩写, 一种软件设计典范, 用一种业务逻辑 ...

  10. k8s 中 Pod 的控制器

    k8s 中 Pod 的控制器 前言 Replication Controller ReplicaSet Deployment 更新 Deployment 回滚 deployment StatefulS ...