我这么玩Web Api(二)
数据验证,全局数据验证与单元测试
目录
一、模型状态 - 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,用于限制属性的单词个数。
View Code
[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();
}
}
源码下载
https://github.com/ErikXu/WebApi.Trial
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- Android中AndroidStudio&Kotlin安装到运行过程及常见问题汇总
- AndroidStudio3.6的卸载安装,Gradle持续下载/Gradle Build失败等问题
- 搭建DNSmasq简单教程带WEB管理面板
- Android Studio报错unable to access android sdk add-on list解决方案
- 自建的纯净dns服务拦截部分广告,南方推荐
- Nginx安装lua-nginx-module模块
- IPinfo 多接口IP查询工具源码
- 教你CentOS7下如何更换内核安装BBR加速
- we-extract解析和采集微信公众号文章的账号及内容必备工具
- android九宫格可分页加载控件使用详解
- WordPress用插件实现MarkDown语法支持
- Android中实现长按照片弹出右键菜单功能的实例代码
- Android Studio无法执行Java类的main方法问题及解决方法
- PlayTube优秀的视频CMS系统/支持本地和youtube导入
- Android Studio 中运行 groovy 程序的方法图文详解