首页 > 代码库 > 再说表单验证,在Web Api中使用ModelState进行接口参数验证

再说表单验证,在Web Api中使用ModelState进行接口参数验证

写在前面

上篇文章中说到了表单验证的问题,然后尝试了一下用扩展方法实现链式编程,评论区大家讨论的非常激烈也推荐了一些很强大的验证插件。其中一位园友提到了说可以使用MVC的ModelState,因为之前通常都在Web项目中用没在Api项目用过,想想Api方法接收的多参数都封装成了一个实体类,独立于数据Model层,这样其实很方便用ModelState做验证,于是尝试了一下。


认识ModelState

我们都知道在MVC中使用ModelState实现表单验证非常简单,借助jquery.validate.unobtrusive这个插件就能轻松的在页面上输出错误信息,详细的介绍可以参考这篇文章《[Asp.net MVC]Asp.net MVC5系列--在模型中添加验证规则》。但是在WebApi中没有视图页让我们来展示错误信息,那要怎么捕获到验证失败的信息并作为请求结果返回给请求端呢?以前学MVC的时候也没有深究ModelState是什么机制实现验证,为什么用Html.ValidationMessageFor就能输出错误信息?这次就系统的了解一下,那就先看看ModelState到底是什么鬼。转到它的定义发现它就是一个Dictionary:

 技术分享

为了看个究竟,打开Reflector找到ModelStateDictionary,发现它有这些属性:

    // Properties    public int Count { get; }    public bool IsReadOnly { get; }    public bool IsValid { get; }    public ModelState this[string key] { get; set; }    public ICollection<string> Keys { get; }    public ICollection<ModelState> Values { get; }

那这里的Keys装的就是被验证的Model的属性啦,Values就是对应key的值(ModelState类型)了。再看看ModelState类型是个什么鬼:

[Serializable]public class ModelState{    // Fields    private ModelErrorCollection _errors;    // Methods    public ModelState();    // Properties    public ModelErrorCollection Errors { get; }    public ValueProviderResult Value { get; set; }}

看它有两个属性Errors和Values,从它们的类型名称就能看出到底是干嘛的了。Errors装的就是验证失败的错误信息(具体就是一个ModelError),继续看到底包含写什么东西:

[Serializable]public class ModelError{    // Methods    public ModelError(Exception exception);    public ModelError(string errorMessage);    public ModelError(Exception exception, string errorMessage);    // Properties    public string ErrorMessage { get; private set; }    public Exception Exception { get; private set; }}

啊~看到ErrorMessage瞬间觉得哈皮啊,这就是我们需要返回去的鬼东西!

可是为什么是Collection呢?那肯定啊,因为一个字段可以有多个验证规则,比如有Required还有MaxLength等等。Value装的就这个字段的值,具体就是一个ValueProviderResult,具体里面是什么就不贴代码了,因为有什么和本文没太大关系,自己回去偷偷看就好了。关于模型是怎么验证的错误信息是怎么绑上去的,看以看看Artech的Model验证系统运行机制是如何实现的?,超详细的解说。好了,来龙去脉都摸清楚了,那就开始码代码,主要就是手动把错误信息抓出来。

 

代码实现

以登录场景为例,为登录接口封装了一个登录模型,并加上验证规则:

    public class MemberLogin    {        /// <summary>        /// 登录手机号        /// </summary>        [Required(ErrorMessage = "请输入手机号码")]        [RegularExpression(@"^1[3|4|5|7|8][0-9]\d{8}$", ErrorMessage = "手机号格式错误")]        public string Phone { get; set; }        /// <summary>        /// 验证码key        /// </summary>        [Required(ErrorMessage = "验证码无效")]        public string CodeKey { get; set; }        /// <summary>        /// 验证码值        /// </summary>        [Required(ErrorMessage = "请输入短信验证码")]        public string CodeValue { get; set; }}

然后在接口里第一行加上:

          if (!ModelState.IsValid)            {                string error = string.Empty;                foreach (var key in ModelState.Keys)                {                    var state = ModelState[key];                    if (state.Errors.Any())                    {                        error = state.Errors.First().ErrorMessage;                        break;                    }                }                return ApiResponse(new ReturnMessage() { Status = ResultStatus.Failed, Message = error });            }

主要思路就是:验证失败后遍历ModelState的Key,如果这个被验证的字段至少有一项验证失败(ModelError),那么就拿到第一个ErrorMessage,然后就结束遍历,因为取到所有的也没什么用,也方便前端对结果进行处理。

用swagger的接口调式工具发起请求,得到响应如下:

 技术分享

CodeValue也是空的但是没有返回错误信息,是因为在取错误信息的时候取到第一条后就break了。

到这里貌似大功告成了,但仔细一想,每个接口里都要写这么大一坨重复代码,真是很难受,那怎么搞?没错,MVC里有个神奇的东西-Filter,WebApi完整地沿用了这一优秀的特性,用比较高端的说法就是面向切面编程(AOP)中的分离横切点的思想,从而实现代码复用。那就创建一个Attribute类并继承System.Web.Http.Filters .ActionFilterAttribute,然后重写OnActionExecuting方法,具体内容就是刚才那一大坨稍微调整一下,完整代码为:

   [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method , Inherited = true)]    public class ModelValidationAttribute : ActionFilterAttribute    {        public override void OnActionExecuting(System.Web.Http.Controllers.HttpActionContext actionContext)        {            var modelState = actionContext.ModelState;            if (!modelState.IsValid)            {                string error = string.Empty;                foreach (var key in modelState.Keys)                {                    var state = modelState[key];                    if (state.Errors.Any())                    {                        error = state.Errors.First().ErrorMessage;                        break;                    }                }                ReturnMessage response = new ReturnMessage() { Status = ResultStatus.Failed, Message = error };                actionContext.Response = new HttpResponseMessage(HttpStatusCode.Accepted)                {                    Content = new StringContent(JsonConvert.SerializeObject(response), System.Text.Encoding.GetEncoding("UTF-8"), "application/json")                };            }        }    }

然后在接口上打上[ModelValidationAttribute]这么个标签就ok了。当然了,这个Attribute我指定了使用范围包含Class,直接打在Controller上面也是阔以滴~这样就不用每个Action都写了。

 

写在最后

没有上一篇的分享,就不会收到大家的建议,也许就不会有这次的实践,所以,分享就意味着收获!

再说表单验证,在Web Api中使用ModelState进行接口参数验证