愛流浪的小風

技術隨手寫

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

| Comments

在對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等),不但可以快速套用到需要的程式碼上,也可以讓每一個程式碼只包含它所需要的邏輯,降低閱讀時的雜訊

延伸閱讀:

撰寫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之間的關聯,因此隨時都可以輕鬆的替換驗證邏輯,讓我們的程式碼更加具有彈性!關於今天的內容,歡迎大家一起討論喔^_^

Comments

comments powered by Disqus