流利的验证将业务验证与验证验证划分

发布于 2025-01-21 00:15:42 字数 1204 浏览 0 评论 0 原文

我正在使用 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();
    }
}

I'm using ASP, CQRS + MediatR and fluent validation. I want to implement user role validation, but I don't want to mix it with business logic validation. Do you have any idea how to implement this?
I mean a specific validator must be executed for a specific request.
Something tells me the solution lies in 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();
    }
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

寄风 2025-01-28 00:15:42

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

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

为此,您需要设置一个调解器管道,因此对于每个命令您都可以找到适当的验证器,验证并决定是否执行该命令或返回失败的结果。

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

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

}

并且,ICommandHandler 如下所示:

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

}

这样我们只能对命令应用验证。您不是继承 IRequestIRequestHandler,而是从 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 容器注入它们。
它等待所有验证来验证异步(因为我的验证主要需要调用数据库本身,例如获取用户角色等)。
然后检查错误并返回错误(这里我创建了一个 DTO 来包装我的错误和值以获得一致的结果)。
如果没有错误,只需让处理程序完成它的工作 return wait 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 是否为 null!IsSuccess,然后创建自定义Controller 库中的 HandleResult(result) 方法可以打开异常以返回 BadReuqestObjectResult(result)ForbiddenObjectResult(结果)

如果您更喜欢抛出、捕获和处理管道中的异常,或者您不想使用非异步实现,请阅读此 https://code-maze.com/cqrs-mediatr-fluidation/
这样,您的所有验证都与处理程序相距甚远,同时保持结果一致。

I see your concern, I also found myself in this situation. I wanted to separate my validators from handlers while also keeping them in the domain/business project. Also I didn't want to throw exceptions just to handle bad request or any other custom business exception.
You have the right idea by

I mean a specific validator must be executed for a specific request

For this, you need to set up a mediator pipeline, so for every Command you can find the appropriate the appropriate validator, validate and decide whether to execute the command or return a failed result.

First, create an interface(although not necessary but it is how I did it) of ICommand like this:

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

}

And, ICommandHandler like:

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

}

This way we can only apply validation to commands. Instead of iheriting IRequest<MyOutputDTO> and IRequestHandler<MyCommand, MyOutputDTO> you inherit from ICommand and ICommandHandler.

Now create a ValidationBehaviour for the mediator as we agreed before.

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));
        }
    }
}

This code simply, excepts all the validators in the constructor, because you register all your validator from assembly for your DI container to inject them.
It waits for all validations to validate async(because my validations mostly require calls to db itself such as getting user roles etc).
Then check for errors and return the error(here I have created a DTO to wrap my error and value to get consistent results).
If there were no errors simply let the handler do it's work return await next();

Now you have to register this pipeline behavior and all the validators.
I use autofac so I can do it easily by

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();
        }

If you use Microsoft DI, you can:

services.AddMediatR(typeof(Application.AssemblyReference).Assembly);
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));

services.AddValidatorsFromAssembly(typeof(Application.AssemblyReference).Assembly); //to add validators

Example usage:
My generic DTO Wrapper

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);
}

A sample Command:

public record CreateNewRecordCommand(int UserId, string record) : ICommand<Result<bool>>;

Validator for it:

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)))
    }
}

This way you always get a result object, you can check if error is null or !IsSuccess and then create a custom HandleResult(result) method in your Controller base which can switch on the exception to return BadReuqestObjectResult(result) or ForbiddenObjectResult(result).

If you prefer to throw, catch and handle the exceptions in the pipeline or you wan't non-async implementation, read this https://code-maze.com/cqrs-mediatr-fluentvalidation/
This way all your validations are very far from your handler while maintaining consistent results.

余罪 2025-01-28 00:15:42

我认为你最初的做法是正确的。当您说要将身份验证验证与其他业务验证分开时,您的意思是返回像 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;
        }

    }
}

I think that your initial approach its right. When you say that you want to keep the auth validations apart from the other business validation, do you mean like returning a http error like 403 and 401 right?
If thats the case try marking the auth validations with and interface to identify they, and do not run all the validations at once. Search first in the collection for a validation with that interface, and if it fails send a custom exception that you can identity in a IActionFilter to set the wanted result. This code does not do that exactly but you can make an idea.

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;
        }

    }
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文