Web API 之数据验证与单元测试

来源:编程玩家

cnblogs.com/Erik_Xu/p/5655520.html

一、模型状态 - 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可参考我上一篇文章

《帮助页面或用户手册(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//", 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().Any() ||

actionContext.ControllerContext.ControllerDescriptor.GetCustomAttributes().Any();

if (passby)

{

return;

}

if (actionContext.ModelState.IsValid == false)

{

actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, actionContext.ModelState);

}

}

}

五、单元测试

我使用BDD的风格编写单元测试,关于BDD的详细信息,可查看我之前的文章《行为驱动开发(BDD)实践示例》(http://www.cnblogs.com/Erik_Xu/p/5297981.html)。

对于全局数据验证,我设计了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()).Returns(new Collection(new[] { new BypassModelStateValidationAttribute() }));

HttpActionContext.ControllerContext.ControllerDescriptor = ControllerDescriptorMock.Object;

ControllerDescriptorMock.Setup(m => m.GetCustomAttributes()).Returns(new Collection());

}

[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()).Returns(new Collection());

HttpActionContext.ControllerContext.ControllerDescriptor = ControllerDescriptorMock.Object;

ControllerDescriptorMock.Setup(m => m.GetCustomAttributes()).Returns(new Collection());

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 ActionDescriptorMock = new Mock();

protected readonly Mock ControllerDescriptorMock = new Mock();

protected HttpActionContext HttpActionContext;

protected GlobalActionFilterAttribute GlobalActionFilterAttribute;

public GlobalActionFilterAttributeTests()

{

HttpActionContext = ContextUtil.CreateActionContext();

GlobalActionFilterAttribute = new GlobalActionFilterAttribute();

}

}

源码下载 :https://github.com/ErikXu/WebApi.Trial

看完本文有收获?请转发分享给更多人

关注「DotNet」,提升.Net技能

淘口令:复制以下红色内容,再打开手淘即可购买

范品社,使用¥极客T恤¥抢先预览(长按复制整段文案,打开手机淘宝即可进入活动内容)

  • 发表于:
  • 原文链接http://kuaibao.qq.com/s/20180302B0IQSO00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。

扫码关注云+社区

领取腾讯云代金券