我這麼玩Web Api(二):數據驗證,全局數據驗證與單元測試

目錄

1、模型狀態 - ModelState
2、數據註解 - Data Annotations
3、自定義數據註解
4、全局數據驗證
5、單元測試
 

1、模型狀態 - ModelState

  我理解的ModelState是微軟在ASP.NET MVC中提出的一種新機制,它主要實現如下幾個功能:html

  1. 保存客戶端傳過來的數據,若是驗證不經過,把數據返回到客戶端,這樣能夠保存用戶輸入,不須要從新輸入。前端

  2. 驗證數據,以及保存數據對應的錯誤信息。jquery

  3. 微軟的一種DRY(Don't Repeat Yourself)設計,經過ModelState能夠作服務端驗證,同時能夠配合jquery validation生成前端數據驗證。git

 

  可是在Web API裏面,ModelState的主要功能就只剩下第2點了。github

  須要注意的是,ModelState通常只作輸入驗證,一些其餘的業務驗證還有要在特定的地方進行處理。web

 

2、數據註解 - Data Annotations

  數據註解能夠理解爲驗證數據的邏輯或方法,微軟自己有提供一批數據註解,固然咱們也能夠自定義數據註解,如下是微軟提供的常見的數據註解:正則表達式

  1. Required - 非空驗證。express

  當一個輸入是null時會引起一個驗證錯誤。編程

  當屬性類型是string的時候,若是設置了AllowEmptyStrings = false(默認爲false),那麼輸入空字符串或者空格,也會引起一個驗證錯誤。api

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

 

3、自定義數據註解

  若是以爲微軟提供的數據註解不夠用,也能夠本身寫數據註解,只須要繼承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)》

 

4、全局數據驗證

  咱們在使用數據驗證的時候,每每會出現許多重複的代碼,以下圖:

  

  有沒有辦法減小這些重複的代碼呢?我從「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);
            }
        }
    }

 

5、單元測試

  我使用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

相關文章
相關標籤/搜索