本文将重点介绍如何通过MediatR的管道功能将FluentValidation集成到项目中实现验证功能。
CQRS(Command Query Responsibility Segregation)也叫命令查询职责分离,是近年来非常流行的应用程序架构模式。CQRS 背后的理念是在逻辑上将应用程序的流程分成两个独立的流程,即命令或查询。
命令用于改变应用程序的状态。对应CRUD的创建、更新和删除部分。查询用于检索应用程序中的信息,对应CRUD的读取部分。
优点:
缺点:
MediatR 使用接口(interface)来表示命令和查询。在我们的项目中,我们将为命令和查询创建单独的抽象。
首先,让我们看看接口是如何定义的:
using MediatR; namespace Application.Abstractions.Messaging { public interface ICommand<out TResponse> : IRequest<TResponse> { } }
using MediatR; namespace Application.Abstractions.Messaging { public interface IQuery<out TResponse> : IRequest<TResponse> { } }
我们在声明TResponse
泛型时使用了 out 关键字,这表示它是协变的。这样,我们就可以使用比泛型参数指定的类型更多的派生类型。要了解有关协变和逆变的更多信息,请查看微软文档。
此外,为了完整起见,我们需要对命令和查询处理程序进行单独的抽象。
using MediatR; namespace Application.Abstractions.Messaging { public interface ICommandHandler<in TCommand, TResponse> : IRequestHandler<TCommand, TResponse> where TCommand : ICommand<TResponse> { } }
using MediatR; namespace Application.Abstractions.Messaging { public interface IQueryHandler<in TQuery, TResponse> : IRequestHandler<TQuery, TResponse> where TQuery : IQuery<TResponse> { } }
这里留下一个小问题,MediatR已经提供了
IRequest
和IRequest<TResponse>
两个接口,那我们为什么还要再次定义IQuery<out TResponse>
和ICommand<out TResponse>
呢?
FluentValidation 库允许我们轻松地为我们的类定义非常丰富的自定义验证。由于我们正在实现 CQRS,所以这里我们仅讨论对Command进行验证。由于Query对象仅仅是从应用程序获取数据,意思我们不必多此一举为Query设计验证器。
我们先设计一个UpdateUserCommand
public sealed record UpdateUserCommand(int UserId, string FirstName, string LastName) : ICommand<Unit>;
Unit
是MediatR定义的一个特殊类,表示请求不返回数据,相当于void
或Task
。
这个命令将用于更新已有用户(通过UserId查找)的FirstName和LastName,关于MediatR如何新增、查询和修改数据,在之前的文章中我们已经介绍过了,这里不再赘述。
接下来我们需要为UpdateUserCommand
定义一个验证器:
public sealed class UpdateUserCommandValidator : AbstractValidator<UpdateUserCommand> { public UpdateUserCommandValidator() { RuleFor(x => x.UserId).NotEmpty(); RuleFor(x => x.FirstName).NotEmpty().MaximumLength(100); RuleFor(x => x.LastName).NotEmpty().MaximumLength(100); } }
此验证器将对UpdateUserCommand
的属性进行以下验证:
CQRS 模式使用命令和查询来传达信息并接收响应。实质上是请求-响应管道。这使我们能够轻松地围绕通过管道的每个请求引入其他行为,而无需实际修改原始请求。
您可能熟悉这种名为装饰器模式的技术。使用装饰器模式的典型例子就是ASP.NET Core中间件。MediatR与中间件的概念类似,称为:IPipelineBehavior
public interface IPipelineBehavior<in TRequest, TResponse> where TRequest : notnull { Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next); }
PipelineBehavior是请求实例的包装器,在如何实现它方面为您提供了很大的灵活性。PipelineBehavior非常适合应用程序中的横切关注点。横切关注点的很好的例子是日志记录、缓存,当然还有验证!
为了在 CQRS 管道中实现验证,我们将使用刚才谈到的概念,即 MediatR 的 IPipelineBehavior 和 FluentValidation。
首先我们创建一个ValidationBehavior
public sealed class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : class, ICommand<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, x => x.ErrorMessage, (propertyName, errorMessages) => new { Key = propertyName, Values = errorMessages.Distinct().ToArray() }) .ToDictionary(x => x.Key, x => x.Values); if (errorsDictionary.Any()) { throw new ValidationException(errorsDictionary); } return await next(); } }
为了处理遇到验证错误时抛出的ValidationException
,我们可以使用 ASP.NET Core的 IMiddleware
接口。
internal sealed class ExceptionHandlingMiddleware : IMiddleware { private readonly ILogger<ExceptionHandlingMiddleware> _logger; public ExceptionHandlingMiddleware(ILogger<ExceptionHandlingMiddleware> logger) => _logger = logger; public async Task InvokeAsync(HttpContext context, RequestDelegate next) { try { await next(context); } catch (Exception e) { _logger.LogError(e, e.Message); await HandleExceptionAsync(context, e); } } private static async Task HandleExceptionAsync(HttpContext httpContext, Exception exception) { var statusCode = GetStatusCode(exception); var response = new { title = GetTitle(exception), status = statusCode, detail = exception.Message, errors = GetErrors(exception) }; httpContext.Response.ContentType = "application/json"; httpContext.Response.StatusCode = statusCode; await httpContext.Response.WriteAsync(JsonSerializer.Serialize(response)); } private static int GetStatusCode(Exception exception) => exception switch { BadRequestException => StatusCodes.Status400BadRequest, NotFoundException => StatusCodes.Status404NotFound, ValidationException => StatusCodes.Status422UnprocessableEnttity, _ => StatusCodes.Status500InternalServerError }; private static string GetTitle(Exception exception) => exception switch { ApplicationException applicationException => applicationException.Title, _ => "Server Error" }; private static IReadOnlyDictionary<string, string[]> GetErrors(Exception exception) { IReadOnlyDictionary<string, string[]> errors = null; if (exception is ValidationException validationException) { errors = validationException.ErrorsDictionary; } return errors; } }
在运行应用程序之前,我们需要确保已向 DI 容器注册了所有服务。MediatR的DI注入方式之前已经介绍过,这里主要演示FluentValidation的注入。由于ValidationBehavior
依赖IValidator<T>
,因此需要注入我们定义的Validator。
// 在Startup.cs中配置 services.AddValidatorsFromAssembly(typeof(Application.AssemblyReference).Assembly); // 在Program.cs中配置(≥ net 6.0) builder.Services.AddValidatorsFromAssembly(typeof(Application.AssemblyReference).Assembly);
最后我们需要将ExceptionHandlingMiddleware
也注册到DI容器和ASP.NET Core的管道中:
// 在Startup.cs中配置 services.AddTransient<ExceptionHandlingMiddleware>(); // 在Program.cs中配置(≥ net 6.0) builder.Services.AddTransient<ExceptionHandlingMiddleware>(); app.UseMiddleware<ExceptionHandlingMiddleware>();
在项目的Controllers文件夹中找到UserController:
/// <summary> /// The users controller. /// </summary> [ApiController] [Route("api/[controller]")] public sealed class UsersController : ControllerBase { private readonly ISender _sender; /// <summary> /// Initializes a new instance of the <see cref="UsersController"/> class. /// </summary> /// <param name="sender"></param> public UsersController(ISender sender) => _sender = sender; /// <summary> /// Updates the user with the specified identifier based on the specified request, if it exists. /// </summary> /// <param name="userId">The user identifier.</param> /// <param name="request">The update user request.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>No content.</returns> [HttpPut("{userId:int}")] public async Task<IActionResult> UpdateUser(int userId, [FromBody] UpdateUserRequest request, CancellationToken cancellationToken) { var command = request.Adapt<UpdateUserCommand>() with { UserId = userId }; await _sender.Send(command, cancellationToken); return NoContent(); } }
我们可以看到,UpdateUser 操作非常简单,它从路由中获取用户Id,从请求正文中获取FirstName和LastName,然后创建一个新的 UpdateUserCommand实例并且通过管道发送命令。最后返回204(请求成功但无响应内容)状态码。
接下来我们通过Swagger调用API接口:
可以看到,请求的FirstName和LastName都是空白字符串。
补充内容之后再次发送请求。
在本文中,我们介绍了CQRS 模式的一些更高级的概念,以及如何在应用程序中通过横切的方式实现数据验证,同时也简单的介绍了如何通过ASP.NTE Core的中间件实现全局异常处理。
点关注,不迷路。
如果您喜欢这篇文章,请不要忘记点赞、关注、转发,谢谢!如果您有任何高见,欢迎在评论区留言讨论……