找回密码
 立即注册
首页 业界区 业界 使用MediatR和FluentValidation实现CQRS应用程序的数据 ...

使用MediatR和FluentValidation实现CQRS应用程序的数据验证

林鱼 昨天 22:47
本文将重点介绍如何通过MediatR的管道功能将FluentValidation集成到项目中实现验证功能。
什么是CQRS?

CQRS(Command Query Responsibility Segregation)也叫命令查询职责分离,是近年来非常流行的应用程序架构模式。CQRS 背后的理念是在逻辑上将应用程序的流程分成两个独立的流程,即命令或查询。
命令用于改变应用程序的状态。对应CRUD的创建、更新和删除部分。查询用于检索应用程序中的信息,对应CRUD的读取部分。
CQRS 的优缺点

优点:

  • 单一职责 – 命令和查询只有一个职责。要么更改应用程序的状态,要么检索它。因此它们很容易推理和理解。
  • 解耦 – 命令或查询与其处理程序完全解耦,因此在处理程序方面有很大的灵活性,可以按照自己认为最合适的方式来实现。
  • 可扩展性 – CQRS 模式在如何组织数据存储方面非常灵活,为您提供了多种可扩展性选择。您可以将一个数据库用于命令和
    查询。您可以使用独立的读/写数据库来提高性能,并在数据库之间使用消息传递或复制来实现同步。
  • 可测试性 – 测试命令或查询处理程序非常简单,因为它们的设计非常简单,只执行一项任务。
缺点:

  • 复杂性 – CQRS 是一种高级设计模式,您需要花时间才能完全理解它。它引入了很多复杂性,会给项目带来摩擦和潜在问题。在决定在项目中使用之前,请务必考虑清楚。
  • 学习曲线 – 虽然 CQRS 看起来是一种简单明了的设计模式,但仍存在学习曲线。大多数开发人员习惯于用过程式(命令式)风格编写代码,而 CQRS 则与之大相径庭。
  • 难以调试 – 由于命令和查询与其处理程序是分离的,因此应用程序没有自然的命令式流程。这使得它比传统应用程序更难调试。
使用 MediatR 的命令和查询

MediatR 使用接口(interface)来表示命令和查询。在我们的项目中,我们将为命令和查询创建单独的抽象。
首先,让我们看看接口是如何定义的:
  1. using MediatR;
  2. namespace Application.Abstractions.Messaging
  3. {
  4.     public interface ICommand<out TResponse> : IRequest<TResponse>
  5.     {
  6.     }
  7. }
复制代码
  1. using MediatR;
  2. namespace Application.Abstractions.Messaging
  3. {
  4.     public interface IQuery<out TResponse> : IRequest<TResponse>
  5.     {
  6.     }
  7. }
复制代码
我们在声明TResponse泛型时使用了 out 关键字,这表示它是协变的。这样,我们就可以使用比泛型参数指定的类型更多的派生类型。要了解有关协变和逆变的更多信息,请查看微软文档。
此外,为了完整起见,我们需要对命令和查询处理程序进行单独的抽象。
  1. using MediatR;
  2. namespace Application.Abstractions.Messaging
  3. {
  4.     public interface ICommandHandler<in TCommand, TResponse> : IRequestHandler<TCommand, TResponse>
  5.         where TCommand : ICommand<TResponse>
  6.     {
  7.     }
  8. }
复制代码
  1. using MediatR;
  2. namespace Application.Abstractions.Messaging
  3. {
  4.     public interface IQueryHandler<in TQuery, TResponse> : IRequestHandler<TQuery, TResponse>
  5.         where TQuery : IQuery<TResponse>
  6.     {
  7.     }
  8. }
复制代码
这里留下一个小问题,MediatR已经提供了IRequest和IRequest两个接口,那我们为什么还要再次定义IQuery和ICommand呢?
使用FluentValidation进行验证

FluentValidation 库允许我们轻松地为我们的类定义非常丰富的自定义验证。由于我们正在实现 CQRS,所以这里我们仅讨论对Command进行验证。由于Query对象仅仅是从应用程序获取数据,意思我们不必多此一举为Query设计验证器。
我们先设计一个UpdateUserCommand
  1. public sealed record UpdateUserCommand(int UserId, string FirstName, string LastName) : ICommand<Unit>;
复制代码
Unit是MediatR定义的一个特殊类,表示请求不返回数据,相当于void或Task。
这个命令将用于更新已有用户(通过UserId查找)的FirstName和LastName,关于MediatR如何新增、查询和修改数据,在之前的文章中我们已经介绍过了,这里不再赘述。
接下来我们需要为UpdateUserCommand定义一个验证器:
  1. public sealed class UpdateUserCommandValidator : AbstractValidator<UpdateUserCommand>
  2. {
  3.     public UpdateUserCommandValidator()
  4.     {
  5.         RuleFor(x => x.UserId).NotEmpty();
  6.         RuleFor(x => x.FirstName).NotEmpty().MaximumLength(100);
  7.         RuleFor(x => x.LastName).NotEmpty().MaximumLength(100);
  8.     }
  9. }
复制代码
此验证器将对UpdateUserCommand的属性进行以下验证:

  • UserId - 不可空
  • FirstName - 不可空且最大长度不超过100个字符
  • LastName - 不可空且最大长度不超过100个字符
使用 MediatR PipelineBehavior创建装饰器

CQRS 模式使用命令和查询来传达信息并接收响应。实质上是请求-响应管道。这使我们能够轻松地围绕通过管道的每个请求引入其他行为,而无需实际修改原始请求。
您可能熟悉这种名为装饰器模式的技术。使用装饰器模式的典型例子就是ASP.NET Core中间件。MediatR与中间件的概念类似,称为:IPipelineBehavior
  1. public interface IPipelineBehavior<in TRequest, TResponse> where TRequest : notnull
  2. {
  3.     Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next);
  4. }
复制代码
PipelineBehavior是请求实例的包装器,在如何实现它方面为您提供了很大的灵活性。PipelineBehavior非常适合应用程序中的横切关注点。横切关注点的很好的例子是日志记录、缓存,当然还有验证!
创建验证PipelineBehavior

为了在 CQRS 管道中实现验证,我们将使用刚才谈到的概念,即 MediatR 的 IPipelineBehavior 和 FluentValidation。
首先我们创建一个ValidationBehavior
  1. public sealed class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
  2.     where TRequest : class, ICommand<TResponse>
  3. {
  4.     private readonly IEnumerable<IValidator<TRequest>> _validators;
  5.    
  6.     public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators) => _validators = validators;
  7.    
  8.     public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
  9.     {
  10.         if (!_validators.Any())
  11.         {
  12.             return await next();
  13.         }
  14.         var context = new ValidationContext<TRequest>(request);
  15.         var errorsDictionary = _validators
  16.             .Select(x => x.Validate(context))
  17.             .SelectMany(x => x.Errors)
  18.             .Where(x => x != null)
  19.             .GroupBy(
  20.                 x => x.PropertyName,
  21.                 x => x.ErrorMessage,
  22.                 (propertyName, errorMessages) => new
  23.                 {
  24.                     Key = propertyName,
  25.                     Values = errorMessages.Distinct().ToArray()
  26.                 })
  27.             .ToDictionary(x => x.Key, x => x.Values);
  28.         if (errorsDictionary.Any())
  29.         {
  30.             throw new ValidationException(errorsDictionary);
  31.         }
  32.         return await next();
  33.     }
  34. }
复制代码
处理验证异常

为了处理遇到验证错误时抛出的ValidationException,我们可以使用 ASP.NET Core的 IMiddleware接口。
  1. internal sealed class ExceptionHandlingMiddleware : IMiddleware
  2. {
  3.     private readonly ILogger<ExceptionHandlingMiddleware> _logger;
  4.    
  5.     public ExceptionHandlingMiddleware(ILogger<ExceptionHandlingMiddleware> logger) => _logger = logger;
  6.    
  7.     public async Task InvokeAsync(HttpContext context, RequestDelegate next)
  8.     {
  9.         try
  10.         {
  11.             await next(context);
  12.         }
  13.         catch (Exception e)
  14.         {
  15.             _logger.LogError(e, e.Message);
  16.             await HandleExceptionAsync(context, e);
  17.         }
  18.     }
  19.    
  20.     private static async Task HandleExceptionAsync(HttpContext httpContext, Exception exception)
  21.     {
  22.         var statusCode = GetStatusCode(exception);
  23.         var response = new
  24.         {
  25.             title = GetTitle(exception),
  26.             status = statusCode,
  27.             detail = exception.Message,
  28.             errors = GetErrors(exception)
  29.         };
  30.         httpContext.Response.ContentType = "application/json";
  31.         httpContext.Response.StatusCode = statusCode;
  32.         await httpContext.Response.WriteAsync(JsonSerializer.Serialize(response));
  33.     }
  34.     private static int GetStatusCode(Exception exception) =>
  35.         exception switch
  36.         {
  37.             BadRequestException => StatusCodes.Status400BadRequest,
  38.             NotFoundException => StatusCodes.Status404NotFound,
  39.             ValidationException => StatusCodes.Status422UnprocessableEnttity,
  40.             _ => StatusCodes.Status500InternalServerError
  41.         };
  42.     private static string GetTitle(Exception exception) =>
  43.         exception switch
  44.         {
  45.             ApplicationException applicationException => applicationException.Title,
  46.             _ => "Server Error"
  47.         };
  48.     private static IReadOnlyDictionary<string, string[]> GetErrors(Exception exception)
  49.     {
  50.         IReadOnlyDictionary<string, string[]> errors = null;
  51.         if (exception is ValidationException validationException)
  52.         {
  53.             errors = validationException.ErrorsDictionary;
  54.         }
  55.         return errors;
  56.     }
  57. }
复制代码
设置依赖注入

在运行应用程序之前,我们需要确保已向 DI 容器注册了所有服务。MediatR的DI注入方式之前已经介绍过,这里主要演示FluentValidation的注入。由于ValidationBehavior依赖IValidator,因此需要注入我们定义的Validator。
  1. // 在Startup.cs中配置
  2. services.AddValidatorsFromAssembly(typeof(Application.AssemblyReference).Assembly);
  3. // 在Program.cs中配置(≥ net 6.0)
  4. builder.Services.AddValidatorsFromAssembly(typeof(Application.AssemblyReference).Assembly);
复制代码
最后我们需要将ExceptionHandlingMiddleware也注册到DI容器和ASP.NET Core的管道中:
  1. // 在Startup.cs中配置
  2. services.AddTransient<ExceptionHandlingMiddleware>();
  3. // 在Program.cs中配置(≥ net 6.0)
  4. builder.Services.AddTransient<ExceptionHandlingMiddleware>();
  5. app.UseMiddleware<ExceptionHandlingMiddleware>();
复制代码
测试验证管道

在项目的Controllers文件夹中找到UserController:
  1. /// <summary>
  2. /// The users controller.
  3. /// </summary>
  4. [ApiController]
  5. [Route("api/[controller]")]
  6. public sealed class UsersController : ControllerBase
  7. {
  8.     private readonly ISender _sender;
  9.    
  10.     /// <summary>
  11.     /// Initializes a new instance of the <see cref="UsersController"/> class.
  12.     /// </summary>
  13.     /// <param name="sender"></param>
  14.     public UsersController(ISender sender) => _sender = sender;
  15.    
  16.     /// <summary>
  17.     /// Updates the user with the specified identifier based on the specified request, if it exists.
  18.     /// </summary>
  19.     /// <param name="userId">The user identifier.</param>
  20.     /// <param name="request">The update user request.</param>
  21.     /// <param name="cancellationToken">The cancellation token.</param>
  22.     /// <returns>No content.</returns>
  23.     [HttpPut("{userId:int}")]
  24.     public async Task<IActionResult> UpdateUser(int userId, [FromBody] UpdateUserRequest request, CancellationToken cancellationToken)
  25.     {
  26.         var command = request.Adapt<UpdateUserCommand>() with
  27.         {
  28.             UserId = userId
  29.         };
  30.         await _sender.Send(command, cancellationToken);
  31.         return NoContent();
  32.     }
  33. }
复制代码
我们可以看到,UpdateUser 操作非常简单,它从路由中获取用户Id,从请求正文中获取FirstName和LastName,然后创建一个新的 UpdateUserCommand实例并且通过管道发送命令。最后返回204(请求成功但无响应内容)状态码。
接下来我们通过Swagger调用API接口:
1.png

可以看到,请求的FirstName和LastName都是空白字符串。
2.png

补充内容之后再次发送请求。
3.png

结论

在本文中,我们介绍了CQRS 模式的一些更高级的概念,以及如何在应用程序中通过横切的方式实现数据验证,同时也简单的介绍了如何通过ASP.NTE Core的中间件实现全局异常处理。
点关注,不迷路。
如果您喜欢这篇文章,请不要忘记点赞、关注、转发,谢谢!如果您有任何高见,欢迎在评论区留言讨论……
4.png


来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册