第一节: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 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
 
posted @ 2022-09-09 14:54  Yaopengfei  阅读(4045)  评论(4编辑  收藏  举报