
使用MediatR在ASP.NET Core中实现CQRS架构模式的实践:从概念到清晰代码的旅程
你好,我是源码库的博主。在构建和维护日益复杂的ASP.NET Core应用时,你是否曾感到控制器(Controller)变得臃肿不堪,业务逻辑、数据访问、验证代码全都纠缠在一起,像一个理不清的毛线团?单元测试也变得举步维艰。这正是我几年前面临的困境,直到我系统地实践了CQRS(命令查询职责分离)模式,并借助一个名为MediatR的轻量级库,才让代码重新变得清晰、可维护。今天,我想和你分享这段实战经验,包括那些我踩过的“坑”和最终的解决方案。
CQRS的核心思想很简单:将修改状态的操作(命令,Command)和读取状态的操作(查询,Query)分离,使用不同的模型来处理。这听起来可能有点抽象,但MediatR巧妙地将其简化为一种“进程内消息传递”机制,让实现变得异常优雅。它不是一个庞大的框架,而是一个“胶水”库,帮助我们在ASP.NET Core中自然地组织代码。
第一步:项目准备与MediatR集成
首先,我们创建一个新的ASP.NET Core Web API项目。然后,通过NuGet安装必要的包:
dotnet add package MediatR
dotnet add package MediatR.Extensions.Microsoft.DependencyInjection
接下来,在 `Program.cs` 中注册MediatR服务。这里有个小技巧:使用 `AddMediatR` 并指定程序集,它会自动扫描并注册该程序集内所有的 `IRequestHandler`。
// Program.cs
using MediatR;
var builder = WebApplication.CreateBuilder(args);
// 添加服务到容器
builder.Services.AddControllers();
builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));
// ... 其他服务配置
var app = builder.Build();
// ... 配置HTTP请求管道
app.Run();
至此,MediatR的基础集成就完成了。是不是很简单?但别急,真正的魔法才刚刚开始。
第二步:定义第一个查询(Query)及其处理器
让我们从一个简单的查询开始。假设我们有一个产品目录,需要查询产品列表。
首先,定义查询模型。它只是一个普通的C#类,但实现了 `IRequest` 接口,其中 `TResponse` 是你期望返回的数据类型。
// Features/Products/GetProductList/GetProductListQuery.cs
using MediatR;
namespace YourProject.Features.Products.GetProductList
{
public class GetProductListQuery : IRequest<List>
{
// 这里可以添加查询参数,例如分页、过滤条件等
public bool? IsActive { get; set; }
}
}
接着,定义返回的数据传输对象(DTO)。
// Features/Products/GetProductList/ProductDto.cs
namespace YourProject.Features.Products.GetProductList
{
public class ProductDto
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
}
}
现在,创建查询处理器(Handler)。这是业务逻辑所在的地方。处理器需要实现 `IRequestHandler` 接口。
// Features/Products/GetProductList/GetProductListQueryHandler.cs
using MediatR;
using Microsoft.EntityFrameworkCore; // 假设使用EF Core
using YourProject.Data;
namespace YourProject.Features.Products.GetProductList
{
public class GetProductListQueryHandler : IRequestHandler<GetProductListQuery, List>
{
private readonly ApplicationDbContext _context;
public GetProductListQueryHandler(ApplicationDbContext context)
{
_context = context;
}
public async Task<List> Handle(GetProductListQuery request, CancellationToken cancellationToken)
{
// 构建查询 - 这里是纯数据查询逻辑
var query = _context.Products.AsQueryable();
if (request.IsActive.HasValue)
{
query = query.Where(p => p.IsActive == request.IsActive.Value);
}
// 映射到DTO并返回
var productList = await query
.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name,
Price = p.Price
})
.ToListAsync(cancellationToken);
return productList;
}
}
}
踩坑提示:初期我常犯一个错误,就是在Handler里写太多业务规则校验或调用其他复杂服务。记住,查询Handler应专注于高效、准确地获取数据。复杂的业务规则(特别是写操作相关的)应放在命令(Command)中。
第三步:定义第一个命令(Command)及其处理器
命令用于修改数据。让我们创建一个“创建产品”的命令。
// Features/Products/CreateProduct/CreateProductCommand.cs
using MediatR;
namespace YourProject.Features.Products.CreateProduct
{
public class CreateProductCommand : IRequest // 返回新产品的ID
{
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
public string Description { get; set; } = string.Empty;
}
}
命令处理器包含了核心的业务逻辑、验证和持久化操作。
// Features/Products/CreateProduct/CreateProductCommandHandler.cs
using FluentValidation; // 推荐使用FluentValidation进行验证
using MediatR;
using Microsoft.EntityFrameworkCore;
using YourProject.Data;
using YourProject.Models;
namespace YourProject.Features.Products.CreateProduct
{
public class CreateProductCommandHandler : IRequestHandler
{
private readonly ApplicationDbContext _context;
private readonly IValidator _validator;
public CreateProductCommandHandler(ApplicationDbContext context, IValidator validator)
{
_context = context;
_validator = validator;
}
public async Task Handle(CreateProductCommand request, CancellationToken cancellationToken)
{
// 1. 验证输入
var validationResult = await _validator.ValidateAsync(request, cancellationToken);
if (!validationResult.IsValid)
{
// 通常这里会抛出一个自定义的验证异常,由全局过滤器处理
throw new ValidationException(validationResult.Errors);
}
// 2. 业务逻辑检查(例如,产品名是否重复)
bool nameExists = await _context.Products.AnyAsync(p => p.Name == request.Name, cancellationToken);
if (nameExists)
{
throw new InvalidOperationException($"产品名称 '{request.Name}' 已存在。");
}
// 3. 创建领域实体
var product = new Product
{
Name = request.Name,
Price = request.Price,
Description = request.Description,
IsActive = true,
CreatedAt = DateTime.UtcNow
};
// 4. 持久化
_context.Products.Add(product);
await _context.SaveChangesAsync(cancellationToken);
// 5. 返回结果(例如新ID)
return product.Id;
}
}
}
你可以看到,命令处理器像一个清晰的“用例”或“工作流”,步骤分明。验证逻辑通过依赖注入的 `IValidator` 分离,保持了Handler的整洁。
第四步:简化控制器(Controller)
现在,我们的控制器变得极其精简,它只负责HTTP层面的工作:接收请求、发送消息(MediatR)、返回响应。
// Controllers/ProductsController.cs
using MediatR;
using Microsoft.AspNetCore.Mvc;
using YourProject.Features.Products.CreateProduct;
using YourProject.Features.Products.GetProductList;
namespace YourProject.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IMediator _mediator;
public ProductsController(IMediator mediator)
{
_mediator = mediator;
}
[HttpGet]
public async Task<ActionResult<List>> GetProducts([FromQuery] bool? isActive)
{
var query = new GetProductListQuery { IsActive = isActive };
var products = await _mediator.Send(query);
return Ok(products);
}
[HttpPost]
public async Task<ActionResult> CreateProduct(CreateProductCommand command)
{
var productId = await _mediator.Send(command);
return CreatedAtAction(nameof(GetProductById), new { id = productId }, productId);
}
// 其他端点...
}
}
控制器瘦身成功!它不再需要知道产品如何创建或查询,它只是一个“交通指挥员”,将请求路由到正确的处理器。这使得控制器的单元测试变得非常简单(只需mock `IMediator`),也便于API版本的维护。
第五步:进阶技巧与实战思考
经过一段时间的实践,我探索了MediatR更多强大的特性:
1. 管道行为(Pipeline Behaviors):这是MediatR的“中间件”,允许你在Handler执行前后插入逻辑,完美实现横切关注点(Cross-Cutting Concerns)。
// 例如,一个日志和行为验证的管道
public class LoggingBehavior : IPipelineBehavior
where TRequest : notnull
{
private readonly ILogger<LoggingBehavior> _logger;
public LoggingBehavior(ILogger<LoggingBehavior> logger)
{
_logger = logger;
}
public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken)
{
_logger.LogInformation($"处理请求 {typeof(TRequest).Name}");
var response = await next(); // 调用下一个管道行为或最终的Handler
_logger.LogInformation($"请求 {typeof(TRequest).Name} 处理完毕");
return response;
}
}
// 在Program.cs中注册:builder.Services.AddTransient(typeof(IPipelineBehavior), typeof(LoggingBehavior));
2. 领域事件(Domain Events):通过实现 `INotification` 接口和 `INotificationHandler`,可以在一个命令成功后,发布多个领域事件,实现松耦合的集成。例如,创建订单后,发布 `OrderCreatedEvent`,库存服务、邮件服务可以各自订阅处理,而不需要修改订单创建的核心逻辑。
踩坑与总结:
- 不要过度设计:对于非常简单的CRUD应用,引入CQRS和MediatR可能会增加不必要的复杂度。评估你的项目规模。
- 文件夹结构很重要:我推荐使用“垂直切片架构”(Vertical Slice Architecture)按功能组织代码(如本文示例的 `Features/Products/...`),这比传统的按技术层次(Controllers, Services, Models)划分更符合功能需求的变化。
- 性能考量:MediatR的反射和对象创建有微小开销,但在绝大多数Web应用中可忽略不计。它的清晰度带来的维护性提升远大于此。
回顾这段旅程,将MediatR引入ASP.NET Core项目,不仅仅是引入一个库,更是引入了一种更清晰、更可测试的代码组织哲学。它迫使你将业务逻辑从控制器中解放出来,放入一个个职责单一的Handler中。一开始可能会觉得多了一些“样板代码”,但当你需要修改某个特定功能、添加新功能或编写测试时,你会发现一切都是值得的。希望这篇实战指南能帮助你开启更优雅的.NET后端开发之旅。

评论(0)