在 .NET Core API中,如果想在接口中验证某个模型,首先需要在该模型(类)类的属性上打上来自于System.ComponentModel.DataAnnotations
命名空间下的特性。
public class SomeModel
{
[Required(ErrorMessage = "电话号码必填")]
[StringLength(30, ErrorMessage = "电话号码长度不能超过30")]
[RegularExpression(@"^1[3458][0-9]{9}$", ErrorMessage = "手机号格式不正确")]
public string Phone { get; set; }
[Required(ErrorMessage = "姓名必填")]
[StringLength(10, ErrorMessage = "姓名长度不能超过10")]
public string Name { get; set; }
}
然后在控制器方法中:
[HttpPost]
public async Task<IActionResult> SomeActionMethod([FromBody]SomeModel command){}
此时请求SomeActionMethod
这个接口,如果SomeModel
的字段值不符合验证定义,就会返回如下报错:
{
"Name": [
"姓名必填"
],
"Phone": [
"手机号格式不正确"
]
}
而在实际项目一般都有统一的返回格式,比如:
{
"success": true,
"message": "",
"errors": {
"name":[],
"phone":[]
}
}
如何让API返回的模型验证失败信息符合自定义格式呢?
在以前的 ASP .NET MVC或者 .NET Core 2.0时代我们可以通过继承ActionFilterAttribute
这个基类来实现。
public class ValidateModelAttribute : ActionFiltereAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if(!context.ModelState.IsValid)
{
context.Result = new BadRequestObjectResult(context.ModelState);
}
}
}
在Startup.cs
中配置。
services.AddMvcCore(options => {
options.Filters.Add(typeof(ValidateModelAttribute));
});
最后在控制器方法上套用。
[ValidateModel]
[HttpPost]
public async Task<IActionResult> SomeActionMethod([FromBody]SomeModel command){}
很不幸,在 .NET Core 2.2下,虽然以上写法没有报错,但实际是走不通的。
阅读官网后发现在 .NET Core 2.2下可以通过实现IResultFilter
接口来实现。在这个接口的OnResultExecuting
方法中返回自定义的格式。
在.NET Core 2.2 API中,当请求过来会来到请求管道,经过的路径大致包括:other middlewares → routing middleware → action selection → filter middlewares, 而在filter middleware中所经历的中间件依次是:authorization filter → resource filter → exception filter → model binding机制 → action filter → result filter,而上面说的
IResultFilter
就属于result filter。可以在一个类上增加多个过滤器。每个过滤器都有同步和异步方法,但不能同时使用同步或异步方法。如果既有同步方法也有异步方法会以异步方法优先。
言归正传。首先实现IResultFilter
接口。
/// <summary>
/// 模型验证失败后返回的结果
/// </summary>
public class ValidateModelResultServiceFilter : IResultFilter
{
public void OnResultExecuted(ResultExecutedContext context)
{
}
public void OnResultExecuting(ResultExecutingContext context)
{
if (!context.ModelState.IsValid)
{
context.Result = new ValidationFailedResult(context.ModelState);
}
}
}
以上返回了自定义的ValidationFailedResult
public class ValidationFailedResult : ObjectResult
{
public ValidationFailedResult(ModelStateDictionary modelState):base(new ValidationVM(modelState))
{
StatusCode = StatusCodes.Status422UnprocessableEntity;
}
}
以上在ValidationFailedResult
的构造函数中把ValidationVM
实例交给了ObjectResult
这个基类,ObjectResult
负责把ValidationVM
实例对象打印出来,即接口返回的格式。
public class ValidationVM
{
/// <summary>
/// 返回数据是否成功
/// </summary>
public bool Success { get; set; }
/// <summary>
/// 返回描述
/// </summary>
public string Message { get; set; }
public List<VaidationError> Errors { get; }
public ValidationVM(ModelStateDictionary modelState)
{
Success = false;
Message = "输入模型无法通过验证";
Errors = modelState.Keys.SelectMany(key => modelState[key].Errors.Select(x => new VaidationError(key, x.ErrorMessage))).ToList();
}
}
public class VaidationError
{
[JsonProperty(NullValueHandling=NullValueHandling.Ignore)]
public string Field { get; }
public string Message { get; }
public VaidationError(string field, string message)
{
Field = string.IsNullOrEmpty(field) ? null : field;
Message = message;
}
}
需要在Startup.cs
中放入DI容器。
services.AddScoped<ValidateModelResultServiceFilter>();
最后放置在某个控制器方法上。
[ServiceFilter(typeof(ValidateModelResultServiceFilter))]
[HttpPost]
public async Task<IActionResult> SomeActionMethod([FromBody]SomeModel model){}