本节内容,部分为补充内容,部分涉及到9.3.8-9.3.9(P327-334)。主要NuGet包:

  • MediatR.Extensions.Microsoft.DependencyInjection

 

领域事件可以切断领域模型之间的强依赖关系,但如果使用传递的事件机制,要求将事件的处理者显式的注册到事件发布者对象中,耦合性强。所以在.NET中实现同一个进程内的领域事件,推荐使用MediatR库,它可以实现事件发布和事件处理对象之间的解耦。

 

一、MediatR的基本使用

1、基本步骤:

  • 第一步:安装NuGet包,MediatR.Extensions.Microsoft.DependencyInjection
  • 第二步:注册MediatR服务:builder.Services.AddMediatR(Assembly.Load(nameof(WebApplication15)));  通过AddMediatR方法的参数为  事件处理者  所在的若干程序集。
  • 第三步:创建事件类,如下例中的TestEvent类
  • 第四步:创建事件处理类,定义事件处理程序(Handle方法),如下例中的TestEventHandler1类和TestEventHandler2类。事件发布和事件处理,可以一对一,也可以一对多。
  • 第五步:创建事件发布类,如下例中的TestPublishController控制器,注入IMediator服务,调用Publish或Send方法,发布事件。

 

2、基本使用案例

(1)事件类

//实现INotification即可
//由于事件一般从发布者到处理者,是单向行为,不可变,所以建议使用record类型
public record TestEvent(string Name):INotification;

 

(2)事件处理类 

//事件处理类1
//INotificationHandler<TestEvent>,订阅事件TestEvent
//Handle方法,为事件处理程序,事件发布后执行
public class TestEventHandler1 : INotificationHandler<TestEvent>
{
    public Task Handle(TestEvent notification, CancellationToken cancellationToken)
    {
        Console.WriteLine($"TestEventHandler1收到了{notification.Name}");
        return Task.CompletedTask;
    }
}

//事件处理类2
//INotificationHandler<TestEvent>,订阅事件TestEvent
//Handle方法,为事件处理程序,事件发布后执行
public class TestEventHandler2 : INotificationHandler<TestEvent>
{
    public async Task Handle(TestEvent notification, CancellationToken cancellationToken)
    {
        await File.WriteAllTextAsync("d:/1.txt",$"TestEventHandler2收到了{notification.Name}");
    }
}

 

(3)事件发布类,此例中使用控制器来发布

//控制器TestPublishController中,注入IMediator,调用IMediator的Publish方法发布一对多事件
//IMediator还有另外一个方法,Send方法,用于发布一对一事件
[Route("api/[controller]/[action]")]
[ApiController]
public class TestPublishController : ControllerBase
{
    private readonly IMediator mediator;
    public TestPublishController(IMediator mediator)
    {
        this.mediator = mediator;
    }

    [HttpPost]
    public async Task<ActionResult> PublishName(string name)
    {
        //发布事件,此处使用await,需要等待全部事件处理程序执行完后,才会向下执行
        //如果不使用await,则不会等待
        //特殊情况:即使使用了await,如果事件处理程序本身是异步的,也不会等待这个异步Handler
        //await能够保证事务的一致性,但需要耗时等待,如果不等待,则需要自行处理事务一致性问题
        await mediator.Publish(new TestEvent(name)); 
        return Ok("已发布事件");
    }
}

 

 

二、Mediat的进阶使用

无论是应用服务,还是领域服务,最终都要调用聚合根中的方法来操作聚合。如果领域事件都在聚合根中发布,就可以确保领域事件不会被漏掉。但是这种模式存在两个问题:(1)我们可能定义了多个修改实体信息的方法,如ChangeName,ChangeEmail,如果每次调用这些方法都发布领域事件,则存在重复发布事件的问题;(2)领域事件可能发布太早,如在构造方法中发布了新增实体的事件,但可能因为数据校验没有通过,新增实体失败,但事件已经发布。为了解决以上问题,书中提供了一个解决方案,在聚合根中定义一个事件集合,领域事件只添加到集合中(称之为注册),暂不发布。将发布事件的工作,延迟到上下文保存修改时才进行。

1、准备工作

1.1 定义注册领域事件相关方法的接口IDomainEvents

public interface IDomainEvents
{
    IEnumerable<INotification> GetDomainEvents(); //获取领域事件集合
    void AddDomainEvent(INotification eventItem); //将领域事件添加到集合中
    void AddDomainEventIfAbsent(INotification eventItem); //集合中不存中相同领域事件时,才添加领域事件
    void ClearDomainEvents(); //清除领域事件
}

 

 

1.2 创建实现IDomainEvents接口的抽象实体基类BaseEntity,聚合根实体类都继承自BaseEntity

public class BasicEntity : IDomainEvents
{
    //初始化一个领域事件的集合
    private List<INotification> DomainEvents = new();
    //将领域事件添加到集合中
    public void AddDomainEvent(INotification eventItem)
    {
        DomainEvents.Add(eventItem);
    }
    //如果集合中不存中相同的领域事件,则添加到集合
    public void AddDomainEventIfAbsent(INotification eventItem)
    {
        if (!DomainEvents.Contains(eventItem))
        {
            DomainEvents.Add(eventItem);
        }
    }
    //获取领域事件集合
    public IEnumerable<INotification> GetDomainEvents()
    {
        return DomainEvents;
    }
    //清除领域事件集合
    public void ClearDomainEvents()
    {
        DomainEvents.Clear();
    }
}

 

 

1.3 封装一个DbContext上下文基类BaseDbContext,重写SaveChanges和SaveChangesAsync方法,在SaveChanges时发布领域事件

public abstract class BaseDbContext:DbContext
{
    //注入IMediat服务
    private readonly IMediator mediator;
    public BaseDbContext(DbContextOptions options,IMediator mediator):base(options)
    {
        this.mediator = mediator;
    }
    //重写SaveChanges方法,禁用它
    public override int SaveChanges(bool acceptAllChangesOnSuccess)
    {
        throw new NotImplementedException("不要使用同步的SaveChanges方法");
    }
    //重写SaveChangesAsync方法
    public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default)
    {
        //ChangeTracker方法,获取所有实现了IDomainEvents接口的实体类,且实体类中的领域事件集合不为空
        //GetDomainEvents()方法,是我们在IDomainEvents接口中定义的方法,返回聚合根的领域事件集合
        var domainEntities = this.ChangeTracker.Entries<IDomainEvents>().Where(x=>x.Entity.GetDomainEvents().Any());
        //获取所有领域事件
        var domainEvents = domainEntities.SelectMany(x=>x.Entity.GetDomainEvents()).ToList();
        //因为已经获取了所有领域事件,所以可以清除聚合根中的领域事件
        domainEntities.ToList().ForEach(x => x.Entity.ClearDomainEvents());
        //发布领域事件
        foreach (var domainEvent in domainEvents)
        {
            await mediator.Publish(domainEvent);
        }
        //调用基类的SaveChangesAsync方法,即在调用基类的保存数据修改前,发布了领域事件
        return await base.SaveChangesAsync(acceptAllChangesOnSuccess,cancellationToken);
    }
}

 

2、定义、订阅和注册事件,调用DbContext上下文的SaveChanges方法,自动发布事件 

2.1 定义事件:以下案例定义了三个事件类

 

public record UserAddEvent(User User):INotification; //添加用户
public record UserUpdateEvent(Guid Id):INotification; //修改用户
public record UserSoftDeleteEvent(Guid Id): INotification; //软删除用户

 

 

 

2.2 创建事件处理类(订阅事件):以下案例创建了两个事件处理类

 

//①添加用户事件处理类
public class UserAddEventHandler : INotificationHandler<UserAddEvent>
{
    //注入ILogger日志服务
    private readonly ILogger<UserAddEventHandler> logger;
    public UserAddEventHandler(ILogger<UserAddEventHandler> logger)
    {
        this.logger = logger;
    }
    //伪发送邮件
    public Task Handle(UserAddEvent notification, CancellationToken cancellationToken)
    {
        var user = notification.User;
        logger.LogInformation($"向{user.Email}发送邮件");
        return Task.CompletedTask;
    }
}
//②修改用户事件处理类
public class UserModifyEventHandler : INotificationHandler<UserUpdateEvent>
{
    //注入ILogger和DbContext
    private readonly MyDbContext myDbContext;
    private readonly ILogger<UserModifyEventHandler> logger;
    public UserModifyEventHandler(MyDbContext myDbContext,ILogger<UserModifyEventHandler> logger)
    {
        this.myDbContext = myDbContext;
        this.logger = logger;
    }
    //伪发邮件给用户
    public async Task Handle(UserUpdateEvent notification, CancellationToken cancellationToken)
    {
        var user = await myDbContext.Users.FindAsync(notification.Id);
        logger.LogInformation($"通知用户{user.Email}信息已经更改");
    }
}

 

 

 

2.3 在聚合实体类中注册事件

 

public class User:BaseEntity
{
    public Guid Id { get; init; } //Guid类型的ID,只允许在创建对象时初始化
    public string UserName { get; init; } //用户名,只允许在创建对象时初始化
    public string Email { get; init; } //Email,只允许在创建对象时初始化
    public string? NickName { get; private set; } //呢称,只允许在类内部修改
    public int? Age { get; private set; } //年龄,只允许在类内部修改
    public bool IsDeleted { get; private set; } //软删除,只允许在类内部修改

    private User() { } //私有无参构造函数,EFCore默认使用

    //调用BaseEntity的AddDomainEvent方法注册事件UserAddEvent
    public User(Guid id, string userName, string email)
    {
        this.Id = id;
        this.UserName = userName;
        this.Email = email;
        AddDomainEvent(new UserAddEvent(this)); 
    }
    //调用BaseEntity的AddDomainEventIfAbsent方法注册事件UserUpdateEvent
    public void ChangeNickName(string? newNickName)
    {
        this.NickName = newNickName;
        AddDomainEventIfAbsent(new UserUpdateEvent(Id)); 
    }
    //调用BaseEntity的AddDomainEventIfAbsent方法注册事件UserUpdateEvent
    public void ChangeAge(int newAge)
    {
        this.Age = newAge;
        AddDomainEventIfAbsent(new UserUpdateEvent(Id));
    }
    //调用BaseEntity的AddDomainEvent方法注册事件UserSoftDeleteEvent
    public void SoftDelete()
    {
        this.IsDeleted = true;
        AddDomainEvent(new UserSoftDeleteEvent(Id));
    }
}

 

 

 

2.4 调用上下文的SaveChanges方法,自动发布事件。在控制器中,不再需要注入IMediat服务

[Route("api/[controller]/[action]")]
[ApiController]
public class TestPublishController : ControllerBase
{
    private readonly MyDbContext ctx;
    public TestPublishController(MyDbContext ctx)
    {
        this.ctx = ctx;
    }

    //新增用户Action
    [HttpPost]
    public async Task<ActionResult> AddUser(User user)
    {
        var u1 = new User(Guid.NewGuid(), user.UserName, user.Email);
        ctx.Users.Add(u1);
        await ctx.SaveChangesAsync();
        return Ok($"添加成功,新用户的ID为{u1.Id}");
    }
    //修改用户Action
    [HttpPut]
    public async Task<ActionResult> UpdateUser(Guid id,string nickName,int age)
    {
        var u1 = ctx.Users.Find(id);
        u1.ChangeNickName(nickName);
        u1.ChangeAge(age);
        await ctx.SaveChangesAsync();
        return Ok("修改成功");
    }
}

 

 

 

特别说明:
1、本系列内容主要基于杨中科老师的书籍《ASP.NET Core技术内幕与项目实战》及配套的B站视频视频教程,同时会增加极少部分的小知识点
2、本系列教程主要目的是提炼知识点,追求快准狠,以求快速复习,如果说书籍学习的效率是视频的2倍,那么“简读系列”应该做到再快3-5倍

 

原文地址:http://www.cnblogs.com/functionMC/p/16927301.html

1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长! 2. 分享目的仅供大家学习和交流,请务用于商业用途! 3. 如果你也有好源码或者教程,可以到用户中心发布,分享有积分奖励和额外收入! 4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解! 5. 如有链接无法下载、失效或广告,请联系管理员处理! 6. 本站资源售价只是赞助,收取费用仅维持本站的日常运营所需! 7. 如遇到加密压缩包,默认解压密码为"gltf",如遇到无法解压的请联系管理员! 8. 因为资源和程序源码均为可复制品,所以不支持任何理由的退款兑现,请斟酌后支付下载 声明:如果标题没有注明"已测试"或者"测试可用"等字样的资源源码均未经过站长测试.特别注意没有标注的源码不保证任何可用性