目錄
一、模型狀態 - ModelState
二、數據注解 - Data Annotations
三、自定義數據注解
四、全局數據驗證
五、單元測試
一、模型狀態 - ModelState
我理解的ModelState是微軟在ASP.NET MVC中提出的一種新機制,它主要實現以下幾個功能:
1. 保存客戶端傳過來的數據,如果驗證不通過,把數據返回到客戶端,這樣可以保存用戶輸入,不需要重新輸入。
2. 驗證數據,以及保存數據對應的錯誤信息。
3. 微軟的一種DRY(Don't Repeat Yourself)設計,通過ModelState可以做服務端驗證,同時可以配合jquery validation生成前端數據驗證。
但是在Web API里面,ModelState的主要功能就只剩下第2點了。
需要注意的是,ModelState一般只做輸入驗證,一些其他的業務驗證還有要在特定的地方進行處理。
二、數據注解 - Data Annotations
數據注解可以理解為驗證數據的邏輯或方法,微軟本身有提供一批數據注解,當然我們也可以自定義數據注解,以下是微軟提供的常見的數據注解:
1. Required - 非空驗證。
當一個輸入是null時會引發一個驗證錯誤。
當屬性類型是string的時候,如果設置了AllowEmptyStrings = false(默認為false),那么輸入空字符串或者空格,也會引發一個驗證錯誤。
[Required] public string Name { get; set; } [Required(AllowEmptyStrings = true)] public string Exchange { get; set; }
2. StringLength - 長度驗證。
當輸入大於指定最大長度,或者小於最大指定長度時,會引發一個驗證錯誤。
[StringLength(100)] public string Symbol { get; set; } [StringLength(100, MinimumLength = 10)] public string Name { get; set; }
3. RegularExpression - 正則表達式驗證。
當輸入內容不滿足指定的正則表達式時,會引發一個驗證錯誤。
注:在.NET Framework 4.6.1添加了一個MatchTimeoutInMilliseconds屬性,用來設定正則表達時驗證時長。如超時,則拋出RegexMatchTimeoutException異常。
[RegularExpression("your expression")] public string Symbol { get; set; }
4. Range - 值范圍驗證
當輸入的值小於最小值或者大於最大值時,會引發一個驗證錯誤,這里要求驗證字段的類型需要實現IComparable接口。
[Range(10, 100)] public double OpenPrice { get; set; } [Range(typeof(double), "10", "100")] public double ClosePrice { get; set; }
5. Compare - 對比驗證
確保對象兩個屬性擁有相同的值。如果兩個值不同,會引發一個驗證錯誤。
public string Name { get; set; } [Compare("Name")] public string ConfirmName { get; set; }
6. Remote - 遠程調用驗證
Remote可以利用服務端回調函數執行客戶端的驗證邏輯。
注:該數據注解是ASP.NET MVC特有的注解,在Web Api中無此注解。
[Remote("CheckName", "Account"] public string UserName{ get; set; } public class AccountController: Controller { public JsonResult CheckName(string name) { return Json(true); } }
三、自定義數據注解
如果覺得微軟提供的數據注解不夠用,也可以自己寫數據注解,只需要繼承ValidationAttribute,並復寫IsValid方法。
下面是一個來自《ASP.NET MVC 5高級編程》的一個例子MaxWordsAttribute,用於限制屬性的單詞個數。

public class MaxWordsAttribute : ValidationAttribute { private readonly int _maxWords; public MaxWordsAttribute(int maxWords) { _maxWords = maxWords; } protected override ValidationResult IsValid(object value, ValidationContext validationContext) { if (value != null) { var valueAsString = value.ToString(); if (valueAsString.Split(' ').Length > _maxWords) { return new ValidationResult("Too many words!"); } } return ValidationResult.Success; } }
[Required] [MaxWords(2)] public string Name { get; set; }
[HttpPost] public IHttpActionResult Create(Stock stock) { if (!ModelState.IsValid) { return BadRequest(ModelState); } return CreatedAtRoute("Get", new { symbol = stock.Symbol }, stock); }
Swashbuckle Help Page測試效果如下:
如何使用Help Page可參考我上一篇文章《我這么玩Web Api(一):幫助頁面或用戶手冊(Microsoft and Swashbuckle Help Page)》。
四、全局數據驗證
我們在使用數據驗證的時候,往往會出現許多重復的代碼,如下圖:
有沒有辦法減少這些重復的代碼呢?我從“Model Validation in ASP.NET Web API”這篇文章中找到了方法。
首先,我們需要寫一個GlobalActionFilterAttribute。
public class GlobalActionFilterAttribute: ActionFilterAttribute { public override void OnActionExecuting(HttpActionContext actionContext) { if (actionContext.ModelState.IsValid == false) { actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, actionContext.ModelState); } } }
然后,在WebApiConfig里注冊一下這個Attribute。
public static void Register(HttpConfiguration config) { config.MapHttpAttributeRoutes(); config.Routes.MapHttpRoute("DefaultApi", "api/{controller}/{id}", new { id = RouteParameter.Optional } ); //register the custom action filter config.Filters.Add(new GlobalActionFilterAttribute()); }
那么,我們把Controller中的數據驗證注釋掉,依舊會得到相同的效果。
如果想只對Post請求進行驗證,可以在GlobalActionFilterAttribute加對請求方式的判斷:
public class GlobalActionFilterAttribute : ActionFilterAttribute { public override void OnActionExecuting(HttpActionContext actionContext) { //If you only want to validate the post request. if (actionContext.Request.Method != HttpMethod.Post) { return; } if (actionContext.ModelState.IsValid == false) { actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, actionContext.ModelState); } } }
如果某些Controller或Action需要繞過數據驗證,那么可以這么實現:
1. 定義一個BypassModelStateValidationAttribute
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = false)] public sealed class BypassModelStateValidationAttribute : Attribute { }
2. 在不需要驗證的Controller或者Action上加這個Attribute
[HttpPut] [BypassModelStateValidation] public IHttpActionResult Update(Stock stock) { //if (!ModelState.IsValid) //{ // return BadRequest(ModelState); //} return StatusCode(HttpStatusCode.NoContent); }
3. 在GlobalActionFilterAttribute加對BypassModelStateValidationAttribute的判斷:
public class GlobalActionFilterAttribute : ActionFilterAttribute { public override void OnActionExecuting(HttpActionContext actionContext) { //If you only want to validate the post request. if (actionContext.Request.Method != HttpMethod.Post) { return; } var passby = actionContext.ActionDescriptor.GetCustomAttributes<BypassModelStateValidationAttribute>().Any() || actionContext.ControllerContext.ControllerDescriptor.GetCustomAttributes<BypassModelStateValidationAttribute>().Any(); if (passby) { return; } if (actionContext.ModelState.IsValid == false) { actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, actionContext.ModelState); } } }
五、單元測試
我使用BDD的風格編寫單元測試,關於BDD的詳細信息,可查看我之前的文章《行為驅動開發(BDD)實踐示例》。
對於全局數據驗證,我設計了3個測試用例。
1. 非Post請求不做驗證 - HttpMethodNotMatched
feature描述:
測試代碼:
[Binding] [Scope(Scenario = @"HttpMethodNotMatched")] public class HttpMethodNotMatchedTest : GlobalActionFilterAttributeTests { [Given(@"非Post方式的請求")] public void Given() { HttpActionContext.Request.Method = HttpMethod.Get; } [When(@"執行OnActionExecuting方法")] public void When() { GlobalActionFilterAttribute.OnActionExecuting(HttpActionContext); } [Then(@"Response為空")] public void Then() { Assert.IsNull(HttpActionContext.Response); } }
2. 設置了跳過驗證 - BypassModelStateValidation
feature描述:
測試代碼:
[Binding] [Scope(Scenario = @"BypassModelStateValidation")] public class BypassModelStateValidationTest : GlobalActionFilterAttributeTests { [Given(@"BypassModelStateValidationAttribute")] public void Given() { HttpActionContext.Request.Method = HttpMethod.Post; HttpActionContext.ActionDescriptor = ActionDescriptorMock.Object; ActionDescriptorMock.Setup(m => m.GetCustomAttributes<BypassModelStateValidationAttribute>()).Returns(new Collection<BypassModelStateValidationAttribute>(new[] { new BypassModelStateValidationAttribute() })); HttpActionContext.ControllerContext.ControllerDescriptor = ControllerDescriptorMock.Object; ControllerDescriptorMock.Setup(m => m.GetCustomAttributes<BypassModelStateValidationAttribute>()).Returns(new Collection<BypassModelStateValidationAttribute>()); } [When(@"執行OnActionExecuting方法")] public void When() { GlobalActionFilterAttribute.OnActionExecuting(HttpActionContext); } [Then(@"Response為空")] public void Then() { Assert.IsNull(HttpActionContext.Response); } }
3. 驗證不通過 - ModelStateInvalid
feature描述:
測試代碼:
[Binding] [Scope(Scenario = @"ModelStateInvalid")] public class ModelStateInvalidTest : GlobalActionFilterAttributeTests { [Given(@"ModelState錯誤信息")] public void Given() { HttpActionContext.Request.Method = HttpMethod.Post; HttpActionContext.ActionDescriptor = ActionDescriptorMock.Object; ActionDescriptorMock.Setup(m => m.GetCustomAttributes<BypassModelStateValidationAttribute>()).Returns(new Collection<BypassModelStateValidationAttribute>()); HttpActionContext.ControllerContext.ControllerDescriptor = ControllerDescriptorMock.Object; ControllerDescriptorMock.Setup(m => m.GetCustomAttributes<BypassModelStateValidationAttribute>()).Returns(new Collection<BypassModelStateValidationAttribute>()); HttpActionContext.ModelState.AddModelError("stock.Name", "The Name field is required."); } [When(@"執行OnActionExecuting方法")] public void When() { GlobalActionFilterAttribute.OnActionExecuting(HttpActionContext); } [Then(@"返回Bad Request")] public void Then() { Assert.AreEqual(HttpStatusCode.BadRequest, HttpActionContext.Response.StatusCode); } }
單元測試結果:
說明:
GlobalActionFilterAttributeTests是單元測試的父類,公共的部分可以抽取到這里。其中ContextUtil是微軟源碼中的測試輔助類。
public class GlobalActionFilterAttributeTests { protected readonly Mock<HttpActionDescriptor> ActionDescriptorMock = new Mock<HttpActionDescriptor>(); protected readonly Mock<HttpControllerDescriptor> ControllerDescriptorMock = new Mock<HttpControllerDescriptor>(); protected HttpActionContext HttpActionContext; protected GlobalActionFilterAttribute GlobalActionFilterAttribute; public GlobalActionFilterAttributeTests() { HttpActionContext = ContextUtil.CreateActionContext(); GlobalActionFilterAttribute = new GlobalActionFilterAttribute(); } }