使用MediatR在ASP.NET Core中实现CQRS架构模式的实践插图

使用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后端开发之旅。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。