寫在前面
上篇文章中說到了表單驗證的問題,然后嘗試了一下用擴展方法實現鏈式編程,評論區大家討論的非常激烈也推薦了一些很強大的驗證插件。其中一位園友提到了說可以使用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都寫了。
寫在最后
沒有上一篇的分享,就不會收到大家的建議,也許就不會有這次的實踐,所以,分享就意味着收獲!