我明白你的担心,我也发现自己处于这种情况。我想将我的验证器与处理程序分开,同时将它们保留在域/业务项目中。此外,我不想仅仅为了处理错误请求或任何其他自定义业务异常而抛出异常。
你有正确的想法
我的意思是必须为特定请求执行特定的验证器
为此,您需要设置一个调解器管道,以便为每个命令找到合适的验证器,验证并决定是执行命令还是返回失败的结果。
首先,创建一个ICommand 的接口(虽然不是必需的,但我就是这样做的),如下所示:
public interface ICommand<TResponse>: IRequest<TResponse>
{
}
还有,ICommandHandler 喜欢:
public interface ICommandHandler<in TCommand, TResponse>: IRequestHandler<TCommand, TResponse>
where TCommand : ICommand<TResponse>
{
}
这样我们只能对命令应用验证。而不是继承 IRequest<MyOutputDTO> 和 IRequestHandler<MyCommand, MyOutputDTO> 您从 ICommand 和 ICommandHandler 继承。
现在按照我们之前的约定为调解员创建一个ValidationBehaviour。
public class ValidationBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : class, ICommand<TResponse>
{
private readonly IEnumerable<IValidator<TRequest>> _validators;
public ValidationBehaviour(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 validationContext = new ValidationContext<TRequest>(request);
var errors = (await Task.WhenAll(_validators
.Select(async x => await x.ValidateAsync(validationContext))))
.SelectMany(x => x.Errors)
.Where(x => x != null)
.Select(x => x.CustomState)
.Cast<TResponse>();
//TResponse should be of type Result<T>
if (errors.Any())
return errors.First();
try
{
return await next();
}
catch(Exception e)
{
//most likely internal server error
//better retain error as an inner exception for debugging
//but also return that an error occurred
return Result<TResponse>.Failure(new InternalServerException(e));
}
}
}
这段代码很简单,除了构造函数中的所有验证器,因为您从程序集中注册了所有验证器,以便您的 DI 容器注入它们。
它等待所有验证来验证异步(因为我的验证主要需要调用 db 本身,例如获取用户角色等)。
然后检查错误并返回错误(这里我创建了一个 DTO 来包装我的错误和值以获得一致的结果)。
如果没有错误,只需让处理程序完成它的工作return await next();
现在您必须注册此管道行为和所有验证器。
我使用autofac所以我可以很容易地做到这一点
builder
.RegisterAssemblyTypes(_assemblies.ToArray())
.AsClosedTypesOf(typeof(IValidator<>))
.AsImplementedInterfaces();
var mediatrOpenTypes = new[]
{
typeof(IRequestHandler<,>),
typeof(IRequestExceptionHandler<,,>),
typeof(IRequestExceptionAction<,>),
typeof(INotificationHandler<>),
typeof(IPipelineBehavior<,>)
};
foreach (var mediatrOpenType in mediatrOpenTypes)
{
builder
.RegisterAssemblyTypes(_assemblies.ToArray())
.AsClosedTypesOf(mediatrOpenType)
.AsImplementedInterfaces();
}
如果您使用 Microsoft DI,您可以:
services.AddMediatR(typeof(Application.AssemblyReference).Assembly);
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
services.AddValidatorsFromAssembly(typeof(Application.AssemblyReference).Assembly); //to add validators
示例用法:
我的通用 DTO 包装器
public class Result<T>: IResult<T>
{
public Result(T? value, bool isSuccess, Exception? error)
{
IsSuccess = isSuccess;
Value = value;
Error = error;
}
public bool IsSuccess { get; set; }
public T? Value { get; set; }
public Exception? Error { get; set; }
public static Result<T> Success(T value) => new (value, true, null);
public static Result<T> Failure(Exception error) => new (default, false, error);
}
一个示例命令:
public record CreateNewRecordCommand(int UserId, string record) : ICommand<Result<bool>>;
验证器:
public class CreateNewRecordCommandValidator : AbstractValidator<CreateNewRecordCommand>
{
public CreateNewVoucherCommandValidator(DbContext _context, IMediator mediator) //will be injected by out DI container
{
RuleFor(x => x.record)
.NotEmpty()
.WithState(x => Result<bool>.Failure(new Exception("Empty record")));
//.WithName("record") if your validation a property in array or something and can't find appropriate property name
RuleFor(x => x.UserId)
.MustAsync(async(id, cToken) =>
{
//var roles = await mediator.send(new GetUserRolesQuery(id, cToken));
//var roles = (await context.Set<User>.FirstAsync(user => user.id == id)).roles
//return roles.Contains(MyRolesEnum.CanCreateRecordRole);
}
)
.WithState(x => Result<bool>.Failure(new MyCustomForbiddenRequestException(id)))
}
}
这样你总是得到一个结果对象,你可以检查是error is null还是!IsSuccess,然后在你的Controller基础中创建一个自定义的HandleResult(result)方法,它可以打开异常以返回BadReuqestObjectResult(result)或@987654342 @。
如果您更喜欢在管道中抛出、捕获和处理异常,或者您不想使用非异步实现,请阅读此https://code-maze.com/cqrs-mediatr-fluentvalidation/
这样,您的所有验证都与您的处理程序相距甚远,同时保持一致的结果。