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(); } }