再說表單驗證,在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都寫了。

 

寫在最后

沒有上一篇的分享,就不會收到大家的建議,也許就不會有這次的實踐,所以,分享就意味着收獲!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM