如何将 ASP.NET Core 5 Web API 控制器操作的失败模型验证结果包装到另一个 class 和 return 响应中

How can I wrap failed model validation result of an ASP.NET Core 5 Web API controller action into another class and return response as OK

我有 ASP.NET Web API 控制器和一些操作(方法)。让我们这样说:

[HttpPost]
public async Task<ActionResult> SavePerson([FromBody]PersonDto person)
{
    await _mediatr.Send(new SavePerson.Command(person));
    return Ok();
}

PersonDto 看起来像这样:

public record PersonDto([Required, MinLength(3)]string Name, int? Age);

当我用无效的人员数据(Name.Length < 3 等...)调用我的 Web API 操作 'SavePerson' 时,ASP.NET 核心模型绑定验证中断执行和 returns 400 (Bad Request) 因为它应该。当我传递有效的个人数据时,它工作正常。

我的问题是:

  1. 如何捕获这个模型绑定验证结果(400 Bad Request)并将其转换为不同的格式,这样我们的前端开发人员才会高兴?
  2. 我应该在 Web API 层验证我的 DTO (PersonDto) 还是在 MediatR 命令处理程序中验证它更好?我正在尝试遵守 Uncle Bob 的 Clean Architecture。我有域、应用程序、基础设施、Web API。我的 MediatR CQRS 处理程序放在应用程序层。

您可以在 Api 方法的 biginig 中执行 ModelState.isValid(),如果模型无效,则 return 执行 BadRequestResult()。您可以 return 验证错误以及 BadRequestResult。

您需要从模型状态中获取验证错误并填写您的自定义错误对象。这样您的客户就可以看到更有意义的错误。

默认启用自动 400 错误请求响应。要禁用它,请在 Startup ConfigureServices 方法中使用以下代码:

services.Configure<ApiBehaviorOptions>(options =>
{
     options.SuppressModelStateInvalidFilter = true;
});

然后您可以像这样手动处理无效模型状态:

[HttpPost]
public async Task<ActionResult> SavePerson([FromBody]PersonDto person)
{
    if(!ModelState.IsValid)
        return BadRequest(ModelState);// or what ever you want
    await _mediatr.Send(new SavePerson.Command(person));
    return Ok();
}

您可以使用 Jason Taylor 的 Clean Architecture 方法。不使用属性验证,而是使用 FluentValidation:

public class CreatePersonCommandValidator : AbstractValidator<SavePerson.Command>
{
    public CreatePersonCommandValidator()
    {
        RuleFor(v => v.Title)
            .NotEmpty().WithMessage("Title is required.")
            .MinimujLength(200).WithMessage("Title at least should have 3 characters.");
    }
}

使用 MediatR 行为执行验证并将错误转换为验证异常:

public class ValidationBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
        where TRequest : IRequest<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())
        {
            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).ToList();

            if (failures.Count != 0)
                throw new ValidationException(failures);
        }
        return await next();
    }
}

验证异常:

public class ValidationException : Exception
{
    public ValidationException()
        : base("One or more validation failures have occurred.")
    {
        Errors = new Dictionary<string, string[]>();
    }

    public ValidationException(IEnumerable<ValidationFailure> failures)
        : this()
    {
        Errors = failures
            .GroupBy(e => e.PropertyName, e => e.ErrorMessage)
            .ToDictionary(failureGroup => failureGroup.Key, failureGroup => failureGroup.ToArray());
    }

    public IDictionary<string, string[]> Errors { get; }
}

最后,实施异常过滤器或异常处理中间件以捕获该异常和 return 所需的响应:

public class ApiExceptionFilterAttribute : ExceptionFilterAttribute
{

    private readonly IDictionary<Type, Action<ExceptionContext>> _exceptionHandlers;

    public ApiExceptionFilterAttribute()
    {
        // Register known exception types and handlers.
        _exceptionHandlers = new Dictionary<Type, Action<ExceptionContext>>
        {
            { typeof(ValidationException), HandleValidationException }
        };
    }

    public override void OnException(ExceptionContext context)
    {
        HandleException(context);

        base.OnException(context);
    }

    private void HandleException(ExceptionContext context)
    {
        Type type = context.Exception.GetType();
        if (_exceptionHandlers.ContainsKey(type))
        {
            _exceptionHandlers[type].Invoke(context);
            return;
        }

        if (!context.ModelState.IsValid)
        {
            HandleInvalidModelStateException(context);
            return;
        }

        HandleUnknownException(context);
    }

    private void HandleValidationException(ExceptionContext context)
    {
        var exception = context.Exception as ValidationException;

        //var details = new ValidationProblemDetails(exception.Errors)
        //{
            //Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1"
        //};


        context.Result = Returns your response type //new BadRequestObjectResult(details);

        context.ExceptionHandled = true;
    }
}