在對FluentValidation有了初步的了解之後,也撰寫了InsertProductModel的驗證程式,並且透過單元測試,我們可以確認我們撰寫的驗證邏輯是沒有錯誤的,那麼我們在今天的分享之中,就要和大家一起來將FluentValidation整合到Api之中,讓Post到Controller的資料可以直接使用FluentValidation來進行驗證。
※與Api整合
在WebSite專案中使用Nuget加入FluentValidation.MVC4
在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;
}
}
在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);
}
}
記得在Global.asax中啟用它
FluentValidationConfig.Initialize();
這麼一來我們就完成了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);
}
}
我們故意將CategoryId設為0,重新Post一次資料,可以看到API已經可以吐回FluentValidation的錯誤訊息囉!
※使用ActionFilter統一驗證處理方法
整合了FluentValidation之後,你可以看到使用起來就跟Asp.Net MVC原本提供的機制一模一樣,但如果我們要在所有Api提供的方法中都對輸入資料進行驗證的話,是不是就會產生大量重複的程式碼,又如果萬一某天需要修改驗證回應資訊的格式,或是有某些人回傳的訊息格式定義不一樣,是不是有可能有更多問題呢? 所以像這種幾乎大家的使用方法都一樣的流程,我們可以透過擴充Asp.Net MVC的ActionFilter來處理這樣的邏輯,讓有需要進行驗證的方法,只要在開頭加上一個Attribute,系統就會幫它處理掉其他的工作囉!
在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
};
}
}
}
改寫Controller,將原本驗證的邏輯移除,改為增加ValidateRequestEntity到函式上方
[HttpPost]
[ValidateRequestEntity]
public ActionResult Create(InsertProductModel product)
{
this.ProductService.InsertProduct(product);
return Json(ApiStatusEnum.Success.ToString());
}
重新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來符合需求的變更。
在Utility建立Extentions.Text專案,並使用nuget加入需要的package
新增驗證輸入資料功能.feature,描述測試的功能
#language: zh-TW
功能: 驗證輸入資料功能
提供給 UI層
當系統傳入資料時,若驗證失敗傳回錯誤訊息,驗證成功則繼續進行Action
撰寫測試案例
場景: 驗證失敗時,回傳驗證失敗訊息
假設 使用者輸入資料驗證失敗
當 觸發驗證使用者傳入資料時
那麼 回傳驗證失敗訊息
場景: 驗證成功時,繼續執行Action
假設 使用者輸入資料驗證成功
當 觸發驗證使用者傳入資料時
那麼 繼續執行Action
完成測試
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);
}
執行測試
※本日小結
將FluentValidation整合到Asp.Net MVC之後,不但可以使用原有熟悉的方式進行資料驗證,還透過撰寫自訂的ActionFilter來讓驗證可以更輕鬆的套用,除此之外,由於我們是透過Autofac來綁定Model和Validator之間的關聯,因此隨時都可以輕鬆的替換驗證邏輯,讓我們的程式碼更加具有彈性!關於今天的內容,歡迎大家一起討論喔^_^