35 | MediatR:让领域事件处理更加优雅

核心对象

IMediator

INotification

INotificationHandler

这两个与之前的 Request 的行为是不一样的,接下来看一下代码

internal class MyEvent : INotification
{
public string EventName { get; set; }
} internal class MyEventHandler : INotificationHandler<MyEvent>
{
public Task Handle(MyEvent notification, CancellationToken cancellationToken)
{
Console.WriteLine($"MyEventHandler执行:{notification.EventName}");
return Task.CompletedTask;
}
} internal class MyEventHandlerV2 : INotificationHandler<MyEvent>
{
public Task Handle(MyEvent notification, CancellationToken cancellationToken)
{
Console.WriteLine($"MyEventHandlerV2执行:{notification.EventName}");
return Task.CompletedTask;
}
}
//await mediator.Send(new MyCommand { CommandName = "cmd01" });
await mediator.Publish(new MyEvent { EventName = "event01" });

之前 mediator 使用了 Send 的方式来处理 Command,它还有一个方法 Publish,这个方法的入参是一个 INotification

启动程序,输出如下:

MyEventHandler执行:event01
MyEventHandlerV2执行:event01

与之前的 IRequest 不同的是,INotification 是可以注册多个 Handler 的,它是一个一对多的关系,借助它就可以对领域事件定义多个处理器来处理

接着看一下之前云服务的代码

public async Task<bool> SaveEntitiesAsync(CancellationToken cancellationToken = default)
{
var result = await base.SaveChangesAsync(cancellationToken);
await _mediator.DispatchDomainEventsAsync(this);
return true;
}

之前在 IUnitOfWork 定义的时候讲过一个发送领域事件的方法 DispatchDomainEventsAsync,看一下这个方法的定义

static class MediatorExtension
{
public static async Task DispatchDomainEventsAsync(this IMediator mediator, DbContext ctx)
{
var domainEntities = ctx.ChangeTracker
.Entries<Entity>()
.Where(x => x.Entity.DomainEvents != null && x.Entity.DomainEvents.Any()); var domainEvents = domainEntities
.SelectMany(x => x.Entity.DomainEvents)
.ToList(); domainEntities.ToList()
.ForEach(entity => entity.Entity.ClearDomainEvents()); foreach (var domainEvent in domainEvents)
await mediator.Publish(domainEvent);
}
}

可以看到这里是将所有的实体内的领域事件全部都查找出来,然后通过 mediator 的 Publish 发送领域事件,具体的领域事件的处理注册在 mediator 里面,这里定义了一个 OrderCreatedDomainEventHandler

public class OrderCreatedDomainEventHandler : IDomainEventHandler<OrderCreatedDomainEvent>
{
ICapPublisher _capPublisher;
public OrderCreatedDomainEventHandler(ICapPublisher capPublisher)
{
_capPublisher = capPublisher;
} public async Task Handle(OrderCreatedDomainEvent notification, CancellationToken cancellationToken)
{
await _capPublisher.PublishAsync("OrderCreated", new OrderCreatedIntegrationEvent(notification.Order.Id));
}
}

它继承自 IDomainEventHandler,而 IDomainEventHandler 继承自 INotificationHandler

public interface IDomainEventHandler<TDomainEvent> : INotificationHandler<TDomainEvent>
where TDomainEvent : IDomainEvent
{
//这里我们使用了INotificationHandler的Handle方法来作为处理方法的定义
Task Handle(TDomainEvent domainEvent, CancellationToken cancellationToken);
}

这也就是为什么 IDomainEventHandler 会识别到 DomainEvent 并且进行处理,同样的在定义 DomainEvent 的时候,也需要标识它是一个 DomainEvent

public class OrderCreatedDomainEvent : IDomainEvent
{
public Order Order { get; private set; }
public OrderCreatedDomainEvent(Order order)
{
this.Order = order;
}
}

而 DomainEvent 实际上也是继承自 INotification

public interface IDomainEvent : INotification
{
}

这也就意味着 EventHandler 可以正确的识别到对应的 Event 并且进行处理,这都是 MediatR 的核心能力

领域事件都是定义在 event 目录下,与领域模型定义在一起,所有的领域事件都继承 DomainEvent,分布于这个目录

领域事件的处理 Handler 都定义在 Application 应用层的 Application 下面的 DomainEventHandlers 目录下面

这样的好处是事件的定义与事件的处理是分开的,并且非常的明确知道有哪些领域事件,有哪些领域事件的处理程序

关于 MediatR 再补充一部分内容,在 TransactionBehavior 内可以看到这个类实际上继承自 IPipelineBehavior

namespace MediatR
{
public interface IPipelineBehavior<in TRequest, TResponse>
{
Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next);
}
}

这个接口的作用是在命令或者事件处理的之前或者之后插入逻辑,它的执行的方式有点像中间件的方式,在 Handler 的入参里面有一个 next 的参数,就是指 CommandHandler 或者 EventHandler 的执行的逻辑,在这里就可以决定 Handler 的具体执行之前或者之后,插入一些逻辑

public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
{
var response = default(TResponse);
var typeName = request.GetGenericTypeName(); try
{
// 首先判断当前是否有开启事务
if (_dbContext.HasActiveTransaction)
{
return await next();
} // 定义了一个数据库操作执行的策略,比如说可以在里面嵌入一些重试的逻辑,这里创建了一个默认的策略
var strategy = _dbContext.Database.CreateExecutionStrategy(); await strategy.ExecuteAsync(async () =>
{
Guid transactionId;
using (var transaction = await _dbContext.BeginTransactionAsync())
using (_logger.BeginScope("TransactionContext:{TransactionId}", transaction.TransactionId))
{
_logger.LogInformation("----- 开始事务 {TransactionId} ({@Command})", transaction.TransactionId, typeName, request); response = await next();// next 实际上是指我们的后续操作,这里的模式有点像之前讲的中间件模式 _logger.LogInformation("----- 提交事务 {TransactionId} {CommandName}", transaction.TransactionId, typeName); await _dbContext.CommitTransactionAsync(transaction); transactionId = transaction.TransactionId;
}
}); return response;
}
catch (Exception ex)
{
_logger.LogError(ex, "处理事务出错 {CommandName} ({@Command})", typeName, request); throw;
}
}

这里实现里在执行命令之前判断事务是否开启,如果事务开启的话继续执行后面的逻辑,如果事务没有开启,先开启事务,再执行后面的逻辑

GitHub源码链接:

https://github.com/MingsonZheng/DotNetCoreDevelopmentActualCombat/tree/main/MediatorDemo

https://github.com/witskeeper/geektime

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

欢迎转载、使用、重新发布,但务必保留文章署名 郑子铭 (包含链接: http://www.cnblogs.com/MingsonZheng/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。

如有任何疑问,请与我联系 (MingsonZheng@outlook.com) 。

.NET Core开发实战(第35课:MediatR:让领域事件处理更加优雅)--学习笔记的更多相关文章

  1. 2月送书福利:ASP.NET Core开发实战

    大家都知道我有一个公众号“恰童鞋骚年”,在公众号2020年第一天发布的推文<2020年,请让我重新介绍我自己>中,我曾说到我会在2020年中每个月为所有关注“恰童鞋骚年”公众号的童鞋们送一 ...

  2. [ASP.NET Core开发实战]开篇词

    前言 本系列课程文章主要是学习官方文档,再输出自己学习心得,希望对你有所帮助. 课程大纲 本系列课程主要分为三个部分:基础篇.实战篇和部署篇. 希望通过本系列课程,能让大家初步掌握使用ASP.NET ...

  3. .NET Core开发实战(第11课:文件配置提供程序)--学习笔记

    11 | 文件配置提供程序:自由选择配置的格式 文件配置提供程序 Microsoft.Extensions.Configuration.Ini Microsoft.Extensions.Configu ...

  4. 2、SpringBoot接口Http协议开发实战8节课(1-6)

    1.SpringBoot2.xHTTP请求配置讲解 简介:SpringBoot2.xHTTP请求注解讲解和简化注解配置技巧 1.@RestController and @RequestMapping是 ...

  5. [ASP.NET Core开发实战]基础篇03 中间件

    什么是中间件 中间件是一种装配到应用管道,以处理请求和响应的组件.每个中间件: 选择是否将请求传递到管道中的下一个中间件. 可在管道中的下一个中间件前后执行. ASP.NET Core请求管道包含一系 ...

  6. [ASP.NET Core开发实战]基础篇02 依赖注入

    ASP.NET Core的底层机制之一是依赖注入(DI)设计模式,因此要好好掌握依赖注入的用法. 什么是依赖注入 我们看一下下面的例子: public class MyDependency { pub ...

  7. [ASP.NET Core开发实战]基础篇01 Startup

    Startup,顾名思义,就是启动类,用于配置ASP.NET Core应用的服务和请求管道. Startup有两个主要作用: 通过ConfigureServices方法配置应用的服务.服务是一个提供应 ...

  8. 2、SpringBoot接口Http协议开发实战8节课(7-8)

    7.SpringBoot2.x文件上传实战 简介:讲解HTML页面文件上传和后端处理实战 1.讲解springboot文件上传 MultipartFile file,源自SpringMVC 1)静态页 ...

  9. [ASP.NET Core开发实战]基础篇06 配置

    配置,是应用程序很重要的组成部分,常常用于提供信息,像第三方应用登录钥匙.上传格式与大小限制等等. ASP.NET Core提供一系列配置提供程序读取配置文件或配置项信息. ASP.NET Core项 ...

  10. [ASP.NET Core开发实战]基础篇05 服务器

    什么是服务器 服务器指ASP.NET Core应用运行在操作系统上的载体,也叫Web服务器. Web服务器实现侦听HTTP请求,并以构建HttpContext的对象发送给ASP.NET Core应用. ...

随机推荐

  1. 2020年了,再不会webpack敲得代码就不香了(近万字实战)

    https://zhuanlan.zhihu.com/p/99959392?utm_source=wechat_session&utm_medium=social&utm_oi=619 ...

  2. java基础(3)--pulic class与class的区别

    1.一个类前面的public是可有可无的2.如果一个类使用 public 修饰,则文件名必须与类名一致3.如果一个类前面没有使用public修饰,则文件名可以与类名不一致.当编译成功后会生成对应类名的 ...

  3. RL 基础 | 如何搭建自定义 gym 环境

    需实现的方法: __init__(self): 需定义 action_space 和 observation_space,使用 space.Box 之类来表示(from gym import spac ...

  4. 例2.9 建立一个带头结点的线性链表,用以存放输人的二进制数,链表中每个结点的data域存放一个二进制位。并在此链表上实现对二进制数加1的运算。

    1.题目 例2.9建立一个带头结点的线性链表,用以存放输人的二进制数,链表中每个结点的data域存放一个二进制位.并在此链表上实现对二进制数加1的运算. 2.算法分析 3.代码 /* 二进制加1 */ ...

  5. [转帖]MySQL 8.2.0 GA

    https://cloud.tencent.com/developer/article/2353798 MySQL新的进化版8.2.0于2023年10月25日发行,让我们一起快速浏览一下该版本发生哪些 ...

  6. [转帖]shell编程之循环语句

    目录 一.循环语句 for循环 for语句的结构 嵌套循环 while语句的结构 while语句应用示例 until语句的结构 until语句示例 二.跳出循环 continue跳出循环 break跳 ...

  7. [转帖]AF_UNIX 本地通信

    文章目录 一.AF_UNIX 本地通信 1. Linux进程通信机制 2. socket本地域套接字AF_UNIX 3. demo示例 二.AF_INET域与AF_UNIX域socket通信原理对比 ...

  8. [转帖] TiDB 产品体系介绍

    https://www.modb.pro/db/521269#:~:text=%E4%BC%81%E4%B8%9A%E7%89%88%E5%92%8C%E7%A4%BE%E5%8C%BA%E7%89% ...

  9. [转帖]我们为什么放弃 MongoDB 和 MySQL,选择 TiDB

    https://zhuanlan.zhihu.com/p/164706527 写在前面的话 技术选型是由技术方向和业务场景 trade-off 决定的,脱离业务场景来说技术选型是没有任何意义的,所以本 ...

  10. [转帖]JSR223控件简介

    JSR223控件简介 1.调用内置函数 2.执行外部java文件 3.执行jar包 JSR223取样器允许执行JSR223脚本代码用于创建/更新所需的某些变量. 由于JSR223脚本编译方式基本相同, ...