第34课:MediatR:轻松实现命令查询职责分离模式(CQRS)

学习分享 丨作者 / 郑 子 铭 丨公众号 / DotNet NB / CloudNative NB

核心对象

IMeditator

IRequese、IRequest

IRequestHandler<in TRequest, TResponse>

首先我们安装了 MediatR 的 8.0 的组件包,还安装了依赖注入框架的扩展包,以及依赖注入框架的核心组件包

  • MediatR

  • MediatR.Extensions.Microsoft.DependencyInjection

  • Microsoft.Extensions.DependencyInjection

大家可以观察到 MediatR 的包名和命名空间少了一个 o,猜测是作者故意这样设计的,因为它具体实现里面会有一个接口和类是 Mediator,如果设置同名的话会有一些引用上的问题

var services = new ServiceCollection();

services.AddMediatR(typeof(Program).Assembly);

我们在这里构建一个 ServiceCollection,然后通过一行代码将我们当前的程序集注入进去,它就可以扫描我们当前程序集相关的类,下面看一下我们定义的两个类

internal class MyCommand : IRequest<long>
{ 
    public string CommandName { get; set; }
}

internal class MyCommandHandler : IRequestHandler<MyCommand, long>
{
    public Task<long> Handle(MyCommand request, CancellationToken cancellationToken)
    {
        Console.WriteLine($"MyCommandHandler执行命令:{request.CommandName}");
        return Task.FromResult(10L);
    }
}

第一个类是 MyCommand,它实现了 IRequest 接口,这个接口就代表中介者要执行的命令

第二个类是 MyCommandHandler,它实现了 IRequestHandler 的接口,这个就是我们对命令的处理器的定义

var serviceProvider = services.BuildServiceProvider();

var mediator = serviceProvider.GetService<IMediator>();

await mediator.Send(new MyCommand { CommandName = "cmd01" });

我们从容器里面获取一个 IMediator,然后通过 send 方法发送一个 MyCommand 命令,我们构造了一个新的 MyCommand 的实例传给它

启动程序,输出如下:

MyCommandHandler执行命令:cmd01

我们可以看到 MyCommandHandler 的 Handle 方法执行了,它输出了 MyCommandHandler 的执行命令 cmd01

这样子,这个中介者它有什么好处呢?

大家可以看到,通过中介者模式,我们将命令的构造和命令的处理可以分离开,那么命令的处理如何知道要处理哪个命令呢,就是通过我们泛型的约束来定义的,我们这里为 IRequestHandler 填入了 MyCommand 类型,所以我们能明确知道 MyCommandHandler 是用来处理 MyCommand 的

如果说我在程序里面实现了多个 Handler,我们可以试验一下

internal class MyCommandHandlerV2 : IRequestHandler<MyCommand, long>
{
    public Task<long> Handle(MyCommand request, CancellationToken cancellationToken)
    {
        Console.WriteLine($"MyCommandHandlerV2执行命令:{request.CommandName}");
        return Task.FromResult(10L);
    }
}

internal class MyCommandHandler : IRequestHandler<MyCommand, long>
{
    public Task<long> Handle(MyCommand request, CancellationToken cancellationToken)
    {
        Console.WriteLine($"MyCommandHandler执行命令:{request.CommandName}");
        return Task.FromResult(10L);
    }
}

启动程序,输出如下:

MyCommandHandlerV2执行命令:cmd01

大家可以看到我们输出的是 V2 执行命令

我们把代码进行一个调整,把这个定义移到后面

internal class MyCommandHandler : IRequestHandler<MyCommand, long>
{
    public Task<long> Handle(MyCommand request, CancellationToken cancellationToken)
    {
        Console.WriteLine($"MyCommandHandler执行命令:{request.CommandName}");
        return Task.FromResult(10L);
    }
}

internal class MyCommandHandlerV2 : IRequestHandler<MyCommand, long>
{
    public Task<long> Handle(MyCommand request, CancellationToken cancellationToken)
    {
        Console.WriteLine($"MyCommandHandlerV2执行命令:{request.CommandName}");
        return Task.FromResult(10L);
    }
}

启动程序,输出如下:

MyCommandHandler执行命令:cmd01

大家可以看到我们这次输出的并不是 V2,而是之前的那个命令,为什么会这样子呢?是因为实际上 mediator 对于 IRequestHandler 的扫描,它是有顺序的,后面扫描到的会替换前面扫描到的 Handler,它只会识别其中最后注册进去的一个,也就是说我们在处理 RequestHandler 的时候,我们要注意在注册时仅注册需要的那个

我们再来看看我们的应用程序,回到我们之前的工程里

namespace GeekTime.API.Application.Commands
{
    public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, long>
    {
        IOrderRepository _orderRepository;
        ICapPublisher _capPublisher;
        public CreateOrderCommandHandler(IOrderRepository orderRepository, ICapPublisher capPublisher)
        {
            _orderRepository = orderRepository;
            _capPublisher = capPublisher;
        }


        public async Task<long> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
        {

            var address = new Address("wen san lu", "hangzhou", "310000");
            var order = new Order("xiaohong1999", "xiaohong", 25, address);

            _orderRepository.Add(order);
            await _orderRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
            return order.Id;
        }
    }
}

我们可以看到我们的 CreateOrderCommandHandler 实现的是 IRequestHandler,这也就是解释了为什么之前我们并没有显示的调用 CreateOrderCommandHandler,代码却能够执行到这里的原因

实际上我们在定义我的查询的时候,也可以这样定义,例如我们定义一个 MyOrderQuery,把订单的所有名称都输出出去

namespace GeekTime.API.Application.Queries
{
    public class MyOrderQuery : IRequest<List<string>>
    {
        public MyOrderQuery(string userName) => UserName = userName;

        public string UserName { get; private set; }
    }
}

我们再定义一个查询器,这里就可以从各种地方查询到我们的数据然后返回出去

namespace GeekTime.API.Application.Queries
{
    public class MyOrderQueryHandler : IRequestHandler<MyOrderQuery, List<string>>
    {
        public Task<List<string>> Handle(MyOrderQuery request, CancellationToken cancellationToken)
        {
            return Task.FromResult(new List<string>());
        }
    }
}

实际上我们这样子就完成了查询和查询处理的定义

我们可以在 Controller 定义

[HttpGet]
public async Task<List<string>> QueryOrder([FromBody]MyOrderQuery myOrderQuery)
{
    return await _mediator.Send(myOrderQuery);
}

这样就完成了查询和查询处理逻辑的分离

我们执行命令是同样的实现方式,我们这样子做的话可以将我们的查询的输入和处理定义在一个目录下面,也可以将我们的命令的定义和命令的执行放在一个目录下面,使我们的 Controller 关注于身份认证或者基础设施缓存等等一些逻辑的处理,它不再关心说我的业务逻辑是怎么样子的

GitHub源码链接:https://github.com/MingsonZheng/DotNetCoreDevelopmentActualCombat/tree/main/MediatorDemo

https://github.com/witskeeper/geektime

Last updated