我这么玩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

原文发布于微信公众号 - 我为Net狂(dotNetCrazy)

原文发表时间:2016-07-19

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏菩提树下的杨过

silverlight动态读取txt文件/解析json数据/调用wcf示例

终于开始正式学习silverlight,虽然有点晚,但总算开始了,今天看了一下sdk,主要是想看下silverlight中如何动态调用数据,对于数据库的访问,s...

246100
来自专栏闵开慧

Mapreduce任务实现邮件监控

Mapreduce任务实现邮件监控     这里主要使用Java自带邮件类实现Mapreduce任务的监控,如果Mapreduce任务报错则发送报错邮件。Map...

33880
来自专栏Java架构师历程

【datatable】Cannot read property ‘style’ of undefined问题解决

遇到这个问题的时候一开始我以为是引入的js有问题,后来研究了源码之后原来是datatable的列的数量和表头列的数量没有对齐,表头我定义的列是这样的

38010
来自专栏Java学习网

常见的 Java 错误及避免方法之第五集(每集10个错误后续持续发布)

当输入期间意外终止文件或流时,将抛出“EOFException”。 以下是抛出EOFException异常的一个示例,来自JavaBeat应用程序:

16630
来自专栏编码小白

tomcat请求处理分析(一) 启动container实例

1.1.1  启动container实例 其主要是进行了生命周期中一系列的操作之后调用StandardEngine中的 startInternal方法,不难看出...

38760
来自专栏上善若水

021android初级篇之android的Context

021android初级篇之Android注解支持(Support Annotations)

12130
来自专栏Flutter入门

Weex是如何在Android客户端上跑起来的

Weex可以通过自己设计的DSL,书写.we文件或者.vue文件来开发界面,整个页面书写分成了3段,template、style、script,借鉴了成熟的MV...

49350
来自专栏程序猿DD

Spring框架中的设计模式(四)​

本文是Spring框架中使用的设计模式第四篇。本文将在此呈现出新的3种模式。一开始,我们会讨论2种结构模式:适配器和装饰器。在第三部分和最后一部分,我们将讨论单...

40460
来自专栏Android 开发学习

JsBridge 源码分析

19330
来自专栏大内老A

Enterprise Library深入解析与灵活应用(6):自己动手创建迷你版AOP框架

基于Enterprise Library PIAB的AOP框架已经在公司项目开发中得到广泛的使用,但是最近同事维护一个老的项目,使用到了Enterprise L...

21480

扫码关注云+社区

领取腾讯云代金券