系列导航及源代码
- 使用.NET 6开发TodoList应用文章索引
需求
在响应请求处理的过程中,我们经常需要对请求参数的合法性进行校验,如果参数不合法,将不继续进行业务逻辑的处理。我们当然可以将每个接口的参数校验逻辑写到对应的Handle方法中,但是更好的做法是借助MediatR提供的特性,将这部分与实际业务逻辑无关的代码整理到单独的地方进行管理。
为了实现这个需求,我们需要结合FluentValidation和
MediatR
提供的特性。
目标
将请求的参数校验逻辑从CQRS的Handler中分离到MediatR的Pipeline框架中处理。
原理与思路
MediatR不仅提供了用于实现CQRS的框架,还提供了
IPipelineBehavior<TRequest, TResult>
接口用于实现CQRS响应之前进行一系列的与实际业务逻辑不紧密相关的特性,诸如请求日志、参数校验、异常处理、授权、性能监控等等功能。
在本文中我们将结合
FluentValidation
和
IPipelineBehavior<TRequest, TResult>
实现对请求参数的校验功能。
实现
添加MediatR参数校验Pipeline Behavior框架支持
首先向
Application
项目中引入
FluentValidation.DependencyInjectionExtensions
Nuget包。为了抽象所有的校验异常,先创建
ValidationException
类:
-
ValidationException.cs
namespace TodoList.Application.Common.Exceptions;public class ValidationException : Exception{public ValidationException() : base("One or more validation failures have occurred."){}public ValidationException(string failures): base(failures){}}
参数校验的基础框架我们创建到
Application/Common/Behaviors/
中:
-
ValidationBehaviour.cs
using FluentValidation;using FluentValidation.Results;using MediatR;using ValidationException = TodoList.Application.Common.Exceptions.ValidationException;namespace TodoList.Application.Common.Behaviors;public class ValidationBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>where TRequest : notnull{private readonly IEnumerable<IValidator<TRequest>> _validators;// 注入所有自定义的Validatorspublic ValidationBehaviour(IEnumerable<IValidator<TRequest>> validators)=> _validators = validators;public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next){if (_validators.Any()){var context = new ValidationContext<TRequest>(request);var validationResults = await Task.WhenAll(_validators.Select(v => v.ValidateAsync(context, cancellationToken)));var failures = validationResults.Where(r => r.Errors.Any()).SelectMany(r => r.Errors).ToList();// 如果有validator校验失败,抛出异常,这里的异常是我们自定义的包装类型if (failures.Any())throw new ValidationException(GetValidationErrorMessage(failures));}return await next();}// 格式化校验失败消息private string GetValidationErrorMessage(IEnumerable<ValidationFailure> failures){var failureDict = failures.GroupBy(e => e.PropertyName, e => e.ErrorMessage).ToDictionary(failureGroup => failureGroup.Key, failureGroup => failureGroup.ToArray());return string.Join(";", failureDict.Select(kv => kv.Key + ": " + string.Join(\' \', kv.Value.ToArray())));}}
在
DependencyInjection
中进行依赖注入:
-
DependencyInjection.cs
// 省略其他...services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehaviour<,>)
添加Validation Pipeline Behavior
接下来我们以添加
TodoItem
接口为例,在
Application/TodoItems/CreateTodoItem/
中创建
CreateTodoItemCommandValidator
:
-
CreateTodoItemCommandValidator.cs
using FluentValidation;using Microsoft.EntityFrameworkCore;using TodoList.Application.Common.Interfaces;using TodoList.Domain.Entities;namespace TodoList.Application.TodoItems.Commands.CreateTodoItem;public class CreateTodoItemCommandValidator : AbstractValidator<CreateTodoItemCommand>{private readonly IRepository<TodoItem> _repository;public CreateTodoItemCommandValidator(IRepository<TodoItem> repository){_repository = repository;// 我们把最大长度限制到10,以便更好地验证这个校验// 更多的用法请参考FluentValidation官方文档RuleFor(v => v.Title).MaximumLength(10).WithMessage("TodoItem title must not exceed 10 characters.").WithSeverity(Severity.Warning).NotEmpty().WithMessage("Title is required.").WithSeverity(Severity.Error).MustAsync(BeUniqueTitle).WithMessage("The specified title already exists.").WithSeverity(Severity.Warning);}public async Task<bool> BeUniqueTitle(string title, CancellationToken cancellationToken){return await _repository.GetAsQueryable().AllAsync(l => l.Title != title, cancellationToken);}}
其他接口的参数校验添加方法与此类似,不再继续演示。
验证
启动
Api
项目,我们用一个校验会失败的请求去创建TodoItem:
-
请求
-
响应
因为之前测试的时候已经在没有加校验的时候用同样的请求生成了一个
TodoItem
,所以校验失败的消息里有两项校验都没有满足。
一点扩展
我们在前文中说了使用MediatR的
PipelineBehavior
可以实现在CQRS请求前执行一些逻辑,其中就包含了日志记录,这里就把实现方式也放在下面,在这里我们使用的是Pipeline里的
IRequestPreProcessor<TRequest>
接口实现,因为只关心请求处理前的信息,如果关心请求处理返回后的信息,那么和前文一样,需要实现
IPipelineBehavior<TRequest, TResponse>
接口并在
Handle
中返回response对象:
// 省略其他...var response = await next();//Response_logger.LogInformation($"Handled {typeof(TResponse).Name}");return response;
创建一个
LoggingBehavior
:
using System.Reflection;using MediatR.Pipeline;using Microsoft.Extensions.Logging;public class LoggingBehaviour<TRequest> : IRequestPreProcessor<TRequest> where TRequest : notnull{private readonly ILogger<LoggingBehaviour<TRequest>> _logger;// 在构造函数中后面我们还可以注入类似ICurrentUser和IIdentity相关的对象进行日志输出public LoggingBehaviour(ILogger<LoggingBehaviour<TRequest>> logger){_logger = logger;}public async Task Process(TRequest request, CancellationToken cancellationToken){// 你可以在这里log关于请求的任何信息_logger.LogInformation($"Handling {typeof(TRequest).Name}");IList<PropertyInfo> props = new List<PropertyInfo>(request.GetType().GetProperties());foreach (var prop in props){var propValue = prop.GetValue(request, null);_logger.LogInformation("{Property} : {@Value}", prop.Name, propValue);}}}
如果是实现
IPipelineBehavior<TRequest, TResponse>
接口,最后注入即可。
// 省略其他...services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehaviour<,>));
如果实现
IRequestPreProcessor<TRequest>
接口,则不需要再进行注入。
效果如下图所示:
可以看到日志中已经输出了Command名称和请求参数字段值。
总结
在本文中我们通过
FluentValidation
和
MediatR
实现了不侵入业务代码的请求参数校验逻辑,在下一篇文章中我们将介绍.NET开发中会经常用到的
ActionFilters
。
参考资料
- FluentValidation
- How to use MediatR Pipeline Behaviours