写在前面
上篇文章中说到了表单验证的问题,然后尝试了一下用扩展方法实现链式编程,评论区大家讨论的非常激烈也推荐了一些很强大的验证插件。其中一位园友提到了说可以使用 MVC 的 ModelState,因为之前通常都在 web 项目中用没在 Api 项目用过,想想 Api 方法接收的多参数都封装成了一个实体类,独立于数据 Model 层,这样其实很方便用 ModelState 做验证,于是尝试了一下。
认识 ModelState
我们都知道在 MVC 中使用 ModelState 实现表单验证非常简单,借助 jquery.validate.unobtrusive 这个插件就能轻松的在页面上输出错误信息,详细的介绍可以参考这篇文章。但是在 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 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 的,超详细的解说。好了,来龙去脉都摸清楚了,那就开始码代码,主要就是手动把错误信息抓出来。
代码实现
以登录场景为例,为登录接口封装了一个登录模型,并加上验证规则:
- 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")
- };
- }
- }
- }
写在最后
没有上一篇的分享,就不会收到大家的建议,也许就不会有这次的实践,所以,分享就意味着收获!
来源: http://www.cnblogs.com/hohoa/p/5839993.html