Skip to content
On this page

MediatR:让命令和查询有个约会

了解CQRS

什么是CQRS

CQRS,全称是 Command Query Responsibility Segregation,即命令查询职责分离。它是一种架构模式,旨在将应用程序的查询和命令操作分离开来,以提高应用程序的可扩展性、灵活性和可维护性。

CQRS 模式中,查询和命令操作是分离的,每个操作都有自己的对象模型和数据存储,以及相应的业务逻辑。这种分离可以使查询和命令操作在不同的处理器上进行处理,以获得更好的可扩展性和性能。同时,CQRS 模式也可以使应用程序更容易维护,因为查询和命令操作的代码是分离的,可以更容易地进行修改和调试。

在 CQRS 模式中,有两种类型的操作:

  • 命令(Command):用于修改应用程序的状态,比如创建、更新或删除数据。
  • 查询(Query):用于检索应用程序的状态,比如从数据存储中检索数据。

传统MVC模式

csharp
public interface IUserService
{
    int CreateUser(string name, string email, string password);
    User GetUserById(int id);
}

public class UserService : IUserService
{
    private readonly IUserRepository _userRepository;

    public UserService(IUserRepository userRepository)
    {
        _userRepository = userRepository;
    }

    public int CreateUser(string name, string email, string password)
    {
        var user = new User
        {
            Name = name,
            Email = email,
            Password = password
        };

        _userRepository.Create(user);

        return user.Id;
    }

    public User GetUserById(int id)
    {
        var user = _userRepository.GetById(id);

        return user;
    }
}
csharp
public class UserController : ControllerBase
{
    private readonly IUserService _userService;

    public UserController(IUserService userService)
    {
        _userService = userService;
    }

    [HttpPost]
    public IActionResult CreateUser(string name, string email, string password)
    {
        var userId = _userService.CreateUser(name, email, password);

        return Ok(userId);
    }

    [HttpGet("{id}")]
    public IActionResult GetUserById(int id)
    {
        var user = _userService.GetUserById(id);

        return Ok(user);
    }
}

CQRS模式

csharp
public class CreateUserCommand : ICommand<int>
{
    public string Name { get; set; }
    public string Email { get; set; }
    public string Password { get; set; }
}

public class GetUserByIdQuery : IQuery<User>
{
    public int Id { get; set; }
}

public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
    public string Password { get; set; }
}

public interface ICommand<TOutput> { }

public interface IQuery<TOutput> { }

public interface ICommandHandler<TCommand, TOutput> where TCommand : ICommand<TOutput>
{
    TOutput Handle(TCommand command);
}

public interface IQueryHandler<TQuery, TOutput> where TQuery : IQuery<TOutput>
{
    TOutput Handle(TQuery query);
}

public class CreateUserCommandHandler : ICommandHandler<CreateUserCommand, int>
{
    private readonly IUserRepository _userRepository;

    public CreateUserCommandHandler(IUserRepository userRepository)
    {
        _userRepository = userRepository;
    }

    public int Handle(CreateUserCommand command)
    {
        var user = new User
        {
            Name = command.Name,
            Email = command.Email,
            Password = command.Password
        };

        _userRepository.Create(user);

        return user.Id;
    }
}

public class GetUserByIdQueryHandler : IQueryHandler<GetUserByIdQuery, User>
{
    private readonly IUserRepository _userRepository;

    public GetUserByIdQueryHandler(IUserRepository userRepository)
    {
        _userRepository = userRepository;
    }

    public User Handle(GetUserByIdQuery query)
    {
        var user = _userRepository.GetById(query.Id);

        return user;
    }
}
csharp
public class UserController : ControllerBase
{
    private readonly IUserRepository _userRepository;
    private readonly ICommandHandler<CreateUserCommand, int> _createUserHandler;
    private readonly IQueryHandler<GetUserByIdQuery, User> _getUserByIdHandler;

    public UserController(IUserRepository userRepository,
                           ICommandHandler<CreateUserCommand, int> createUserHandler,
                           IQueryHandler<GetUserByIdQuery, User> getUserByIdHandler)
    {
        _userRepository = userRepository;
        _createUserHandler = createUserHandler;
        _getUserByIdHandler = getUserByIdHandler;
    }

    [HttpPost]
    public IActionResult CreateUser(CreateUserCommand command)
    {
        var userId = _createUserHandler.Handle(command);

        return Ok(userId);
    }

    [HttpGet("{id}")]
    public IActionResult GetUserById(int id)
    {
        var query = new GetUserByIdQuery { Id = id };
        var user = _getUserByIdHandler.Handle(query);

        return Ok(user);
    }
}

CQRS+中介者模式 进一步改写

首先,定义一个中介者接口IMediator,它包含了处理命令和查询的抽象方法。

csharp
public interface IMediator
{
    TOutput Send<TOutput>(IQuery<TOutput> query);
    TOutput Send<TOutput>(ICommand<TOutput> command);
}

然后,实现中介者Mediator,它依赖于所有的命令和查询处理程序。在Send方法中,根据命令或查询的类型,将其发送给对应的处理程序。

csharp
public class Mediator : IMediator
{
    private readonly IUserRepository _userRepository;
    private readonly ICommandHandler<CreateUserCommand, int> _createUserHandler;
    private readonly IQueryHandler<GetUserByIdQuery, User> _getUserByIdHandler;

    public Mediator(IUserRepository userRepository,
                    ICommandHandler<CreateUserCommand, int> createUserHandler,
                    IQueryHandler<GetUserByIdQuery, User> getUserByIdHandler)
    {
        _userRepository = userRepository;
        _createUserHandler = createUserHandler;
        _getUserByIdHandler = getUserByIdHandler;
    }

    public TOutput Send<TOutput>(IQuery<TOutput> query)
    {
        if (query is GetUserByIdQuery getUserByIdQuery)
        {
            return _getUserByIdHandler.Handle(getUserByIdQuery);
        }

        throw new NotSupportedException("Unsupported query type.");
    }

    public TOutput Send<TOutput>(ICommand<TOutput> command)
    {
        if (command is CreateUserCommand createUserCommand)
        {
            return _createUserHandler.Handle(createUserCommand);
        }
        
        throw new NotSupportedException("Unsupported command type.");
    }
}

最后,修改UserController,将依赖于处理程序的代码改为依赖于中介者。

csharp
public class UserController : ControllerBase
{
    private readonly IMediator _mediator;

    public UserController(IMediator mediator)
    {
        _mediator = mediator;
    }

    [HttpPost]
    public IActionResult CreateUser(CreateUserCommand command)
    {
        var userId = _mediator.Send(command);

        return Ok(userId);
    }

    [HttpGet("{id}")]
    public IActionResult GetUserById(int id)
    {
        var query = new GetUserByIdQuery { Id = id };
        var user = _mediator.Send(query);

        return Ok(user);
    }
}

在日常项目中不止CreateUserGetUserById,还有一些其他的命令和查询操作,该怎么办?

可以使用依赖注入容器来解决这个问题。将所有的命令和查询处理程序都注册到容器中,然后在中介者的构造函数中注入容器并使用容器来获取处理程序。这样,中介者就不需要知道所有的处理程序,而是通过容器来获取它们。

使用.NET Core自带的依赖注入容器,可以在Startup.cs文件中注册所有的命令和查询处理程序:

csharp
// 注册所有的命令和查询处理程序
builder.Services.AddScoped<ICommandHandler<CreateUserCommand, int>, CreateUserCommandHandler>();
builder.Services.AddScoped<IQueryHandler<GetUserByIdQuery, User>, GetUserByIdQueryHandler>();

// 注册中介者
builder.Services.AddScoped<IMediator, Mediator>();

// 注册用户仓储
builder.Services.AddScoped<IUserRepository, UserRepository>();

然后,修改中介者的构造函数,注入IServiceProvider并使用它来获取命令和查询处理程序:

csharp
public class Mediator : IMediator
{
    private readonly IServiceProvider _serviceProvider;

    public Mediator(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public TOutput Send<TOutput>(IQuery<TOutput> query)
    {
        var handler = _serviceProvider.GetService<IQueryHandler<TQuery, TOutput>>();
        return handler.Handle(query);
    }

    public TOutput Send<TOutput>(ICommand<TOutput> command)
    {
        var handler = _serviceProvider.GetService<ICommandHandler<TCommand, TOutput>>();
        return handler.Handle(command);
    }
}

然后,Controller 中需要注入 IMediator 接口,然后通过它来发送命令和查询。具体代码如下:

csharp
public class UserController : ControllerBase
{
    private readonly IMediator _mediator;

    public UserController(IMediator mediator)
    {
        _mediator = mediator;
    }

    [HttpPost]
    public async Task<IActionResult> CreateUser(CreateUserCommand command)
    {
        int userId = await _mediator.Send(command);
        return Ok(userId);
    }

    [HttpGet("{id}")]
    public async Task<IActionResult> GetUserById(int id)
    {
        GetUserByIdQuery query = new GetUserByIdQuery { Id = id };
        User user = await _mediator.Send(query);
        return Ok(user);
    }
}

MediatR 简介

什么是 MediatR

MediatR 是一个 .NET 中介者库,它帮助我们实现 CQRS 和中介者模式。它提供了一种将请求和响应对象分离的方式,并将它们传递给一个或多个请求处理程序来处理请求的机制。

使用MediatR实现CQRS

csharp
public class CreateUserCommand : IRequest<int>
{
    public string Name { get; set; }
    public string Email { get; set; }
    public string Password { get; set; }
}

public class GetUserByIdQuery : IRequest<User>
{
    public int Id { get; set; }
}

public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
    public string Password { get; set; }
}

public class CreateUserCommandHandler : IRequestHandler<CreateUserCommand, int>
{
    private readonly IUserRepository _userRepository;

    public CreateUserCommandHandler(IUserRepository userRepository)
    {
        _userRepository = userRepository;
    }

    public Task<int> Handle(CreateUserCommand request, CancellationToken cancellationToken)
    {
        var user = new User
        {
            Name = request.Name,
            Email = request.Email,
            Password = request.Password
        };

        _userRepository.Create(user);

        return Task.FromResult(user.Id);
    }
}

public class GetUserByIdQueryHandler : IRequestHandler<GetUserByIdQuery, User>
{
    private readonly IUserRepository _userRepository;

    public GetUserByIdQueryHandler(IUserRepository userRepository)
    {
        _userRepository = userRepository;
    }

    public Task<User> Handle(GetUserByIdQuery request, CancellationToken cancellationToken)
    {
        var user = _userRepository.GetById(request.Id);

        return Task.FromResult(user);
    }
}
csharp
public class UserController : ControllerBase
{
    private readonly IMediator _mediator;

    public UserController(IMediator mediator)
    {
        _mediator = mediator;
    }

    [HttpPost]
    public async Task<IActionResult> CreateUser(CreateUserCommand command)
    {
        var userId = await _mediator.Send(command);

        return Ok(userId);
    }

    [HttpGet("{id}")]
    public async Task<IActionResult> GetUserById(int id)
    {
        var query = new GetUserByIdQuery { Id = id };
        var user = await _mediator.Send(query);

        return Ok(user);
    }
}

MediatR 的优势和用途

MediatR 可以使代码更具可读性、可维护性和可测试性。它可以帮助我们实现 CQRS 和中介者模式,并简化代码的结构和逻辑。MediatR 可以应用于 Web 应用程序、后台服务、消息队列等场景。

什么情况下推荐使用MediatR

  • 当您的应用程序需要实现 CQRS 模式时,MediatR 可以帮助您实现请求/响应逻辑的分离。
  • 当您的应用程序需要处理大量的请求和响应时,使用 MediatR 可以使您的代码更加清晰和易于维护。
  • 当您的应用程序需要实现复杂的业务逻辑时,使用 MediatR 可以使您的代码更加模块化和可重用。
  • 当您需要在不同的层之间传递数据时,使用 MediatR 可以使您的代码更加灵活和可扩展。

MediatR 基础

安装 MediatR NuGet 包

可以使用 Visual Studio 的 NuGet 包管理器或者通过命令行来安装:Install-Package MediatR

定义请求和响应对象

请求对象: 表示需要执行的操作 响应对象: 表示执行操作后的结果 定义请求对象和响应对象可以使用泛型接口 IRequest<TResponse> 和普通类

泛型类:

csharp
public class MyRequest<T> : IRequest<MyResponse<T>>
{
    public T Data { get; set; }
}

public class MyResponse<T>
{
    public T Result { get; set; }
    public bool Success { get; set; }
}

普通类:

csharp
public class HelloRequest : IRequest<HelloResponse>
{
    public string Name { get; set; }
}

public class HelloResponse
{
    public string Greeting { get; set; }
}

需要注意的是,请求对象必须实现 IRequest 接口,并指定响应类型作为泛型参数。这样,MediatR 才能够自动找到正确的处理程序,并将请求转发给它。

定义请求处理程序

请求处理程序是一个实现 IRequestHandler<TRequest, TResponse> 接口的类。可以在类中实现 Handle 方法来处理请求。

csharp
public class HelloRequestHandler : IRequestHandler<HelloRequest, HelloResponse>
{
    public Task<string> Handle(HelloRequest request, CancellationToken cancellationToken)
    {
        var greeting = $"Hello, {request.Name}!";
        return Task.FromResult(greeting);
    }
}

MediatR 进阶

注册请求处理程序

在应用程序启动时,需要将请求处理程序注册到服务容器中,以便 MediatR 能够将请求发送到处理程序。可以使用 AddMediatR 方法来注册请求处理程序。

在 Program.cs 文件中进行配置:

csharp
// 注册 MediatR
builder.Services.AddMediatR(typeof(Program).Assembly);

发送请求并获取响应

可以使用 IMediator 接口的 Send 方法来发送请求,并使用 await 关键字等待响应。

csharp
var response = await mediator.Send(new HelloRequest { Name = "MediatR" });
Console.WriteLine(response);

使用管道行为

MediatR 允许我们在请求处理程序执行前或执行后添加一些行为。可以使用 IPipelineBehavior<TRequest, TResponse> 接口来实现管道行为。

csharp
public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
{
    public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
    {
        Console.WriteLine($"Handling {typeof(TRequest).Name}");
        var response = await next();
        Console.WriteLine($"Handled {typeof(TRequest).Name}");
        return response;
    }
}

builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));

MediatR 实战

使用 MediatR 实现 CQRS

CQRS 是一种将读操作和写操作分离的架构模式。可以使用 MediatR 来实现 CQRS。将读操作表示为查询请求,将写操作表示为命令请求,使用不同的请求对象来分离它们。

csharp
public class GetProductsQuery : IRequest<List<Product>>
{
}

public class CreateProductCommand : IRequest
{
    public string Name { get; set; }
    public decimal Price { get; set; }
}

public class ProductCommandHandler : IRequestHandler<CreateProductCommand>
{
    private readonly DbContext _dbContext;

    public ProductCommandHandler(DbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task<Unit> Handle(CreateProductCommand request, CancellationToken cancellationToken)
    {
        var product = new Product
        {
            Name = request.Name,
            Price = request.Price
        };
        await _dbContext.Products.AddAsync(product);
        await _dbContext.SaveChangesAsync(cancellationToken);
        return Unit.Value;
    }
}

public class ProductQueryHandler : IRequestHandler<GetProductsQuery, List<Product>>
{
    private readonly DbContext _dbContext;

    public ProductQueryHandler(DbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task<List<Product>> Handle(GetProductsQuery request, CancellationToken cancellationToken)
    {
        var products = await _dbContext.Products.ToListAsync(cancellationToken);
        return products;
    }
}

使用 MediatR 实现中介者模式

中介者模式是一种将对象之间的通信集中到一个中介对象中的设计模式。可以使用 MediatR 来实现中介者模式。将不同类型的请求对象发送到中介者对象,中介者对象将它们分发给对应的请求处理程序来处理。

csharp
public class MessageMediator : IMessageMediator
{
    private readonly IMediator _mediator;

    public MessageMediator(IMediator mediator)
    {
        _mediator = mediator;
    }

    public async Task<TResponse> Send<TResponse>(IMessage<TResponse> message)
    {
        return await _mediator.Send(message);
    }
}

public interface IMessageMediator
{
    Task<TResponse> Send<TResponse>(IMessage<TResponse> message);
}

public interface IMessage<TResponse>
{
}

public class HelloMessage : IMessage<string>
{
    public string Name { get; set; }
}

public class HelloMessageHandler : IRequestHandler<HelloMessage, string>
{
    public Task<string> Handle(HelloMessage message, CancellationToken cancellationToken)
    {
        var greeting = $"Hello, {message.Name}!";
        return Task.FromResult(greeting);
    }
}

使用 MediatR 实现事件驱动架构

事件驱动架构是一种将应用程序分解为一系列事件和事件处理程序的设计模式。可以使用 MediatR 来实现事件驱动架构。将事件表示为请求对象,将事件处理程序表示为请求处理程序。

csharp
public class OrderCreatedEvent : INotification
{
    public int OrderId { get; set; }
}

public class OrderCreatedEventHandler : INotificationHandler<OrderCreatedEvent>
{
    public Task Handle(OrderCreatedEvent notification, CancellationToken cancellationToken)
    {
        Console.WriteLine($"Order {notification.OrderId} created.");
        return Task.CompletedTask;
    }
}

builder.Services.AddMediatR(typeof(Program).Assembly);
builder.Services.AddTransient(typeof(INotificationHandler<>), typeof(OrderCreatedEventHandler));

MediatR 最佳实践

如何组织请求和响应对象

应该将请求和响应对象放在同一个项目中,并按照业务逻辑进行组织。可以使用命名空间来将它们分组。

csharp
namespace MyApplication.Requests.Products
{
    public class GetProductByIdQuery : IRequest<Product>
    {
        public int Id { get; set; }
    }

    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }
}

如何组织请求处理程序

应该将请求处理程序放在同一个项目中,并按照业务逻辑进行组织。可以使用命名空间来将它们分组。

csharp
namespace MyApplication.Handlers.Products
{
    public class GetProductByIdQueryHandler : IRequestHandler<GetProductByIdQuery, Product>
    {
        private readonly MyDbContext _dbContext;

        public GetProductByIdQueryHandler(MyDbContext dbContext)
        {
            _dbContext = dbContext;
        }

        public async Task<Product> Handle(GetProductByIdQuery request, CancellationToken cancellationToken)
        {
            var product = await _dbContext.Products.FindAsync(request.Id);
            return new Product { Id = product.Id, Name = product.Name };
        }
    }
}

如何测试 MediatR 应用程序

可以使用单元测试框架来测试 MediatR 应用程序。可以模拟请求对象和请求处理程序,并验证它们的行为和结果是否符合预期。

csharp
public class GetProductByIdQueryHandlerTests
{
    private readonly Mock<MyDbContext> _dbContextMock;
    private readonly GetProductByIdQueryHandler _handler;

    public GetProductByIdQueryHandlerTests()
    {
        _dbContextMock = new Mock<MyDbContext>();
        _handler = new GetProductByIdQueryHandler(_dbContextMock.Object);
    }

    [Fact]
    public async Task Handle_ShouldReturnProduct_WhenProductExists()
    {
        // Arrange
        var query = new GetProductByIdQuery { Id = 1 };
        var product = new MyEntity { Id = 1, Name = "Product 1" };
        _dbContextMock.Setup(x => x.Products.FindAsync(1)).ReturnsAsync(product);

        // Act
        var result = await _handler.Handle(query, CancellationToken.None);

        // Assert
        Assert.NotNull(result);
        Assert.Equal(1, result.Id);
        Assert.Equal("Product 1", result.Name);
    }
}

MediatR 应用场景

Web 应用程序

可以在 ASP.NET Core 控制器中使用 MediatR 来处理请求,并将响应返回给客户端。

后台服务

可以在后台服务中使用 MediatR 来处理任务,并将结果发送到消息队列或数据库中。

消息队列

可以使用 MediatR 来将消息发送到消息队列,并将它们分发给对应的请求处理程序来处理。

MediatR 与其他框架的集成

ASP.NET Core

MediatR 与 ASP.NET Core 框架的集成非常简单,只需要在 Program.cs 中进行服务注册即可。

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

在上面的代码中,我们注册了 MediatR 服务,并指定了要扫描的程序集。 在控制器中,我们可以使用注入的 IMediator 接口来调用 MediatR 请求处理器。

c#
public class MyController : ControllerBase
{
    private readonly IMediator _mediator;

    public MyController(IMediator mediator)
    {
        _mediator = mediator;
    }

    [HttpGet("{id}")]
    public async Task<IActionResult> Get(int id)
    {
        var result = await _mediator.Send(new MyQuery { Id = id });
        return Ok(result);
    }
}

在上面的代码中,我们通过构造函数注入了 IMediator 接口,并在 Get 方法中使用它来调用请求处理器。

Entity Framework Core

MediatR 与 Entity Framework Core 的集成也非常简单,只需要将 DbContext 注入到请求处理器中即可。

csharp
public class MyQueryHandler : IRequestHandler<MyQuery, MyResponse>
{
    private readonly MyDbContext _dbContext;

    public MyQueryHandler(MyDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task<MyResponse> Handle(MyQuery request, CancellationToken cancellationToken)
    {
        var entity = await _dbContext.MyEntities.FindAsync(request.Id);
        return new MyResponse { Name = entity.Name };
    }
}

在上面的代码中,我们通过构造函数注入了 MyDbContext,并在 Handle 方法中使用它来查询数据库。

AutoMapper

MediatR 与 AutoMapper 的集成可以帮助我们更加方便地进行对象映射。

csharp
public class MyProfile : Profile
{
    public MyProfile()
    {
        CreateMap<MyEntity, MyResponse>();
    }
}

public class MyQueryHandler : IRequestHandler<MyQuery, MyResponse>
{
    private readonly IMapper _mapper;
    private readonly MyDbContext _dbContext;

    public MyQueryHandler(IMapper mapper, MyDbContext dbContext)
    {
        _mapper = mapper;
        _dbContext = dbContext;
    }

    public async Task<MyResponse> Handle(MyQuery request, CancellationToken cancellationToken)
    {
        var entity = await _dbContext.MyEntities.FindAsync(request.Id);
        return _mapper.Map<MyResponse>(entity);
    }
}

在上面的代码中,我们定义了一个 AutoMapper 的配置文件 MyProfile,并在请求处理器中注入了 IMapper 接口。在 Handle 方法中,我们使用 IMapper 来进行对象映射。

FluentValidation

MediatR 与 FluentValidation 的集成可以帮助我们更加方便地进行请求和响应模型的验证。以下是一个示例:

csharp
public class MyQueryValidator : AbstractValidator<MyQuery>
{
    public MyQueryValidator()
    {
        RuleFor(x => x.Id).GreaterThan(0);
    }
}

public class MyQueryHandler : IRequestHandler<MyQuery, MyResponse>
{
    private readonly MyDbContext _dbContext;

    public MyQueryHandler(MyDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task<MyResponse> Handle(MyQuery request, CancellationToken cancellationToken)
    {
        var entity = await _dbContext.MyEntities.FindAsync(request.Id);
        return new MyResponse { Name = entity.Name };
    }
}

在上面的代码中,我们定义了一个 MyQueryValidator,用于验证 MyQuery 请求模型中的 Id 属性。在请求处理器中,我们可以省略对请求模型的手动验证,因为 MediatR 会自动执行验证操作。

Date: 2023/05/12

Authors: 彭强强

Tags: mediatR、CQRS