webapi - 模型驗證

神牛003發表於2016-12-26

本次要和大家分享的是webapi的模型驗證,講解的內容可能不單單是做驗證,但都是圍繞模型來說明的;首先來吐槽下,今天下午老闆為自己買了套新辦公傢俱,看起來挺好說明老闆有錢,不好的是我們幹技術的又成了搬運工(誰叫技術部男的多呢哈哈),話說讓我們搬點兒什麼小座椅板凳就夠了吧,為什麼4大箱的傢俱都讓我們動手,每箱東西拆分出來每件幾乎需要至少4個人才能挪到的東西,而且不少呢,這是讓我們搬完後不用上班的節奏吧;我很想問的是買這麼貴的東西,難道不給包送和組裝?行政部門就不能請點搬運工,非要節約這點錢(技術可不是兼職的搬運工啊)?東西少那是鍛鍊身體沒關係,東西多了那就是折磨人,這裡說的想必也是大眾技術朋友們的心聲吧呵呵;好了不多說了,本章內容希望大家喜歡,也希望各位多多掃碼支援和點贊謝謝:

 

» 增加模型驗證

» 自定義過濾器,輸出模型驗證資訊

» FromUri和FromBody用途

 

下面一步一個腳印的來分享:

» 增加模型驗證

首先,我們測試用例使用上一篇的 MoStudent 學生類,模型驗證需要在對應提交類中的需要驗證格式的屬性增加一些註解標記,常用的標記有:

. Required:必須滿足不為空

. RegularExpression:正規表示式驗證

. StringLength:指定字元允許的範圍

. DataType:資料型別,常用於密碼型別,如: DataType.Password 

. Range:指定數字允許的範圍

這裡我們例項的 MoStudent  類用到的註解,如下程式碼:

 public class MoStudent
    {

        public int Id { get; set; }

        [Required(ErrorMessage = "名稱必須填寫")]
        [RegularExpression(@"\w{2,15}", ErrorMessage = "名稱應為2-15長度的字母組合")]
        public string Name { get; set; }

        public bool Sex { get; set; }
        public DateTime Birthday { get; set; }
    }

然後,我們把儲存學生的方法內容定義成這樣,iis訪問地址為 http://localhost:1001/s/add ,程式碼對應:

        [Route("add")]
        [HttpPost]
        public HttpResponseMessage AddStudent(MoStudent moStudent)
        {

            if (ModelState.IsValid)
            {

                return Request.CreateResponse(HttpStatusCode.OK, moStudent);
            }
            return Request.CreateResponse(HttpStatusCode.BadRequest, ModelState);
         }

這裡的關鍵程式碼是 ModelState.IsValid ,通過這個來判斷使用者錄入的資訊是否滿足我們定義在實體類中的註解規則,不滿足我這裡直接輸出訪問狀態為 HttpStatusCode.BadRequest ,再來咋們用ajax提交新增學生表單到這個介面,但不錄入任何學生資訊,會得到console輸出的返回的資訊:

這裡能看到404,明顯從上面的程式碼來看走到了 Request.CreateResponse(HttpStatusCode.BadRequest, ModelState); 這段程式碼,也就是說 ModelState.IsValid 的結果是false,驗證沒有通過,因為我們在錄入學生資訊頁面的文字框內,沒有輸入任何資訊就直接提交的表單,所以這裡驗證沒有通過;但是作為一個webapi介面而言,這種如果提交格式不正確,就直接返回400錯誤,這樣明顯不友好,那麼我們需要做一下變動,首先需要定義一個公共介面返回類 MoResult ,該類主要用來作為webapi介面的統一返回資料格式標準(這也是通常介面需要的一種響應資料規則格式),我們定義如下格式程式碼:

 1 /// <summary>
 2     /// 結果輸出類
 3     /// </summary>
 4     public class MoResult
 5     {
 6 
 7         /// <summary>
 8         /// 0:失敗,1:成功 其他
 9         /// </summary>
10         public EnResultStatus Status { get; set; }
11 
12         /// <summary>
13         /// 錯誤資訊
14         /// </summary>
15         public object Msg { get; set; }
16 
17         /// <summary>
18         /// 物件資訊
19         /// </summary>
20         public object Data { get; set; }
21     }
22 
23     /// <summary>
24     /// 列舉型別
25     /// </summary>
26     public enum EnResultStatus
27     {
28         失敗 = 0,
29         成功 = 1
30     }

然後,調整下apiController對應儲存學生資訊的Action方法程式碼如:

 1 [Route("add")]
 2         [HttpPost]
 3         public HttpResponseMessage AddStudent(MoStudent moStudent)
 4         {
 5 
 6             var moResult = new MoResult() { Msg = EnResultStatus.失敗 };
 7             try
 8             {
 9                 if (ModelState.IsValid)
10                 {
11                     var isfalse = db.Save(moStudent);
12                     moResult.Status = isfalse ? EnResultStatus.成功 : EnResultStatus.失敗;
13                     moResult.Data = moStudent;
14                 }
15                 else
16                 {
17                     //如果驗證是吧,只取第一個錯誤資訊返回
18                     var item = ModelState.Values.Take(1).SingleOrDefault();
19                     moResult.Msg = item.Errors.Where(b => !string.IsNullOrWhiteSpace(b.ErrorMessage)).Take(1).SingleOrDefault().ErrorMessage;
20                 }
21             }
22             catch (Exception ex)
23             {
24                 moResult.Msg = ex.Message;
25             }
26             return Request.CreateResponse(HttpStatusCode.OK, moResult);
27         }

需要注意的幾點是:

1. 這裡我們通過 MoResult 的Data屬性來返回我們新增成功Student輸入資訊;

2. 如果 ModelState.IsValid 驗證失敗,取第一個驗證錯誤資訊返回給呼叫方,其目的讓呼叫方能夠提醒使用者哪些資訊錄入的不全或者格式不對

好了咋們來一起看下這個時候ajax提交的空表單資料後,獲取到webapi介面返回的資料資訊如:

很明顯這個錯誤資訊就是咋們在實體 MoStudent 中對Name屬性做的一個驗證錯誤資訊,再這裡一個前端呼叫webapi介面驗證的流程基本就完事了,當然一般這種空驗證也是需要前端自己驗證的,好了咋們也來看下,如果錄入完成學生資訊的表單提交過後會有什麼樣子的資料返回呢:

 

» 自定義過濾器,輸出模型驗證資訊

首先,我們需要明確的是,任意webapi介面基本都是需要有請求引數的格式驗證的,如果按照上一節直接在實體類中增加註解驗證,那麼我們就沒必要在每一個api介面對應的action中再寫程式碼 ModelState.IsValid 去判斷是否驗證成功,或者是獲取驗證後的錯誤資訊了,因為webapi提供一個 ActionFilterAttribute Action過濾器,這個作用主要是請求到api的Action方法時候用來執行一些東西,本例項通過繼承她,來自定義一個驗證過濾器 ValidateModelAttribute ,主要用來統一獲取驗證錯誤資訊,並且輸出響應給呼叫方,下面先來看下程式碼:

 1 /// <summary>
 2     /// 模型格式驗證
 3     /// </summary>
 4     public class ValidateModelAttribute : ActionFilterAttribute
 5     {
 6 
 7 
 8         public override void OnActionExecuting(System.Web.Http.Controllers.HttpActionContext actionContext)
 9         {
10 
11             if (!actionContext.ModelState.IsValid)
12             {
13 
14                 var moResult = new MoResult() { Msg = "請求資料異常", Status = EnResultStatus.失敗 };
15                 //自定義錯誤資訊
16                 var item = actionContext.ModelState.Values.Take(1).SingleOrDefault();
17                 moResult.Msg = item.Errors.Where(b => !string.IsNullOrWhiteSpace(b.ErrorMessage)).Take(1).SingleOrDefault().ErrorMessage;
18                 actionContext.Response = actionContext.Request.CreateResponse(System.Net.HttpStatusCode.OK, moResult);
19             }
20         }
21     }

同第一小節的程式碼差不多,主要用來判斷是否通過實體類的註解驗證,如果不通過獲取第一個錯誤資訊返回給呼叫方;這裡要注意的是我們重寫了 ActionFilterAttribute 過濾器中的 OnActionExecuting 方法;好了下面我們還需要把自定義的 ValidateModelAttribute  在 App_Start/WebApiConfig.cs 加入一下 config.Filters.Add(new ValidateModelAttribute()); 這個程式碼,意思吧自定義的過濾器假如到webapi中;再來咋們直接就可以在我們新增學生的Action上方使用 [ValidateModel] 標記來驗證呼叫方傳遞給webapi的引數了,我們把學生新增Action AddStudent 程式碼改為如下所示:

 1  [Route("add")]
 2         [HttpPost]
 3         [ValidateModel]  //這裡是自定義過濾
 4         public HttpResponseMessage AddStudent(MoStudent moStudent)
 5         {
 6 
 7             var moResult = new MoResult();
 8             try
 9             {
10                 var isfalse = db.Save(moStudent);
11                 moResult.Status = isfalse ? EnResultStatus.成功 : EnResultStatus.失敗;
12                 moResult.Data = moStudent;
13             }
14             catch (Exception ex)
15             {
16                 moResult.Msg = ex.Message;
17             }
18             return Request.CreateResponse(HttpStatusCode.OK, moResult);
19         }

然後,再用ajax提交一下空表單資料,會得到如圖所示的資訊:

能看出自定義驗證過濾器效果和我們之前直接寫在程式碼中驗證的提示資訊一樣,總體來說自定義的驗證使得api中的Action程式碼精簡了很多,很提倡使用;

 

» FromUri和FromBody用途

首先,我們來看一段程式碼:

 1 [Route("all01_2/{id:int?}")]
 2         [AcceptVerbs("POST", "GET")]
 3         public HttpResponseMessage GetAllStudents01_2(int id = 0)
 4         {
 5             var students = db.GetAll();
 6 
 7             if (id > 0)
 8             {
 9                 students = students.Where(b => b.Id == id).ToList();
10             }
11 
12             return Request.CreateResponse(HttpStatusCode.OK, students);
13         }

這裡的action傳遞進來了一個名稱id的引數,然後下面做了 id > 0 的判斷來獲取不同的資料,這種場景我們經常會遇到,並且查詢條件的引數不止一個,這樣來看我們會不斷的在action裡面增加傳遞的引數,格式可能會如此: public HttpResponseMessage GetAllStudents01_2(int id = 0,引數2,引數3,引數...) ,這樣如果需要用到的引數有10個以上,通常看起來不是很方便,這個時候FromUri和FromBody的用途就來了;先來看FromUri,首先需要我們把上面的那個Action方法的入參格式改成這樣:

 1 [Route(@"all01_3/{id:int=0}/{name?}")]
 2         [AcceptVerbs("POST", "GET")]
 3         public HttpResponseMessage GetAllStudents01_3([FromUri]MoSearch moSearch)
 4         {
 5             var students = db.GetAll().AsEnumerable();
 6 
 7             if (moSearch.Id > 0)
 8             {
 9                 students = students.Where(b => b.Id == moSearch.Id);
10             }
11 
12             if (!string.IsNullOrWhiteSpace(moSearch.Name))
13             {
14                 students = students.Where(b => b.Name.Contains(moSearch.Name));
15             }
16 
17             return Request.CreateResponse(HttpStatusCode.OK, students);
18         }

通過實體類 MoSearch 來獲取呼叫端傳遞的引數,實體類MoSearch定義的屬性欄位如:

public class MoSearch
    {

        public int Id { get; set; }

        public string Name { get; set; }
    }

然後我們改造下查詢學生列表的ajax請求方法如:

 1 $("#btnSearch").on("click", function () {
 2 
 3             var tabObj = $("#tab tbody");
 4             tabObj.html('tr><td colspan="4">載入中...</td></tr>');
 5 
 6 
 7             var url = "http://localhost:1001/s/all01_3/1";
 8             var txtName = $("input[name='txtName']").val();
 9             if (txtName.length > 0) {
10                 url = url + "/" + txtName;
11             }
12 
13             $.get(url, function (data) {
14 
15                 console.log(data);
16 
17                 var tabHtml = [];
18                 $.each(data, function (i, item) {
19 
20                     tabHtml.push('<tr>');
21                     tabHtml.push("<td>" + item.Id + "</td>");
22                     tabHtml.push("<td>" + item.Name + "</td>");
23                     tabHtml.push("<td>" + (item.Sex ? "" : "") + "</td>");
24                     tabHtml.push("<td>" + item.Birthday + "</td>");
25                     tabHtml.push('</tr>');
26                 });
27                 if (tabHtml.length <= 0) { tabHtml.push('tr><td colspan="4">暫無資料</td></tr>'); }
28 
29                 tabObj.html(tabHtml.join(''));
30             });
31         });

這裡Id=1是固定了,直接查詢id=1的學生資訊,並且有一個可傳遞的引數學生名稱txtName的值,如果使用者沒有輸入學生名稱,那麼就不會加入到這個get請求中,下面來看下首先不輸入學生姓名查詢條件的效果:

這裡能看出來只有學生編號為1的學生被查出來了,下面我們再錄入姓名查詢條件,能看到結果如:

能看到此時查不到名稱為“小2”的學生資訊,咋們再改成“小1”試試,

這個時候小1的學生能被查出來,這說明咋們的api的all01_3能夠通過 FromUri]MoSearch moSearch 獲取到我們輸入的引數;下面我們使用FromBody來演示她的效果,首先需要修改js請求webapi格式如:

然後把webapi的all01_3修改為如下程式碼:

 1 [Route(@"all01_3/{name?}")]
 2         [AcceptVerbs("POST", "GET")]
 3         public HttpResponseMessage GetAllStudents01_3([FromBody]MoSearch moSearch)
 4         {
 5             var students = db.GetAll().AsEnumerable();
 6 
 7             if (moSearch.Id > 0)
 8             {
 9                 students = students.Where(b => b.Id == moSearch.Id);
10             }
11 
12             if (!string.IsNullOrWhiteSpace(moSearch.Name))
13             {
14                 students = students.Where(b => b.Name.Contains(moSearch.Name));
15             }
16 
17             return Request.CreateResponse(HttpStatusCode.OK, students);
18         }

主要區別吧FromUri換成了FromBody,最後咋們來一起看下沒有錄入學生名稱查詢條件的測試效果圖:

錄入學生查詢條件後的效果圖:

由上看出能正常查詢出來資料,這說明 [FromBody]MoSearch moSearch 獲取到了呼叫客戶端的請求引數;好了FromBody和FromUri測試的例子就這些,這兩者的區別這裡總結下:

FromBody:主要用來獲取post方式請求的引數

FromUri:主要獲取url地址的請求引數(可以看成get方式)

這兩者的效果對比我就不做了,有興趣的朋友可以自己驗證下;本次分享的內容就到這裡吧,主要講解了關於資料模型的一些知識,希望各位喜歡,多多點贊。

相關文章