1、模型狀態 - ModelState
2、數據註解 - Data Annotations
3、自定義數據註解
4、全局數據驗證
5、單元測試
我理解的ModelState是微軟在ASP.NET MVC中提出的一種新機制,它主要實現如下幾個功能:html
1. 保存客戶端傳過來的數據,若是驗證不經過,把數據返回到客戶端,這樣能夠保存用戶輸入,不須要從新輸入。前端
2. 驗證數據,以及保存數據對應的錯誤信息。jquery
3. 微軟的一種DRY(Don't Repeat Yourself)設計,經過ModelState能夠作服務端驗證,同時能夠配合jquery validation生成前端數據驗證。git
可是在Web API裏面,ModelState的主要功能就只剩下第2點了。github
須要注意的是,ModelState通常只作輸入驗證,一些其餘的業務驗證還有要在特定的地方進行處理。web
數據註解能夠理解爲驗證數據的邏輯或方法,微軟自己有提供一批數據註解,固然咱們也能夠自定義數據註解,如下是微軟提供的常見的數據註解:正則表達式
1. Required - 非空驗證。express
當一個輸入是null時會引起一個驗證錯誤。編程
當屬性類型是string的時候,若是設置了AllowEmptyStrings = false(默認爲false),那麼輸入空字符串或者空格,也會引起一個驗證錯誤。api
[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(); } }