CQRS with MediatR#
简介#
职责分离:CQRS 将读取和写入操作分开,避免它们混在一起。命令(Commands)只关注数据的更改,查询(Queries)只关注数据的读取,这样可以更清晰地管理业务逻辑。
解耦:使用 MediatR 可以解耦各个组件之间的直接依赖。MediatR 充当了一个中介者,接收请求并将其转发到相应的处理器。这样,Controller 不再直接依赖于具体的业务逻辑实现,而是通过 MediatR 发送命令或查询,接收响应。
可扩展性:随着系统需求的增长,CQRS 架构使得可以根据不同的需求扩展读取和写入操作。例如,查询操作可能会涉及复杂的聚合或者缓存策略,而命令操作可能涉及复杂的事务和状态变更。CQRS 可以让这些两者的扩展更加独立,避免了对现有功能的修改。
性能优化:由于读取操作和写入操作的职责分开,你可以对查询部分进行更细粒度的优化,比如缓存、只读数据库、或者特殊的查询模型,而写入部分则可以专注于数据一致性和事务。
简化测试:通过 MediatR 和 CQRS,测试变得更加简单,因为每个命令和查询都有单独的处理逻辑,便于独立测试。你不再需要担心读取和写入逻辑混在一起所带来的复杂性。
举个例子,假设你有一个电商平台,想要查询用户的订单列表和创建新订单。使用 CQRS 后,你可以把查询订单的操作与创建订单的操作分开,查询操作可能是只读的且可以优化缓存,而创建订单则需要考虑事务和数据一致性。
简单来说,CQRS 和 MediatR 在 WebAPI 中的作用就是帮助开发者保持代码的简洁、模块化,同时提升系统的可维护性和可扩展性。
接下来进行重构CreateRestaurant方法#
在Application安装
Install-Package MediatR -Version 12.4.1
Install-Package FluentValidation.AspNetCore -Version 11.3.0
Install-Package AutoMapper.Extensions.Microsoft.DependencyInjection -Version 12.0.1
新建CreateRestaurantCommand.cs、CreateRestaurantCommandHandler.cs、CreateRestaurantCommandValidator.cs
using MediatR;
namespace CleanArchitecture.Application.Restaurants.Commands.CreateRestaurant;
public class CreateRestaurantCommand : IRequest<int>
{
public string Name { get; set; } = default!;
public string Description { get; set; } = default!;
public string Category { get; set; } = default!;
public bool HasDelivery { get; set; }
public string? ContactEmail { get; set; }
public string? ContactNumber { get; set; }
public string? City { get; set; }
public string? Street { get; set; }
public string? PostalCode { get; set; }
}
using FluentValidation;
namespace CleanArchitecture.Application.Restaurants.Commands.CreateRestaurant;
public class CreateRestaurantCommandValidator : AbstractValidator<CreateRestaurantCommand>
{
private readonly List<string> validCategorys = ["蔬菜"];
public CreateRestaurantCommandValidator()
{
RuleFor(x => x.Name).Length(2, 100);
//自定义
RuleFor(x => x.Category)
.Must(validCategorys.Contains)
.WithMessage("分类不对");
RuleFor(x => x.ContactNumber)
.NotEmpty()
.WithMessage("请输入正确手机号码");
}
}
using AutoMapper;
using CleanArchitecture.Domain.Entities;
using CleanArchitecture.Domain.Repositories;
using MediatR;
using Microsoft.Extensions.Logging;
namespace CleanArchitecture.Application.Restaurants.Commands.CreateRestaurant;
public class CreateRestaurantCommandHandler(ILogger<CreateRestaurantCommandHandler> logger,
IMapper mapper,
IRestaurantsRepository restaurantsRepository) : IRequestHandler<CreateRestaurantCommand, int>
{
public async Task<int> Handle(CreateRestaurantCommand request, CancellationToken cancellationToken)
{
logger.LogInformation("新增餐馆");
var restaurant = mapper.Map<Restaurant>(request);
int id = await restaurantsRepository.CreateAsync(restaurant);
return id;
}
}
修改RestaurantProfile.cs
using AutoMapper;
using CleanArchitecture.Application.Restaurants.Commands.CreateRestaurant;
using CleanArchitecture.Domain.Entities;
namespace CleanArchitecture.Application.Restaurants.Dtos;
public class RestaurantProfile : Profile
{
public RestaurantProfile()
{
//使用MediatR把CreateRestaurantDto改成CreateRestaurantCommand
CreateMap<CreateRestaurantCommand, Restaurant>()
.ForMember(d => d.Address, opt => opt.MapFrom(src => new Address()
{
City = src.City,
Street = src.Street,
PostalCode = src.PostalCode,
}));
CreateMap<Restaurant, RestaurantDto>()
.ForMember(d => d.City, opt => opt.MapFrom(src => src == null ? null : src.Address.City))
.ForMember(d => d.Street, opt => opt.MapFrom(src => src == null ? null : src.Address.Street))
.ForMember(d => d.PostalCode, opt => opt.MapFrom(src => src == null ? null : src.Address.PostalCode))
.ForMember(d => d.Dishes, opt => opt.MapFrom(src => src.Dishes));
}
}
在Application中的ServiceCollectionExtensions添加MediatR
using CleanArchitecture.Application.Restaurants;
using FluentValidation;
using FluentValidation.AspNetCore;
using Microsoft.Extensions.DependencyInjection;
namespace CleanArchitecture.Application.Extensions;
public static class ServiceCollectionExtensions
{
public static void AddApplication(this IServiceCollection services)
{
var applicationAssembly = typeof(ServiceCollectionExtensions).Assembly;
services.AddScoped<IRestaurantsService, RestaurantsService>();
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(applicationAssembly));
services.AddAutoMapper(applicationAssembly);
services.AddValidatorsFromAssembly(applicationAssembly)
.AddFluentValidationAutoValidation();
}
}
修改RestaurantsController.cs
using CleanArchitecture.Application.Restaurants;
using CleanArchitecture.Application.Restaurants.Commands.CreateRestaurant;
using CleanArchitecture.Application.Restaurants.Dtos;
using MediatR;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Mvc;
namespace CleanArchitecture.API.Controllers;
[Route("api/[controller]")]
[ApiController]
public class RestaurantsController(IRestaurantsService restaurantsService, IMediator mediator) : ControllerBase
{
[HttpGet]
public async Task<IActionResult> GetAll()
{
var restaurants = await restaurantsService.GetAllRestaurants();
return Ok(restaurants);
}
[HttpGet("{id}")]
public async Task<IActionResult> GetId([FromRoute] int id)
{
var restaurant = await restaurantsService.GetId(id);
if (restaurant is null)
return NotFound();
return Ok(restaurant);
}
[HttpPost]
public async Task<IActionResult> CreateRestaurant([FromBody] CreateRestaurantCommand command)
{
int id = await mediator.Send(command);
return CreatedAtAction(nameof(GetId), new { id }, null);
}
}