如何将 ASP.NET Core 5 Web API 控制器操作的失败模型验证结果包装到另一个类中并将响应返回为 OK

Posted

技术标签:

【中文标题】如何将 ASP.NET Core 5 Web API 控制器操作的失败模型验证结果包装到另一个类中并将响应返回为 OK【英文标题】: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 【发布时间】:2021-07-03 17:23:37 【问题描述】:

我有带有一些操作(方法)的 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

我的问题是:

    我怎样才能捕捉到这个模型绑定验证结果(400 Bad Request)并将其转换为不同的格式,这样我们的前端开发人员才会满意? 我应该在 Web API 层验证我的 DTO (PersonDto) 还是最好在 MediatR 命令处理程序中验证它?我正在尝试遵守鲍勃叔叔的清洁架构。我有域、应用程序、基础设施、Web API。我的 MediatR CQRS 处理程序放置在应用层中。

【问题讨论】:

如果您通过将 400 个错误转换为 200 个错误来抑制它们,前端开发人员将不会高兴。相反,只需在正文中包含错误信息,这是 Web API 默认执行的操作。 @Aluan Haddad。事实上,我告诉我们的前端开发人员,我无法将错误包装到他们期望的类中。据我所知,IIS 会生成诸如 503 Service Unavailable 之类的错误。但是他们坚持要包装错误,所以我在这里。 【参考方案1】:

您可以使用 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; 

最后,实现一个异常过滤器或异常处理中间件来捕获该异常并返回所需的响应:

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;
    

【讨论】:

【参考方案2】:

默认情况下启用自动 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();

【讨论】:

谢谢你,这个'options.SuppressModelStateInvalidFilter = true;'有一天可能会派上用场。【参考方案3】:

您可以在 Api 方法的 biginig 中执行 ModelState.isValid(),如果模型无效,则返回 BadRequestResult()。您可以将验证错误与 BadRequestResult 一起返回。

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

【讨论】:

以上是关于如何将 ASP.NET Core 5 Web API 控制器操作的失败模型验证结果包装到另一个类中并将响应返回为 OK的主要内容,如果未能解决你的问题,请参考以下文章

如何将 ASP.NET Core 5 Web API 控制器操作的失败模型验证结果包装到另一个类中并将响应返回为 OK

将 ASP.NET Core 5 Web API 发布到 Azure 应用服务

Asp.Net Core Web API 5.0 和 Angular 中基于自定义角色的授权

EF Core 5.0 - 更新 ASP.NET Core Web API 中的多对多实体

在 ASP .NET Core 2.1 Web Api 中启用 CORS

如何从我的 ASP.NET Core 5 Web 应用程序中的 SignalR 操作中解决错误“无法访问已释放的上下文实例”?