• 我这么玩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,用于限制属性的单词个数。

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

  • 相关阅读:
    MIPS笔记
    花生壳动态域名解析工具原理
    SEE MIPS RUN 第六章 内存管理与TLB
    C/C++动态内存创建与内存管理
    ngclass 用法
    看了一个烟花的html作品 引用:http://www.w3cfuns.com/blog54440495404365.html
    ngclip angualr 的copy功能
    学习技术的方法
    restful restAPI 的定义方式
    我与计算机
  • 原文地址:https://www.cnblogs.com/Erik_Xu/p/5655520.html
Copyright © 2020-2023  润新知