Ultimate ASP.NET CORE 6.0 Web API --- 读书笔记(32)
32 Bonus 2 -Introduction To CQRS And MediatR With ASP.NET Core Web API
本文内容来自书籍: Marinko Spasojevic - Ultimate ASP.NET Core Web API - From Zero To Six-Figure Backend Developer (2nd edition)
需要本书和源码的小伙伴可以留下邮箱,有空看到会发送的
33.1.1 CQRS
CQRS:Command Query Responsibility Segregation
,是关于将命令和查询的职责划分为不同的模型
CQRS
模式对于如何切分没有正式的要求,它可以是在同一个应用中简单地切分,一直到在不同的服务器傻瓜物理地分离应用。而物理切分是基于扩展需求和基础设施的因素影响,这个这里不讨论这种形式。
那么对于创建一个CQRS
系统的关键是,只需要从writes
的部分中将reads
的部分分离出来
那么这种方案解决了什么问题?
一种常见的原因是,当我们设计系统的时候,是从数据存储的设计开始的。我们按照范式使得数据库表规范化,会添加主键和外键来强制保证业务数据的完整性,还会添加索引,并通常要保证write
的优化。
对于关系型数据库,这是一种常见的设置。而在其他时候,我们会首要考虑read
的使用,尝试将数据放到数据库,减少对重复数据或者其他关系型数据库的担忧
两种方法都没有错,但问题是,这是read
和write
之间一种持续性的平衡行为,而最终一方将胜出
。所有的更进一步的发展意味着读写双方都需要解析,而且往往其中一方会受到损害
CQRS
允许我们从这种考虑中挣脱出来,并给予两个体系公平的设计和思考,而不用考虑影响到其他系统。这对性能和敏捷性上有很大的好处,特别是有单独的团队在这些系统上工作的时候
CQRS
的好处:
- 单一责任:
Commands
和Queries
只有一种工作,要么更改应用状态,要么检索它,因此,它们非常容易被推测和理解 - 解耦:
Commands
和Queries
之间是完全解耦的,而在处理端可以按照你希望的方式去实现它 - 可伸缩性:在如何组织数据存储方面,
CQRS
模式非常灵活,为您提供了高可伸缩性的选项。你可以选择用同一个数据库做Commands
和Queries
,或者分开两个数据库来实现,以提高性能,两个数据库之间的同步可以通过信息传递或者复制的形式实现 - 可测试性:它们很容易被测试,因为它们的设计很简单,而且只执行一种作业方式
CQRS
的坏处:
- 复杂性:它是一种先进的设计模式,你需要花费时间去理解它。它引入了大量的复杂性,这些复杂性会在项目中产生摩擦和潜在的问题,在决定使用这种模式之前需要考虑清楚
- 学习曲线:虽然它看起来是一种简单直接的设计模式,但还是有学习曲线的。大多数开发人员习惯了过程式(命令式)的编码风格,但是
CQRS
与之相去甚远 - 很难Debug:因为
Commands
和Queries
和它们的处理程序是解耦的,所以在应用程序上不是自然的命令流,这导致它比传统的应用更加难Debug
33.1.3 Mediator Pattern
Mediator Pattern
只是简单地定义了一个对象,这个对象封装了对象之间的交互。对象之间不再直接地依赖对方,而是通过mediator
来交互,让mediator
将这些交互信息发送给对方
Mediator Pattern
就和IoC
一样有用,它变得松耦合,因为依赖图是最小的,所以代码变得很简单并且容易测试。换句话说,组件的考虑因素越少,开发和发展就越容易
33.2 How MediatR facilitates CQRS and Mediator Patterns
你可以认为MediatR
是一个进程中
的Mediator
实现,它可以帮我们构建CQRS
系统。所有用户接口和数据存储之间的通信都是通过MediatR
这里,in process
是一种重要的限制。因为它是一种.NET的库,用来管理同一个进程中的类内部的交互,而如果你想要用来分离Commands
和Queries
到两个系统,那么这个库是不合适的,更好的方法是使用消息中间件,比如:Kafka
或者Azure Service Bus
但是在本章节,我们只是实现一个单进程的CQRS
,所以使用MediatR
是没问题的
33.3 Adding Application Project and Initial Configuration
这里使用的示例是前面章节的项目,但是不需要Service
和Service.Contracts
,然后新建一个项目Application
,然后安装包MediatR
,并且创建一个空类AssemblyReference
接着是在主项目中安装包MediatR.Extensions.Microsoft.DependencyInjection
,用来注册服务到ASP.NET Core
配置服务
builder.Services.AddMediatR(typeof(Application.AssemblyReference).Assembly);
这个方法会将扫描项目程序集,将包含handlers
,就是那些处理业务逻辑的类,都加入容器
配置好了之后,我们就可以在Presentation
上使用了
[Route("api/companies")]
[ApiController]
public class CompaniesController : ControllerBase
{
private readonly ISender _sender;
public CompaniesController(ISender sender) => _sender = sender;
}
在控制器上,我们注入了一个ISender
,我们就是用这个发送请求到我们的handlers
33.4 Requests With MediatR
MediatR
请求是简单的请求-响应类型的消息,其中单个请求由单个处理程序同步处理
有两种请求类型,一种是有返回值,一种是没有的
接下来在Application
中创建三个文件夹,代表三种组件queries, commands, 和 handlers
首先是queries
// 代表会返回company的列表
public sealed record GetCompaniesQuery(bool TrackChanges) : IRequest<IEnumerable<CompanyDto>>;
handlers
,其实就是类似前面章节所建立的Service
,也是使用Repository
来访问数据库
// 方法还缺少一个AutoMapper的库做自动转换
internal sealed class GetCompaniesHandler : IRequestHandler<GetCompaniesQuery,
IEnumerable<CompanyDto>>
{
private readonly IRepositoryManager _repository;
public GetCompaniesHandler(IRepositoryManager repository) => _repository =
repository;
public async Task<IEnumerable<CompanyDto>> Handle(GetCompaniesQuery request,
CancellationToken cancellationToken)
{
var companies = await
_repository.Company.GetAllCompaniesAsync(request.TrackChanges);
var companiesDto = _mapper.Map<IEnumerable<CompanyDto>>(companies);
return companiesDto;
}
}
Commands
,上面是查询,这里就是修改数据状态
public sealed record CreateCompanyCommand(CompanyForCreationDto Company) :
IRequest<CompanyDto>;
然后是handlers
internal sealed class CreateCompanyHandler : IRequestHandler<CreateCompanyCommand,
CompanyDto>
{
private readonly IRepositoryManager _repository;
private readonly IMapper _mapper;
public CreateCompanyHandler(IRepositoryManager repository, IMapper mapper)
{
_repository = repository;
_mapper = mapper;
}
public async Task<CompanyDto> Handle(CreateCompanyCommand request,
CancellationToken cancellationToken)
{
var companyEntity = _mapper.Map<Company>(request.Company);
_repository.Company.CreateCompany(companyEntity);
await _repository.SaveAsync();
var companyToReturn = _mapper.Map<CompanyDto>(companyEntity);
return companyToReturn;
}
}
这里最重要的事情是,我们和一个数据存储的交流是通过一个简单的信息结构,而我们并不清楚它的实现是什么。然后Commands
和Queries
可以指向的是不同的数据存储方式,它们并不知道它们的请求是怎么样被处理的它们也不关心这个。
33.6 MedidatR Notifications
前面了解了一个请求对应一个处理函数,那么如果需要一个请求对应多个处理程序呢?在这样的场景下,我们可以使用Notifications
的功能
// 信息发送类
public sealed record CompanyDeletedNotification(Guid Id, bool TrackChanges) : INotification;
// 删除发送邮件处理
internal sealed class EmailHandler : INotificationHandler<CompanyDeletedNotification>
{
private readonly ILoggerManager _logger;
public EmailHandler(ILoggerManager logger) => _logger = logger;
public async Task Handle(CompanyDeletedNotification notification,
CancellationToken cancellationToken)
{
_logger.LogWarn($"Delete action for the company with id: {notification.Id} has occurred.");
await Task.CompletedTask;
}
}
// 删除处理
internal sealed class DeleteCompanyHandler : INotificationHandler<CompanyDeletedNotification>
{
private readonly IRepositoryManager _repository;
public DeleteCompanyHandler(IRepositoryManager repository) =>
_repository = repository;
public async Task Handle(CompanyDeletedNotification notification, CancellationToken cancellationToken)
{
var company = await _repository.Company.GetCompanyAsync(notification.Id, notification.TrackChanges);
if (company is null)
throw new CompanyNotFoundException(notification.Id);
_repository.Company.DeleteCompany(company);
await _repository.SaveAsync();
}
}
// controller 不再是使用sender发送,而是使用publisher
private readonly ISender _sender;
private readonly IPublisher _publisher;
public CompaniesController(ISender sender, IPublisher publisher)
{
_sender = sender;
_publisher = publisher;
}
33.7 MediatR Behaviors
在构建应用的时候,已经会碰到一些交叉性的问题,比如授权、验证和日志
为了在handlers
替换不断重复的一些逻辑,我们可以使用Behaviors
,它类似ASP.NET Core的中间件,它也是接收一个请求,然后做一些动作,然后传递这个请求到下一个位置
下面就根据参数验证,在Commands
中添加一些验证逻辑,这里需要用到包
FluentValidation.AspNetCore
然后是注册服务
builder.Services.AddValidatorsFromAssembly(typeof(ApplicationAssemblyReference).Assembly);
接着是创建验证相关信息
public sealed class CreateCompanyCommandValidator : AbstractValidator<CreateCompanyCommand>
{
public CreateCompanyCommandValidator()
{
RuleFor(c => c.Company.Name).NotEmpty().MaximumLength(60);
RuleFor(c => c.Company.Address).NotEmpty().MaximumLength(60);
}
}
与ASP.NET Core 的中间件pipeline很相似,这里也有一个IPipelineBehavior
public interface IPipelineBehavior<in TRequest, TResponse> where TRequest : notnull
{
Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next);
}
public sealed class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly IEnumerable<IValidator<TRequest>> _validators;
public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators) => _validators = validators;
public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
{
if (!_validators.Any()) return await next();
var context = new ValidationContext<TRequest>(request);
var errorsDictionary = _validators
.Select(x => x.Validate(context))
.SelectMany(x => x.Errors)
.Where(x => x != null)
.GroupBy(
x => x.PropertyName.Substring(x.PropertyName.IndexOf('.') + 1),
x => x.ErrorMessage,(propertyName, errorMessages) => new
{
Key = propertyName,
Values = errorMessages.Distinct().ToArray()
})
.ToDictionary(x => x.Key, x => x.Values);
if (errorsDictionary.Any())
throw new ValidationAppException(errorsDictionary);
return await next();
}
}
将这个Behaviors
注册到ASP.NET Core中,就可以了
builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?