目录

一、模型状态 - 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()]
public string Symbol { get; set; } [StringLength(, MinimumLength = )]
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(, )]
public double OpenPrice { get; set; } [Range(typeof(double), "", "")]
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()]
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

我这么玩Web Api(二):数据验证,全局数据验证与单元测试的更多相关文章

  1. [转]ASP.NET Web API(三):安全验证之使用摘要认证(digest authentication)

    本文转自:http://www.cnblogs.com/parry/p/ASPNET_MVC_Web_API_digest_authentication.html 在前一篇文章中,主要讨论了使用HTT ...

  2. ASP.NET Web API(三):安全验证之使用摘要认证(digest authentication)

    在前一篇文章中,主要讨论了使用HTTP基本认证的方法,因为HTTP基本认证的方式决定了它在安全性方面存在很大的问题,所以接下来看看另一种验证的方式:digest authentication,即摘要认 ...

  3. 【ASP.NET Web API教程】6.4 模型验证

    本文是Web API系列教程的第6.4小节 6.4 Model Validation 6.4 模型验证 摘自:http://www.asp.net/web-api/overview/formats-a ...

  4. 购物车Demo,前端使用AngularJS,后端使用ASP.NET Web API(3)--Idetity,OWIN前后端验证

    原文:购物车Demo,前端使用AngularJS,后端使用ASP.NET Web API(3)--Idetity,OWIN前后端验证 chsakell分享了前端使用AngularJS,后端使用ASP. ...

  5. 利用查询条件对象,在Asp.net Web API中实现对业务数据的分页查询处理

    在Asp.net Web API中,对业务数据的分页查询处理是一个非常常见的接口,我们需要在查询条件对象中,定义好相应业务的查询参数,排序信息,请求记录数和每页大小信息等内容,根据这些查询信息,我们在 ...

  6. ASP.NET Web API 2基于令牌的身份验证

    基于令牌的认证 我们知道WEB网站的身份验证一般通过session或者cookie完成的,登录成功后客户端发送的任何请求都带上cookie,服务端根据客户端发送来的cookie来识别用户. WEB A ...

  7. 创建支持CRUD(增删改查)操作的Web API(二)

    一:准备工作 你可以直接下载源码查看 Download the completed project.     下载完整的项目 CRUD是指“创建(C).读取(R).更新(U)和删除(D)”,它们是四个 ...

  8. web api 二

    接着上一回说,上回说到,web api有几种访问方式,具体有几种,我还真没去研究过,但是这里打算从get.post.put.delete四种请求方式分别谈谈基础类型(包括int/string/date ...

  9. Asp.Net Core 3.1 学习3、Web Api 中基于JWT的token验证及Swagger使用

    1.初始JWT 1.1.JWT原理 JWT(JSON Web Token)是目前最流行的跨域身份验证解决方案,他的优势就在于服务器不用存token便于分布式开发,给APP提供数据用于前后端分离的项目. ...

随机推荐

  1. Linux 内核概述 - Linux Kernel

    Linux 内核学习笔记整理. Unix unix 已有40历史,但计算机科学家仍认为其是现存操作系统中最大和最优秀的系统,它已成为一种传奇的存在,历经时间的考验却依然声名不坠. 1973 年,在用 ...

  2. ES5对Array增强的9个API

    为了更方便的对Array进行操作,ES5规范在Array的原型上新增了9个方法,分别是forEach.filter.map.reduce.reduceRight.some.every.indexOf ...

  3. ASP.NET Core 折腾笔记一

    前言: 在ASP.NET Core 1.0时,曾折腾过一次,后因发现不了System.Data而停止. 更因VS2015提示过期Delete掉VS了,其实主要还是笔记本的硬盘空间吃紧. 快双十一了,本 ...

  4. C# 破解 Reflector8.5

    一.分析 破解.net .dll,可以使用reflector,但官方提供的reflector是需要购买的,因此,破解reflector势在必行. 二.破解Reflector具体步骤 下面为详细的破解步 ...

  5. CI Weekly #10 | 2017 DevOps 趋势预测

    2016 年的最后几个工作日,我们对 flow.ci Android & iOS 项目做了一些优化与修复: iOS 镜像 cocoapods 版本更新: fir iOS上传插件时间问题修复: ...

  6. 冒泡,setinterval,背景图的div绑定事件,匿名函数问题

    1.会冒泡到兄弟元素么? $(function(){ $("#a").click(function(){alert("a")}) $("#b" ...

  7. css中的浮动与三种清除浮动的方法

    说到浮动之前,先说一下CSS中margin属性的两种特殊现象 1, 外边距的合并现象: 如果两个div上下排序,给上面一个div设置margin-bottom,给下面一个div设置margin-top ...

  8. Asp.Net Core + Dapper + Repository 模式 + TDD 学习笔记

    0x00 前言 之前一直使用的是 EF ,做了一个简单的小项目后发现 EF 的表现并不是很好,就比如联表查询,因为现在的 EF Core 也没有啥好用的分析工具,所以也不知道该怎么写 Linq 生成出 ...

  9. 计算Div标签内Checkbox个数或已被disabled的个数

    先看下面的html: 计算div内的checkbox个数:$('#divmod input[type="checkbox"]').length 计算div内checkbox被dis ...

  10. 微信小程序体验(1):携程酒店机票火车票

    在 12 月 28 日微信公开课上,张小龙对微信小程序的形态进行了阐释,小程序有四个特定:无需安装.触手可及.用完即走.无需卸载. 由于携程这种订酒店.火车票和机票等工具性质非常强的服务,非常符合张小 ...