FluentValidation 模型驗證

FluentValidation 是 .NET 下的模型驗證組件,和 ASP.NET MVC 基於Attribute 聲明式驗證的不一樣處,其利用表達式語法鏈式編程,使得驗證組件與實體分開。正如 FluentValidation 的 介紹:express

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/ide

想體驗 Lambda Expression 流暢的感受嗎,下面 let's go!函數

首先,你須要經過 NuGet 獲取 FluentValidation、FluentValidation.MVC3 包,我當前使用的版本以下:post

<?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(屬性)。ui

 
/// <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。this

 
[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 文件中的代碼,我就用截圖了):spa

 

翻譯得很差,請多多包涵!從這裏下載

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
{
    
}
相關文章
相關標籤/搜索