第一节:MediatR简介、快速上手、基于MediatR实现领域事件
一. MediatR简介
1. 说明
MediatR是.NET中的开源简单中介者模式实现,它通过一种进程内消息传递机制(无其他外部依赖),进行请求/响应、命令、查询、通知和事件的消息传递,并通过泛型来支持消息的智能调度, 多用于领域事件中。
(GitHub:https://github.com/jbogard/MediatR)
2. 补充
领域事件是:微服务内部的,进程内的通信。
集成事件是:微服务之间的,进程外的通信。
二. 快速上手
1. 通过Nuget安装程序集
给WebApi程序 安装程序集【MediatR.Extensions.Microsoft.DependencyInjection 10.0.1】
2. 注册MediatR服务
builder.Services.AddMediatR(Assembly.GetExecutingAssembly());
3. 发送消息
使用Publish进行一对多消息发送,即领域事件的发送,传递的参数要用 record类型,必须实现INotification接口 【重点】
/// <summary>
/// 测试Api
/// </summary>
[Route("api/[controller]/[action]")]
[ApiController]
public class LoginController : ControllerBase
{
private readonly IMediator mediator;
public LoginController(IMediator mediator)
{
this.mediator = mediator;
}
/// <summary>
/// 校验登录
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
[HttpPost]
public string CheckLogin(LoginModel model)
{
//发送消息 (一对多)
mediator.Publish(new UserInfo(model.userAccount,model.userPwd));
return "ok," + model.userAccount + "," + model.userPwd;
}
}
4. 接收消息
编写接收事件类EventHandle1、EventHandle2,实现INotificationHandler<T>接口的Handle方法。
传递参数类UserInfo
public record UserInfo(string userAccount,string userPwd) : INotification;
EventHandle1
/// <summary>
/// 事件接收者1
/// </summary>
public class EventHandle1 : INotificationHandler<UserInfo>
{
public Task Handle(UserInfo notification, CancellationToken cancellationToken)
{
Console.WriteLine($"EventHandle1收到新消息:{notification.userAccount},{notification.userPwd} 登录成功了");
return Task.CompletedTask;
}
}
EventHandle2
/// <summary>
/// 事件接收者2
/// </summary>
public class EventHandle2 : INotificationHandler<UserInfo>
{
public Task Handle(UserInfo notification, CancellationToken cancellationToken)
{
Console.WriteLine($"EventHandle1收到新消息:{notification.userAccount},{notification.userPwd}登录成功了");
return Task.CompletedTask;
}
}
5. 进行测试,大功告成。
三. 基于MediatR实现领域事件
1. 整体思想
借鉴微软开源的eShopOnContainers项目中的做法,把领域事件的发布延迟到上下文保存修改时。实体中只是注册要发布的领域事件(比如在构造函数中、新增、修改方法中), 然后在上下文 的SaveChanges方法被调用时,我们再发布事件。
2. 实操
(1). 映射实体
【Scaffold-DbContext "Server=localhost;Database=EFCore6xDB;User ID=sa;Password=123456;" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Entity -Context EFCore6xDBContext -UseDatabaseNames -DataAnnotations -NoPluralize】
(2). 创建IDomainEvent 和 BaseEntity
分别为领域事件的接口和实现类,包括添加、清空、获取所有领域事件的方法
IDomainEvent
/// <summary>
/// 领域事件接口
/// </summary>
public interface IDomainEvents
{
/// <summary>
/// 获取所有领域事件
/// </summary>
/// <returns></returns>
List<INotification> GetAllDomainEvents();
/// <summary>
/// 添加事件
/// </summary>
/// <param name="item"></param>
void AddDomainEvent(INotification item);
/// <summary>
/// 添加事件(前提是不存在)
/// </summary>
/// <param name="item"></param>
void AddDomainEventIfNoExist(INotification item);
/// <summary>
/// 清空所有事件
/// </summary>
void ClearAllDomainEvents();
}
BaseEntity
查看代码 /// <summary>
/// 领域事件实现类
/// (这里声明为抽象类,不能直接实例化,继承的子类可以直接使用里面的普通方法,也可以里面的普通方法进行override)
/// </summary>
public abstract class BaseEntity : IDomainEvents
{
//[NotMapped] //.Net6.0 中不需要加这个特性了
private List<INotification> DoaminEventList = new ();
/// <summary>
/// 获取所有领域事件
/// </summary>
/// <returns></returns>
public List<INotification> GetAllDomainEvents()
{
return DoaminEventList;
}
/// <summary>
/// 添加事件
/// </summary>
/// <param name="item">实现了INotification接口的record类</param>
public void AddDomainEvent(INotification item)
{
DoaminEventList.Add(item);
}
/// <summary>
/// 添加事件(前提是不存在)
/// </summary>
/// <param name="item">实现了INotification接口的record类</param>
public void AddDomainEventIfNoExist(INotification item)
{
if (!DoaminEventList.Contains(item))
{
DoaminEventList.Add(item);
}
}
/// <summary>
/// 清空所有事件
/// </summary>
public void ClearAllDomainEvents()
{
DoaminEventList.Clear();
}
}
(3). 创建BaseDbContext
继承DbContext类,注入IMediator类,重写SaveChangeAsync方法,获取所有事件进行发布。
/// <summary>
/// 改造DbContext类,用于重写SaveChanges
/// </summary>
public class BaseDbContext : DbContext
{
private IMediator mediator;
public BaseDbContext(DbContextOptions options, IMediator mediator) : base(options)
{
this.mediator = mediator;
}
/// <summary>
/// 重写SaveChangesAsync方法【改造顺序】
/// 改造顺序,将publish放在SaveChangesAsync后,当SaveChangesAsync失败就不会发送消息,但依旧清空消息
/// </summary>
/// <param name="acceptAllChangesOnSuccess"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default)
{
//1.获取所有实现IDomainEvents接口 且 含有未发布事件的对象
var domainEntities = this.ChangeTracker.Entries<IDomainEvents>().Where(u => u.Entity.GetAllDomainEvents().Any());
//2. 获取所有待发布的消息【剖析selectMany的作用,两次查找】
var domainEvents = domainEntities.SelectMany(u => u.Entity.GetAllDomainEvents()).ToList();
//操作数据库
var result = await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
//3. 清空所有待发布的消息
domainEntities.ToList().ForEach(u => u.Entity.ClearAllDomainEvents());
//4. 发送消息
foreach (var item in domainEvents)
{
await mediator.Publish(item, cancellationToken);
}
return result;
}
}
(4). 修改默认EFCore上下文EFCore6xDBContext,使其继承BaseDbContext
修改构造函数。
public partial class EFCore6xDBContext : BaseDbContext
{
//public EFCore6xDBContext()
//{
//}
//public EFCore6xDBContext(DbContextOptions<EFCore6xDBContext> options)
// : base(options)
//{
//}
public EFCore6xDBContext(DbContextOptions<EFCore6xDBContext> options, IMediator mediator) : base(options, mediator)
{
}
public virtual DbSet<GoodsInfo> GoodsInfo { get; set; } = null!;
public virtual DbSet<RoleInfo> RoleInfo { get; set; } = null!;
public virtual DbSet<UserInfo> UserInfo { get; set; } = null!;
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<GoodsInfo>(entity =>
{
entity.Property(e => e.rowVersion)
.IsRowVersion()
.IsConcurrencyToken();
});
modelBuilder.Entity<RoleInfo>(entity =>
{
entity.Property(e => e.id).ValueGeneratedNever();
});
OnModelCreatingPartial(modelBuilder);
}
partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}
(5). UserInfo的领域事件类
A. 继承BaseEntity
B. 两个领域事件传递类,UserAddedEvent、UserUpdatedEvent
C. 利用构造函数创建实体,并且添加领域事件AddDomainEvent
D. 新增修改Age的方法ChangeAge,添加领域事件AddDomainEventIfNoExist
UserAddedEvent
/// <summary>
/// 用来传递 新增User的领域事件类
/// </summary>
/// <param name="Item"></param>
public record UserAddedEvent(UserInfo Item) : INotification;
UserUpdatedEvent
/// <summary>
/// 用来传递 修改User的领域事件类
/// </summary>
/// <param name="Item"></param>
public record UserEditedEvent(string userName, int age) : INotification;
UserInfo
public partial class UserInfo
{
[Key]
[StringLength(32)]
[Unicode(false)]
public string id { get; set; } = null!;
[StringLength(50)]
[Unicode(false)]
public string? userName { get; set; }
[StringLength(500)]
[Unicode(false)]
public string? userPwd { get; set; }
[StringLength(5)]
[Unicode(false)]
public string? userGender { get; set; }
public int? userAge { get; set; }
[Column(TypeName = "datetime")]
public DateTime? addTime { get; set; }
public int? delflag { get; set; }
}
和
/// <summary>
/// UserInfo的领域事件类
/// </summary>
public partial class UserInfo : BaseEntity
{
public UserInfo()
{
//提供无参构造方法。避免EF Core加载数据的时候调用有参的构造方法触发领域事件
}
public UserInfo(string userName, int userAge)
{
this.id = Guid.NewGuid().ToString("N");
this.userName = userName;
this.userAge = userAge;
this.userGender = "男";
this.userPwd = "123456";
this.addTime = DateTime.Now;
this.delflag = 0;
AddDomainEvent(new UserAddedEvent(this));
}
public void ChangeAge(int newAge)
{
this.userAge = newAge;
AddDomainEventIfNoExist(new UserEditedEvent(this.userName, newAge));
}
}
(6). UserController新增接口
添加用户方法和修改年龄的方法
查看代码
[Route("api/[controller]/[action]")]
[ApiController]
public class UserController : ControllerBase
{
private EFCore6xDBContext dbContext;
public UserController(EFCore6xDBContext dbContext)
{
this.dbContext = dbContext;
}
/// <summary>
/// 新增用户
/// </summary>
/// <param name="userName"></param>
/// <param name="userAge"></param>
/// <returns></returns>
[HttpGet]
public async Task<string> AddUser(string userName, int userAge)
{
try
{
UserInfo userInfo = new(userName, userAge);
dbContext.Add(userInfo);
await dbContext.SaveChangesAsync();
return "ok";
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
return "error";
}
}
/// <summary>
/// 修改用户年龄
/// </summary>
/// <param name="id"></param>
/// <param name="userAge"></param>
/// <returns></returns>
[HttpGet]
public async Task<string> EditAge(string id, int userAge)
{
try
{
UserInfo user=dbContext.UserInfo.Where(u=>u.id==id).FirstOrDefault();
if (user == null)
{
return "暂无该用户";
}
//修改年龄
user.ChangeAge(userAge);
await dbContext.SaveChangesAsync();
return "ok";
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
return "error";
}
}
}
(7). 事件处理类
A. 新增类的 UserAddHandle
/// <summary>
/// 接收用户新增成功的消息
/// </summary>
public class UserAddHandle : INotificationHandler<UserAddedEvent>
{
public Task Handle(UserAddedEvent user, CancellationToken cancellationToken)
{
// 注:这里Item是 UserAddedEvent(UserInfo Item)中的参数
Console.WriteLine($"收到消息:用户新增成功,用户为:{user.Item.id},{user.Item.userName}");
return Task.CompletedTask;
}
}
B. 修改类的 UserEditHandle
/// <summary>
/// 接受用户修改消息
/// </summary>
public class UserEditHandle : INotificationHandler<UserEditedEvent>
{
public Task Handle(UserEditedEvent notification, CancellationToken cancellationToken)
{
Console.WriteLine($"接收消息:用户{ notification.userName}的age修改为{notification.age}");
return Task.CompletedTask;
}
}
(8). 测试
A. 测试AddUser、EditAge接口,UserAddHandle、UserEditHandle中均收到消息。
B. 模拟错误进行测试,比如savechange操作DB注定是失败,看handle中是否收到消息,改造顺序后的不会发送消息。
3. 剖析SaveChangesAsync重写方法
获取所有未发布事件的对象 → 获取待发布消息 → savechange操作数据库 → 清空消息 → publish消息
!
- 作 者 : Yaopengfei(姚鹏飞)
- 博客地址 : http://www.cnblogs.com/yaopengfei/
- 声 明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
- 声 明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
2021-09-09 第三节:Vue3向下兼容2(v-for、数组方法、v-model、计算属性、监听器)