鼎鼎知识库
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

17.NET Core 2.2在API中返回自定义模型验证失败信息.md 5.2KB

在 .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){}