iT邦幫忙

DAY 12
2

使用Asp.Net MVC打造Web Api系列 第 12

使用Asp.Net MVC打造Web Api (12) - 整合FluentValidation到Api中

在對FluentValidation有了初步的了解之後,也撰寫了InsertProductModel的驗證程式,並且透過單元測試,我們可以確認我們撰寫的驗證邏輯是沒有錯誤的,那麼我們在今天的分享之中,就要和大家一起來將FluentValidation整合到Api之中,讓Post到Controller的資料可以直接使用FluentValidation來進行驗證。
※與Api整合

  1. 在WebSite專案中使用Nuget加入FluentValidation.MVC4

  2. 在Utlity建立Extensions專案,新增ModelValidatorFactorycs,這支程式是用來整合DI Framework與FluentValidation,透過DI Framework提供FluentValidation所需要的Validator,如此一來如果我們需要動態更換Validator就不是一件難事了。

        public class ModelValidatorFactory : ValidatorFactoryBase
        {        
            public override IValidator CreateInstance(Type validatorType)
            {
                IValidator validator = DependencyResolver.Current.GetService(validatorType) as IValidator;
    
                return validator;
            }
        }
    
  3. 在WebSite的App_Start新增FluentValidationConfig.cs,註冊FluentValidation到MVC的ModelValidateProviders中

        public class FluentValidationConfig
        {
            public static void Initialize()
            {
                var container = AutofacDependencyResolver.Current.ApplicationContainer as IContainer;
    
                var fluentValidationModelValidatorProvider = new FluentValidationModelValidatorProvider(new ModelValidatorFactory());
                DataAnnotationsModelValidatorProvider.AddImplicitRequiredAttributeForValueTypes = false;
                fluentValidationModelValidatorProvider.AddImplicitRequiredValidator = false;
                ModelValidatorProviders.Providers.Add(fluentValidationModelValidatorProvider);
            }
        }
    
  4. 記得在Global.asax中啟用它

        FluentValidationConfig.Initialize();
    
  5. 這麼一來我們就完成了FluentValidation的整合,可以直接在Controller當中使用和原本一樣的方法來檢查Model是否正確,並且吐回錯誤訊息

        [HttpPost]
        public ActionResult Create(InsertProductModel product)
        {
            if (this.ModelState.IsValid)
            {
                this.ProductService.InsertProduct(product);
    
                return Json(ApiStatusEnum.Success.ToString());
            }
            else
            {
                string messages = string.Join("; ", this.ModelState.Values
                                                        .SelectMany(x => x.Errors)
                                                        .Select(x => x.ErrorMessage));
    
                return Json(messages);
            }
        }
    
  6. 我們故意將CategoryId設為0,重新Post一次資料,可以看到API已經可以吐回FluentValidation的錯誤訊息囉!

※使用ActionFilter統一驗證處理方法
整合了FluentValidation之後,你可以看到使用起來就跟Asp.Net MVC原本提供的機制一模一樣,但如果我們要在所有Api提供的方法中都對輸入資料進行驗證的話,是不是就會產生大量重複的程式碼,又如果萬一某天需要修改驗證回應資訊的格式,或是有某些人回傳的訊息格式定義不一樣,是不是有可能有更多問題呢? 所以像這種幾乎大家的使用方法都一樣的流程,我們可以透過擴充Asp.Net MVC的ActionFilter來處理這樣的邏輯,讓有需要進行驗證的方法,只要在開頭加上一個Attribute,系統就會幫它處理掉其他的工作囉!

  1. 在Extensions新增ValidateRequestEntityAttribute,在Asp.Net MVC執行Action之前,先檢查輸入資料能否通過驗證,若不行就吐回錯誤訊息

        public class ValidateRequestEntityAttribute : ActionFilterAttribute
        {
            public override void OnActionExecuting(ActionExecutingContext filterContext)
            {
                var modelState = filterContext.Controller.ViewData.ModelState;
                if (!filterContext.Controller.ViewData.ModelState.IsValid)
                {
                    string errorMessages = string.Join("; ", modelState.Values
                                                                  .SelectMany(x => x.Errors)
                                                                  .Select(x => x.ErrorMessage));                    
    
                    filterContext.Result = new JsonResult()
                    {
                        Data=errorMessages,
                        ContentEncoding = Encoding.UTF8,
                        JsonRequestBehavior = JsonRequestBehavior.AllowGet
                    };
                }
            }
        }
    
  2. 改寫Controller,將原本驗證的邏輯移除,改為增加ValidateRequestEntity到函式上方

        [HttpPost]
        [ValidateRequestEntity]
        public ActionResult Create(InsertProductModel product)
        {
            this.ProductService.InsertProduct(product);
    
            return Json(ApiStatusEnum.Success.ToString());
        }
    
  3. 重新Post資料,發現驗證一樣有效,而且我們統一了回傳訊息格式

※什麼是ActionFilter?
Asp.Net MVC所提供的ActionFilter其實是一種Aop的實作模式,取代以往直接在程式碼中呼叫,它透過DataAnnotation的方式標記在Class或Function上頭,而Asp.Net MVC會在初始化或執行時期,根據DataAnnotation所標註的內容,執行對應的指令(例如上面的例子就是在執行Action之前,先檢查輸入資料是否符合驗證)

Action Filter可以將一些常用、通用的邏輯獨立出來並封裝(例如: Log、權限和Cache等),不但可以快速套用到需要的程式碼上,也可以讓每一個程式碼只包含它所需要的邏輯,降低閱讀時的雜訊

延伸閱讀:
* Understanding Action Filters
* AOP 觀念與術語

※撰寫ActionFilter的單元測試
因為ActionFilter可以讓所有需要的Controller都能夠套用,因此確保執行正確無誤也是很重要,所以接下來將對ActionFilter進行單元測試,有了單元測試我們也可以放心的隨時改寫ActionFilter來符合需求的變更。

  1. 在Utility建立Extentions.Text專案,並使用nuget加入需要的package

  2. 新增驗證輸入資料功能.feature,描述測試的功能

    	#language: zh-TW
    	功能: 驗證輸入資料功能
    		提供給 UI層
    		當系統傳入資料時,若驗證失敗傳回錯誤訊息,驗證成功則繼續進行Action
    
  3. 撰寫測試案例

    	場景: 驗證失敗時,回傳驗證失敗訊息
    		假設 使用者輸入資料驗證失敗
    		當 觸發驗證使用者傳入資料時
    		那麼 回傳驗證失敗訊息
    
    	場景: 驗證成功時,繼續執行Action
    		假設 使用者輸入資料驗證成功
    		當 觸發驗證使用者傳入資料時
    		那麼 繼續執行Action
    
  4. 完成測試

        private ActionExecutingContext context;
    
        [Given(@"使用者輸入資料驗證失敗")]
        public void 假設使用者輸入資料驗證失敗()
        {
            HttpContextBase httpContext = MockRepository.GenerateStub<HttpContextBase>();
            ControllerBase controller = MockRepository.GenerateStub<ControllerBase>();
            controller.ViewData = new ViewDataDictionary();
            controller.ViewData.ModelState.AddModelError("Error", "Error");
    
            ControllerContext controllerContext = new ControllerContext(httpContext, new RouteData(), controller);
    
            this.context = new ActionExecutingContext(controllerContext, MockRepository.GenerateStub<ActionDescriptor>(), new Dictionary<string, object>());
        }
    
        [Given(@"使用者輸入資料驗證成功")]
        public void 假設使用者輸入資料驗證成功()
        {
            HttpContextBase httpContext = MockRepository.GenerateStub<HttpContextBase>();
            ControllerBase controller = MockRepository.GenerateStub<ControllerBase>();
            controller.ViewData = new ViewDataDictionary();
    
            ControllerContext controllerContext = new ControllerContext(httpContext, new RouteData(), controller);
    
            this.context = new ActionExecutingContext(controllerContext, MockRepository.GenerateStub<ActionDescriptor>(), new Dictionary<string, object>());
        }
    
        [When(@"觸發驗證使用者傳入資料時")]
        public void 當觸發驗證使用者傳入資料時()
        {
            ValidateRequestEntityAttribute attribute = new ValidateRequestEntityAttribute();
            attribute.OnActionExecuting(this.context);
        }
    
        [Then(@"回傳驗證失敗訊息")]
        public void 那麼回傳驗證失敗訊息()
        {
            Assert.IsFalse(this.context.Controller.ViewData.ModelState.IsValid);
            Assert.IsNotNull(this.context.Result);
        }        
    
        [Then(@"繼續執行Action")]
        public void 那麼繼續執行Action()
        {
            Assert.IsTrue(this.context.Controller.ViewData.ModelState.IsValid);
            Assert.IsNull(this.context.Result);
        }
    
  5. 執行測試

※本日小結
將FluentValidation整合到Asp.Net MVC之後,不但可以使用原有熟悉的方式進行資料驗證,還透過撰寫自訂的ActionFilter來讓驗證可以更輕鬆的套用,除此之外,由於我們是透過Autofac來綁定Model和Validator之間的關聯,因此隨時都可以輕鬆的替換驗證邏輯,讓我們的程式碼更加具有彈性!關於今天的內容,歡迎大家一起討論喔^_^


上一篇
使用Asp.Net MVC打造Web Api (11) - 使用FluentValidation進行驗證
下一篇
使用Asp.Net MVC打造Web Api (13) - 使用Json.Net解析Json
系列文
使用Asp.Net MVC打造Web Api30

尚未有邦友留言

立即登入留言