如何将 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) 因为它应该。当我传递有效的个人数据时,它工作正常。
我的问题是:
- 如何捕获这个模型绑定验证结果(400 Bad Request)并将其转换为不同的格式,这样我们的前端开发人员才会高兴?
- 我应该在 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;
}
}
我有 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) 因为它应该。当我传递有效的个人数据时,它工作正常。
我的问题是:
- 如何捕获这个模型绑定验证结果(400 Bad Request)并将其转换为不同的格式,这样我们的前端开发人员才会高兴?
- 我应该在 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;
}
}