【问题标题】:Fluent validation divide business validation from auth validatonFluent 验证将业务验证与 auth 验证分开
【发布时间】:2022-07-13 21:32:59
【问题描述】:

我正在使用 ASP、CQRS + MediatR 和流利的验证。我想实现用户角色验证,但我不想将它与业务逻辑验证混为一谈。你知道如何实现这个吗? 我的意思是必须为特定请求执行特定的验证器。 有人告诉我解决方案在于IEnumerable< IValidator>

{
    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())
        {
            var context = new ValidationContext<TRequest>(request);
            var validationResults = await Task.WhenAll(_validators.Select(v => v.ValidateAsync(context, cancellationToken)));
            var failures = validationResults.SelectMany(r => r.Errors).Where(f => f != null).ToArray();

            if (failures.Any())
            {
                var errors = failures
                    .Select(x => new Error(x.ErrorMessage, x.ErrorCode))
                    .ToArray();
                throw new ValidationException(errors);
            }
        }

        return await next();
    }
}

【问题讨论】:

  • 您好,欢迎来到 Stack Overflow!查看您的代码,它看起来不错。您是否愿意添加更多细节以进一步说明您遇到的问题,因为目前图片似乎过于笼统。随意编辑您的问题并添加更多带有角色、具体验证器等的代码示例。如果您已经编写、编译和工作代码,您可能需要考虑在code review 上提出这个问题,让其他代码爱好者好好看看并留下有用的评论。

标签: c# asp.net cqrs fluentvalidation mediatr


【解决方案1】:

我明白你的担心,我也发现自己处于这种情况。我想将我的验证器与处理程序分开,同时将它们保留在域/业务项目中。此外,我不想仅仅为了处理错误请求或任何其他自定义业务异常而抛出异常。 你有正确的想法

我的意思是必须为特定请求执行特定的验证器

为此,您需要设置一个调解器管道,以便为每个命令找到合适的验证器,验证并决定是执行命令还是返回失败的结果。

首先,创建一个ICommand 的接口(虽然不是必需的,但我就是这样做的),如下所示:

public interface ICommand<TResponse>: IRequest<TResponse>
{

}

还有,ICommandHandler 喜欢:

public interface ICommandHandler<in TCommand, TResponse>: IRequestHandler<TCommand, TResponse>
        where TCommand : ICommand<TResponse>
{

}

这样我们只能对命令应用验证。而不是继承 IRequest&lt;MyOutputDTO&gt;IRequestHandler&lt;MyCommand, MyOutputDTO&gt; 您从 ICommandICommandHandler 继承。

现在按照我们之前的约定为调解员创建一个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/ 这样,您的所有验证都与您的处理程序相距甚远,同时保持一致的结果。

【讨论】:

    【解决方案2】:

    我认为您最初的方法是正确的。当您说您希望将身份验证验证与其他业务验证区分开来时,您的意思是像返回 403 和 401 之类的 http 错误吗? 如果是这种情况,请尝试使用和接口标记身份验证验证以识别它们,并且不要一次运行所有验证。首先在集合中搜索该接口的验证,如果验证失败,则发送自定义异常,您可以在 IActionFilter 中标识该异常以设置所需结果。这段代码并没有完全做到这一点,但您可以提出一个想法。

    public class HttpResponseExceptionFilter : IActionFilter, IOrderedFilter
    {
        private ISystemLogger _logger;
        public HttpResponseExceptionFilter()
        {
        }
        public int Order { get; } = int.MaxValue - 10;
    
        public void OnActionExecuting(ActionExecutingContext context) { }
    
        public void OnActionExecuted(ActionExecutedContext context)
        {
            if (context.Exception is PipelineValidationException exception)
            {
                context.Result = new ObjectResult(new Response(false, exception.ValidationErrors.FirstOrDefault()?.ErrorMessage ?? I18n.UnknownError));
                context.ExceptionHandled = true;
            }
            else if (context.Exception != null)
            {
                _logger ??= (ISystemLogger)context.HttpContext.RequestServices.GetService(typeof(ISystemLogger));
                _logger?.LogException(this, context.Exception, methodName: context.HttpContext.Request.Method);
                context.Result = new ObjectResult(new Response(false, I18n.UnknownError));
                context.ExceptionHandled = true;
            }
    
        }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-08-15
      • 1970-01-01
      • 1970-01-01
      • 2011-04-30
      • 1970-01-01
      • 2018-06-20
      相关资源
      最近更新 更多