DDD的技术落地 — 用MediatR实现领域事件

领域事件可以切断领域模型之间的强依赖关系,事件发布完成后,由事件的处理者决定如何响应事件,这样我们可以实现事件发布和事件处理之间的解耦。

在.NET中实现领域事件的时候,我们可以使用C#的事件语法,但是事件语法要求事件的处理者被显式地注册到事件的发布者对象中,耦合性很强,本书作者推荐使用MediaR实现领域事件。

MediatR是一个在.NET中实现进程内事件传递的开源库,它可以实现事件的发布和事件的处理之间的解耦。

MediatR中支持”一个发布者对应一个处理者”和”一个发布者对应多个处理者”两种模式,后者的应用更广泛,因此下面我们会使用这种用法。

第1步:

创建一个ASP.NET Core项目,然后通过NuGet安装MediatR.Extensions.Microsoft.DependencyInjection。

第2步:

在项目的Program.cs中调用AddMediatR方法把与MediatR相关的服务注册到依赖注入容器中,AddMediatR方法的参数中一般指定事件处理者所在的若干个程序集。

注册MediatR如以下代码所示:

C#
builder.Services.AddMediatR(Assembly.Load("用MediatR实现领域事件"));

第3步:

定义一个在事件的发布者和处理者之间进行数据传递的类TestEvent,这个了需要实现INotification接口,如以下代码所示:

C#
 public record TestEvent(string UserName) : INotification;

TestEvent中的UserName属性代表登录用户的用户名。

事件一般都是从发布者传递到处理者的,很少有在事件的处理者处直接反向通知事件发布者的需求,因此实现INotification的TestEvent类的属性一般都是不可变的,我们用record关键字来声明这个类。

第4步:

事件的处理者要实现NotificationHandler<TNotification>接口,其中的泛型参数TNotification代表此事件处理者要处理的消息类型。

所有TNotification类型的事件都会被事件处理者处理。

我们编写两个事件处理者,如以下代码所示,它们分别把收到的事件输出到控制台和写入文件:

C#
 public class TestEventHandler1 : INotificationHandler<TestEvent>
 {
     public Task Handle(TestEvent notification, CancellationToken cancellationToken)
     {
         Console.WriteLine($"我收到了{notification.UserName}");
         return Task.CompletedTask;
     }
 }
 
  public class TestEventHandler2 : INotificationHandler<TestEvent>
 {
     public async Task Handle(TestEvent notification, CancellationToken cancellationToken)
     {
         await File.WriteAllTextAsync("d:/1.txt", $"来了{notification.UserName}");
     }
 }

第5步:

在需要发布事件的类中注入IMediator类型的服务,然后我们调用Publish方法来发布,注意不要错误第调用Send方法来发布事件,因为Send方法是用来发布一对一事件的,而Publish方法是用来发布一对多事件的。

我们需要在控制器的登陆方法中发布事件,这里我们省略了实际的登录代码,如以下代码所示:

C#
public class TestController : ControllerBase
{
    private readonly IMediator mediator;

    public TestController(IMediator mediator)
    {
        this.mediator = mediator;
    }

    [HttpPost]
    public async Task<IActionResult> Login(LoginRequest req)
    {
        //不要写成Send
        await mediator.Publish(new TestEvent(req.UserName));
        return Ok("ok");
    }
}

运行上面的程序,然后调用登录接口,我们就可以看到控制台和文件中都有代码输出的信息,这说明两个事件处理者都被执行了,如下图所示:

如果我们使用await的方式来调用Publish方法,那么程序会等待所有的事件处理者的Handle方法执行完成后才继续向后执行。

因此事件发布者和事件处理者的代码是运行在相同的调用堆栈中的,这样我们可以轻松地实现强一致性的事务。

如果事件发布者不需要等待事件处理者的执行,那么我们可以不用await方法来调用Publish方法;

即使我们需要使用await方法来调用Publish方法发布事件,如果某个事件处理者的代码执行太耗时,为了避免影响用户体验,我们也可以在事件处理者的Handle方法中异步执行事件的处理逻辑。

如果我们选择步等待事件处理者,就要处理事务的最终一致性!

订阅评论
提醒
0 评论
最旧
最新 最多投票
内联反馈
查看所有评论
滚动至顶部