模型驗證組件 FluentValidation
FluentValidation 是 .NET 下的模型驗證組件,和 ASP.NET MVC 基於Attribute 聲明式驗證的不同處,其利用表達式語法鏈式編程,使得驗證組件與實體分開。正如 FluentValidation 的 介紹:
A small validation library for .NET that uses a fluent interface and lambda expressions for building validation rules for your business objects.
使用後,只能用一句話來形容:真乃神器也!
項目地址:http://fluentvalidation.codeplex.com/
想體驗 Lambda Expression 流暢的感覺嗎,下面 let‘s go!
首先,你需要通過 NuGet 獲取 FluentValidation、FluentValidation.MVC3 包,我當前使用的版本如下:
<?xml version="1.0" encoding="utf-8"?> <packages> <package id="FluentValidation" version="3.3.1.0" /> <package id="FluentValidation.MVC3" version="3.3.1.0" /> </packages>
快速入門
1. 建立模型類
為了演示,我這裏建了一個 Person 類,並且假設有下面這些 Property(屬性)。
/// <summary> /// 個人 /// </summary> public class Person { /// <summary> /// 姓 /// </summary> public string Surname { get; set; } /// <summary> /// 名 /// </summary> public string Forename { get; set; } /// <summary> /// 公司 /// </summary> public string Company { get; set; } /// <summary> /// 地址 /// </summary> public string Address { get; set; } /// <summary> /// 郵政編碼 /// </summary> public string Postcode { get; set; } /// <summary> /// 個人空間的地址的別名,比如:bruce-liu-cnblogs、cnblogs_bruce_liu /// </summary> public string UserZoneUrl { get; set; } }
根據 FluentValidation 的使用方法,我們直接可以在 Person 類上面直接標記對應的 Validator,比如: [Validator(typeof(PersonValidator))]。但如果我們的模型層(Model Layer)不允許修改(假設),並且你像我一樣喜歡幹凈的模型層,不想要標記太多業務型的 Attribute 時,我們就使用繼承的方式來標記,在派生類上標記。下面我們建一個 Customer 類,繼承自 Person 類,並且再增加 2 個 Property(屬性),最後標記 Validator Attribute。
[Validator(typeof(CustomerValidator))] public class Customer : Person { /// <summary> /// 是否有折扣 /// </summary> public bool HasDiscount { get; set; } /// <summary> /// 折扣 /// </summary> public float Discount { get; set; } }
2. 建立模型類相應的 FluentValidation 驗證類
public class CustomerValidator : AbstractValidator<Customer> { public CustomerValidator() { // 在這裏寫驗證規則,比如: // Cascade(FluentValidation.CascadeMode.StopOnFirstFailure) 可以指定當前 CustomerValidator 的驗證模式,可重寫全局驗證模式 RuleFor(customer => customer.Surname).Cascade(FluentValidation.CascadeMode.StopOnFirstFailure).NotEmpty().Length(3, int.MaxValue).WithLocalizedName(() => "姓").WithLocalizedMessage(() => "親,{PropertyName}不能為空字符串,並且長度大於{0}!!!"); // 更多... // 更多... } }
3. 在 Global.asax 裏面的 Application_Start 中配置 FluentValidation
默認情況下,FluentValidation 使用的驗證錯誤消息是英文的,且官方自帶的語言包中沒有中文,於是我自己就手動翻譯,建立了一個資源文件 FluentValidationResource.resx,並且在 Global.asax 中配置。
protected void Application_Start() { ConfigureFluentValidation(); } protected void ConfigureFluentValidation() { // 設置 FluentValidation 默認的資源文件提供程序 - 中文資源 ValidatorOptions.ResourceProviderType = typeof(FluentValidationResource); /* 比如驗證用戶名 not null、not empty、length(2,int.MaxValue) 時,鏈式驗證時,如果第一個驗證失敗,則停止驗證 */ ValidatorOptions.CascadeMode = CascadeMode.StopOnFirstFailure; // ValidatorOptions.CascadeMode 默認值為:CascadeMode.Continue // 配置 FluentValidation 模型驗證為默認的 ASP.NET MVC 模型驗證 FluentValidationModelValidatorProvider.Configure(); }
FluentValidationResource 代碼中的 Key-Value 如下(PS:由於不知道怎麽貼 Resource 文件中的代碼,我就用截圖了):
翻譯得不好,請多多包涵!從這裏下載
4. 客戶端調用
本來用控制臺程序就可以調用的,由於筆者建立的項目是 ASP.NET MVC 項目,本文的重點也是 FluentValidation 在 ASP.NET MVC 中使用,於是就在 Action 裏面驗證了。在 HomeController 的 Index 方法裏面的代碼如下:
public ActionResult Index() { /* 下面的例子驗證 FluentValidation 在 .net 中的使用,非特定與 ASP.NET MVC */ Customer customer = new Customer(); // 我們這裏直接 new 了一個 Customer 類,看看模型驗證能否通過 CustomerValidator validator = new CustomerValidator(); ValidationResult results = validator.Validate(customer); // 或者拋出異常 validator.ValidateAndThrow(customer); bool validationSucceeded = results.IsValid; IList<ValidationFailure> failures = results.Errors; StringBuilder textAppender = new StringBuilder(); if (!results.IsValid) { foreach (var failureItem in failures) { textAppender.Append("<br/>==========================================<br/>"); textAppender.AppendFormat("引起失敗的屬性值為:{0}<br/>", failureItem.AttemptedValue); textAppender.AppendFormat("被關聯的失敗狀態為:{0}<br/>", failureItem.CustomState); textAppender.AppendFormat("錯誤消息為:{0}<br/>", failureItem.ErrorMessage); textAppender.AppendFormat("Property(屬性)為:{0}<br/>", failureItem.PropertyName); textAppender.Append("<br/>==========================================<br/>"); } } ViewBag.Message = textAppender.ToString(); return View(); }
最後,運行就能看到效果!
進階篇
1. 屬性類(Property Class)的驗證
既然是顧客,那麽顧客就可能會有訂單,我們建立一個 Order 類,把 Customer 類作為 Order 類的一個 Property(屬性)。
/// <summary> /// 訂單 /// </summary> [Validator(typeof(OrderValidator))] public class Order { public Customer Customer { get; set; } /// <summary> /// 價格 /// </summary> public decimal Price { get; set; } }
相應的,我們還需要建立一個驗證類 OrderValidator。為了共用 CustomerValidator 類,我們需要在 OrderValidator 類的構造函數中,為 Order 類的 Customer 屬性指定 Validator。
/// <summary> /// 訂單驗證類 /// </summary> public class OrderValidator : AbstractValidator<Order> { public OrderValidator() { RuleFor(order => order.Price).NotNull().GreaterThanOrEqualTo(0m).WithLocalizedName(() => "價格"); // 重用 CustomerValidator RuleFor(order => order.Customer).SetValidator(new CustomerValidator()); } }
在 ASP.NET MVC 中使用時,在 Action 方法的參數上,可以像使用 Bind Attribute 一樣:
public ActionResult AddCustomer([Bind(Include = "Company", Exclude = "Address")]Customer customer)
使用 CustomizeValidator Attribute,來指定要驗證的 Property(屬性):
[HttpGet] public ActionResult AddCustomer() { return View(new Customer()); } [HttpPost] public ActionResult AddCustomer([CustomizeValidator(Properties="Surname,Forename")] Customer customer) { /* 在 Action 的參數上標記 CustomizeValidator 可以指定 Interceptor(攔截器)、Properties(要驗證的屬性,以逗號分隔)。 如果指定了 Properties (要驗證的屬性,以逗號分隔),請註意是否別的屬性有客戶端驗證,導致客戶端提交不了,而服務器端 又可以不用驗證。 */ if (!ModelState.IsValid) { return View(customer); } return Content("驗證通過"); }
由此可見,FluentValidation 真是用心良苦,這都想到了,不容易啊!
擴展篇
1. 完善 CustomerValidator
接下來,我們繼續 完善 CustomerValidator ,增加更多的驗證規則。
public class CustomerValidator : AbstractValidator<Customer> { public CustomerValidator() { // CascadeMode = CascadeMode.StopOnFirstFailure; 可以指定當前 CustomerValidator 的驗證模式,可重寫全局驗證模式 RuleFor(customer => customer.Surname).Cascade(FluentValidation.CascadeMode.StopOnFirstFailure).NotEmpty().Length(3, int.MaxValue).WithLocalizedName(() => "姓").WithLocalizedMessage(() => "親,{PropertyName}不能為空字符串,並且長度大於{0}!!!"); // 註意:調用 Cascade(FluentValidation.CascadeMode.StopOnFirstFailure) 表示當一個驗證條件失敗後,不再繼續驗證 RuleFor(customer => customer.Forename).NotEmpty().WithLocalizedName(() => "名").WithLocalizedMessage(() => "{PropertyName} 一定要不為空,Do you know ?"); RuleFor(customer => customer.Company).NotNull().WithLocalizedName(() => "公司名稱").WithMessage(string.Format("{{PropertyName}} 不能 \"{0}\",下次記住哦,{1}!", "為空", "呵呵")); RuleFor(customer => customer.Discount).NotEqual(0).WithLocalizedName(() => "折扣").When(customer => customer.HasDiscount); RuleFor(customer => customer.Address).Length(20, 250).WithLocalizedName(() => "地址").Matches("^[a-zA-Z]+$").WithLocalizedMessage(() => "地址的長度必須在 20 到 250 個字符之間,並且只能是英文字符!"); RuleFor(customer => customer.Postcode).Must(BeAValidPostcode).WithLocalizedName(() => "郵政編碼").WithMessage("請指定一個合法的郵政編碼"); // 註意:如果用了 Must 驗證方法,則沒有客戶端驗證。 Custom((customer, validationContext) => { bool flag1 = customer.HasDiscount; bool flag2 = !validationContext.IsChildContext; return flag1 && flag2 && customer.Discount > 0 ? null : new ValidationFailure("Discount", "折扣錯誤", customer.Discount); }); } /// <summary> /// 檢查是否是合法的郵政編碼 /// </summary> /// <param name="postcode"></param> /// <returns></returns> private bool BeAValidPostcode(string postcode) { if (!string.IsNullOrEmpty(postcode) && postcode.Length == 6) { return true; } return false; } }
當我想要給 Customer.UserZoneUrl(個人空間的地址的別名) 寫驗證規則的時候,我發現它的驗證規則可以提取出來,方便下次有類似的功能需要用到。那能不能像調用 NotNull() 、NoEmpty() 方法那樣,調用我們寫的 EntryName() 呢?答案:當然可以!
這樣調用怎麽樣?
RuleFor(customer => customer.UserZoneUrl).EntryName();
其中 EntryName() 是一個擴展方法。
using FluentValidation; public static class FluentValidatorExtensions { public static IRuleBuilderOptions<T, string> EntryName<T>(this IRuleBuilder<T, string> ruleBuilder) { return ruleBuilder.SetValidator(new EntryNameValidator()); } }
我們看到,調用 EntryName 擴展方法其實是調用另外一個 Validator - EntryNameValidator。
public class EntryNameValidator : PropertyValidator, IRegularExpressionValidator { private readonly Regex regex; const string expression = @"^[a-zA-Z0-9][\w-_]{1,149}$"; public EntryNameValidator() : base(() => ExtensionResource.EntryName_Error) { regex = new Regex(expression, RegexOptions.IgnoreCase); } protected override bool IsValid(PropertyValidatorContext context) { if (context.PropertyValue == null) return true; if (!regex.IsMatch((string)context.PropertyValue)) { return false; } return true; } public string Expression { get { return expression; } } }
這裏我們的 EntryNameValidator 除了繼承自 PropertyValidator,還實現了 IRegularExpressionValidator 接口。為什麽要實現 IRegularExpressionValidator 接口 呢?是因為可以共享由 FluentValidation 帶來的好處,比如:客戶端驗證等等。
其中 ExtensionResource 是一個資源文件,我用來擴展 FluentValidation 時使用的資源文件。
2. 復雜驗證
下面我們再建立一個 Pet(寵物)類,為 Customer 類增加一個 public List<Pet> Pets { get; set; } 屬性。
/// <summary> /// 顧客類 /// </summary> [Validator(typeof(CustomerValidator))] public class Customer : Person { /// <summary> /// 是否有折扣 /// </summary> public bool HasDiscount { get; set; } /// <summary> /// 折扣 /// </summary> public float Discount { get; set; } /// <summary> /// 一個或多個寵物 /// </summary> public List<Pet> Pets { get; set; } } /// <summary> /// 寵物類 /// </summary> public class Pet { public string Name { get; set; } }
那 FluentValidation 對集合的驗證,該如何驗證呢?下面我們要求顧客的寵物不能超過 10 個。你一定想到了用下面的代碼實現:
Custom(customer => { return customer.Pets.Count >= 10 ? new ValidationFailure("Pets", "不能操作 10 個元素") : null; });
或者我們寫一個自定義的 Property(屬性)驗證器 ListMustContainFewerThanTenItemsValidator<T>,讓它繼承自 PropertyValidator
public class ListMustContainFewerThanTenItemsValidator<T> : PropertyValidator { public ListMustContainFewerThanTenItemsValidator() : base("屬性 {PropertyName} 不能超過 10 個元素!") { // 註意:這裏的錯誤消息也可以用資源文件 } protected override bool IsValid(PropertyValidatorContext context) { var list = context.PropertyValue as IList<T>; if (list != null && list.Count >= 10) { return false; } return true; } }
應用這個屬性驗證器就很容易了,在 Customer 的構造函數中:
RuleFor(customer => customer.Pets).SetValidator(new ListMustContainFewerThanTenItemsValidator<Pet>());
再或者為了公用,寫一個擴展方法,擴展 IRuleBuilder<T, IList<TElement>> 類
/// <summary> /// 定義擴展方法,是為了方便調用。 /// </summary> public static class MyValidatorExtensions { public static IRuleBuilderOptions<T, IList<TElement>> MustContainFewerThanTenItems<T, TElement>(this IRuleBuilder<T, IList<TElement>> ruleBuilder) { return ruleBuilder.SetValidator(new ListMustContainFewerThanTenItemsValidator<TElement>()); } }
調用也像上面調用 EntryName() 一樣,直接調用:
RuleFor(customer => customer.Pets).MustContainFewerThanTenItems();
3. 與 IoC 容器(Autofac、Unity、StructureMap等)集成
下面以 Autofac 為例進行演示
1. 創建自己的 ValidatorFactory
比如我這裏創建為 AutofacValidatorFactory,繼承自 FluentValidation.ValidatorFactoryBase,而 ValidatorFactoryBase 本身是實現了 IValidatorFactory 的。IValidatorFactory 的代碼如下:
// 摘要: // Gets validators for a particular type. public interface IValidatorFactory { // 摘要: // Gets the validator for the specified type. IValidator<T> GetValidator<T>(); // // 摘要: // Gets the validator for the specified type. IValidator GetValidator(Type type); }
ValidatorFactoryBase 的代碼如下:
public abstract class ValidatorFactoryBase : IValidatorFactory { protected ValidatorFactoryBase(); public abstract IValidator CreateInstance(Type validatorType); public IValidator<T> GetValidator<T>(); public IValidator GetValidator(Type type); }
我們看到 ValidatorFactoryBase 其實是把 IValidatorFactory 接口的 2 個方法給實現了,但核心部分還是抽象出來了,那我們的 AutofacValidatorFactory 需要根據 Autofac 的使用方法進行編碼,代碼如下:
public class AutofacValidatorFactory : ValidatorFactoryBase { private readonly IContainer _container; public AutofacValidatorFactory(IContainer container) { _container = container; } /// <summary> /// 嘗試創建實例,返回值為 NULL 表示不應用 FluentValidation 來做 MVC 的模型驗證 /// </summary> /// <param name="validatorType"></param> /// <returns></returns> public override IValidator CreateInstance(Type validatorType) { object instance; if (_container.TryResolve(validatorType, out instance)) { return instance as IValidator; } return null; } }
2. 在 Application_Start 中註冊 Autofac
protected void Application_Start() { RegisterAutofac(); } protected void RegisterAutofac() { // 註冊 IoC ContainerBuilder builder = new ContainerBuilder(); builder.RegisterNewsManagement(); // 創建 container IContainer _container = builder.Build(); // 在 NewsManagement 模型下設置 container _container.SetAsNewsManagementResolver(); ModelValidatorProviders.Providers.Add(new FluentValidationModelValidatorProvider(new AutofacValidatorFactory(_container))); DataAnnotationsModelValidatorProvider.AddImplicitRequiredAttributeForValueTypes = false; }
其中上面那 2 個方法(RegisterNewsManagement、SetAsNewsManagementResolver)是擴展方法,代碼如下:
public static class AutofacExtensions { public static void RegisterNewsManagement(this ContainerBuilder builder) { builder.RegisterType<NewsCategoryValidator>().As<IValidator<NewsCategoryModel>>(); builder.RegisterType<NewsValidator>().As<IValidator<NewsModel>>(); builder.RegisterControllers(typeof(MvcApplication).Assembly); } public static void SetAsNewsManagementResolver(this IContainer contaner) { DependencyResolver.SetResolver(new AutofacDependencyResolver(contaner)); } }
至此,我們的模型上面就可以註釋掉對應的 Attribute 了。
/// <summary> /// 文章表模型 /// </summary> //[Validator(typeof(NewsValidator))] public class NewsModel : NewsEntity { }
模型驗證組件 FluentValidation