【翻译】FluentValidation验证组件的使用
由于本文是翻译,所以将原文原原本本的搬上来,大家看原文有什么不懂的也可以对照这里。
给出地址:https://fluentvalidation.net/
FluentValidation
fluentvalidation是一款用于模型验证的组件,尤其是在asp.net core中,我们通常使用attribute的验证方式,比如[Required(ErrorMessage="xxxxx")]这种,还有很多。这个组件的好处是可以对模型的验证规则进行集中的管理,不会让模型的验证散落到程序的各个角落,所以是一个好东西,下面来介绍他的使用方法。
安装
最简单的安装方式是通过nuget包管理器或者通过dotnet cli命令。
nuget:Install-Package FluentValidation
.net core cli:dotnet add package FluentValidation
当我们在asp.net core中使用的时候,还要安装另外一个包:Install-Package FluentValidation.AspNetCore
创建第一个验证器(validator)
要创建一组针对某个模型的验证器,首先你要写一个继承了AbstractValidator<T>的类。T就代表一个需要被验证的模型类。
打个比方,首先假定你有如下一个类:
public class Customer { public int Id { get; set; } public string Surname { get; set; } public string Forename { get; set; } public decimal Discount { get; set; } public string Address { get; set; } }
你需要通过继承AbstractValidator<Customer>来定义一个验证Customer的类:
using FluentValidation; public class CustomerValidator : AbstractValidator<Customer> { }
验证类中的这些验证规则应该定义在验证类的Rule方法中,该方法必须被重写 。
要为一个类指定一组验证规则,需要调用RuleFor方法,为该方法传递一个lambda表达式,这个表达式指定了你想要验证的这个类中的某一个属性。打个比方,如果你不想让CUstomer类中的Surname为空,那么你的验证类看起来像这样:
using FluentValidation; public class CustomerValidator : AbstractValidator<Customer> { public CustomerValidator() { RuleFor(customer => customer.Surname).NotNull(); } }
重要的说明:像NotNull()这样的方法就是一个验证器,validator
要运行这个验证类,你需要实例化它并且调用validate方法,并将被验证的类对象传入:
Customer customer = new Customer(); CustomerValidator validator = new CustomerValidator(); ValidationResult result = validator.Validate(customer);
Validate方法返回一个ValidationResult 类型的对象,这个对象包含了两个属性:
- IsValid:一个布尔值,指明这个验证有没有通过。
- Errors:是一个包含了ValidationFailure 类型对象的一个集合,这个集合中包含了所有验证失败的细节。
下面的代码将每个错误在Console中写出来:
Customer customer = new Customer(); CustomerValidator validator = new CustomerValidator(); ValidationResult results = validator.Validate(customer); if(! results.IsValid) { foreach(var failure in results.Errors) { Console.WriteLine("Property " + failure.PropertyName + " failed validation. Error was: " + failure.ErrorMessage); } }
ValidationResult的ToString方法会将所有的错误结果都表示出来,默认情况下,没个错误都会另起一行,当然你也可以通过给ToString方法传入一些字符来自定义一个分隔符:
ValidationResult results = validator.Validate(customer); string allMessages = results.ToString("~"); // In this case, each message will be separated with a `~`
注意:如果没有错误发生(验证通过),那么ToString方法会返回一个空字符串。
支持链式调用
你可以在同一个被验证的属性上进行链式的调用:
using FluentValidation; public class CustomerValidator : AbstractValidator<Customer> { public CustomerValidator() RuleFor(customer => customer.Surname).NotNull().NotEqual("foo"); } }
可抛出异常
除了调用Validate,你还可以调用ValidateAndThrow,这会在验证失败后抛出异常。
Customer customer = new Customer(); CustomerValidator validator = new CustomerValidator(); validator.ValidateAndThrow(customer);
这个方法会抛出一个ValidationException异常,包含了Erros属性上的所有错误信息。ValidationException是一个扩展方法,请确保你引用了FluentValidation这个命名空间。
验证集合类型的属性
你可以通过RuleForEach这个方法来对一个集合属性上的所有成员实施一个相同的检测:
public class Person { public List<string> AddressLines {get;set;} = new List<string>(); } public class PersonValidator : AbstractValidator<Person> { public PersonValidator() { RuleForEach(x => x.AddressLines).NotNull(); } }
上面这个代码会对AddressLines这个属性上的每一项都实施一个非空的检测。
不过你可以通过Where方法对集合中的某几项元素的检测进行跳过,主要Where方法必须放在RuleForeach方法后:
RuleForEach(x => x.Orders)
.Where(x => x.Cost != null)
.SetValidator(new OrderValidator());
上面这段代码在我这里没有通过,没有Where这么一个方法,但是完全可以调用Orders的Where扩展方法来挑选元素,这个没什么我觉得。由于本文是翻译,所以将原文原原本本的搬上来,大家看原文有什么不懂的也可以对照这里。
复杂类型的属性
对于复杂类型的属性,定义的规则可以复用。打个比方你有两个类:
public class Customer { public string Name { get; set; } public Address Address { get; set; } } public class Address { public string Line1 { get; set; } public string Line2 { get; set; } public string Town { get; set; } public string County { get; set; } public string Postcode { get; set; } }
然后你定义了一个AddressValidator:
public class AddressValidator : AbstractValidator<Address> { public AddressValidator() { RuleFor(address => address.Postcode).NotNull(); //etc } }
如果你要对Customer类进行验证,那么你可以在CustomerVaidator上面复用AddressValidator这个验证规则:
public class CustomerValidator : AbstractValidator<Customer> { public CustomerValidator() { RuleFor(customer => customer.Name).NotNull(); RuleFor(customer => customer.Address).SetValidator(new AddressValidator()); } }
通过调用SetValidator这个方法来解决的。
所以如果你通过调用CustomerValidator对象的Validate方法时,会在两个验证类中执行验证的代码。
规则集允许你将验证规则分组在一起,这些规则可以作为一个组一起执行,而忽略其他规则:
打个比方,想像下在Person对象上的三个属性(Id,Surname,
Forename),我们可以将Surname and Forename这两个的验证规则放在一个叫做Names的组中:
public class PersonValidator : AbstractValidator<Person> { public PersonValidator() { RuleSet("Names", () => { RuleFor(x => x.Surname).NotNull(); RuleFor(x => x.Forename).NotNull(); }); RuleFor(x => x.Id).NotEqual(0); } }
现在,针对Surname和Forename这两个属性的验证规则被放在一个叫做“Names”的验证集合中,当我们调用Validate这个扩展方法(重载后的)时,我们可以为这个扩展方法的ruleSet命名参数传递RuleSet的名字,然后我们验证的时候只验证这个RuleSet:
var validator = new PersonValidator(); var person = new Person(); var result = validator.Validate(person, ruleSet: "Names");//一个重载的Validate方法。
这允许你把一个复杂的验证定义划分为许多细小的隔离的验证规则,如果你调用Validate方法时没有传递一个ruleSet,那么验证只会执行那些不在RuleSet中的规则。你可以通过逗号分隔来指定多个ruleset:
validator.Validate(person, ruleSet: "Names,MyRuleSet,SomeOtherRuleSet")
你也可以通过传递一个“default”字面量来指定那些没有在任何ruleset的规则:
validator.Validate(person, ruleSet: "default,MyRuleSet")
上面的方法会执行MyRuleSet中的规则和那些没有在任何RuleSet中的规则。
你也可以通过 “*”这个字面量来指定所有的规则:
validator.Validate(person, ruleSet: "*")
引入/包含规则(INCLUDE)
你可以包含来自其他验证器的规则,前提是它们验证相同的类型,这意味着你可以将同一个类型中的验证规则划分成几个validator:
public class PersonAgeValidator : AbstractValidator<Person> { public PersonAgeValidator() { RuleFor(x => x.DateOfBirth).Must(BeOver18); } protected bool BeOver18(DateTime date) { //... } } public class PersonNameValidator : AbstractValidator<Person> { public PersonNameValidator() { RuleFor(x => x.Surname).NotNull().Length(0, 255); RuleFor(x => x.Forename).NotNull().Length(0, 255); } }
因为上面这两个验证规则的目标都是Person,所以你可以在一个验证规则中包含他们:
public class PersonValidator : AbstractValidator<Person> { public PersonValidator() { Include(new PersonAgeValidator()); Include(new PersonNameValidator()); } }
你只能包含相同目标的验证规则。
配置
重写消息
你可以通过WithMessage 方法来重写验证错误的消息:
RuleFor(customer => customer.Surname).NotNull().WithMessage("Please ensure that you have entered your Surname");
在这个过程中你可以使用占位符来标志一些重要的信息,比如使用“{PropertyName}"可以显式你当前验证的哪个属性的名字:
RuleFor(customer => customer.Surname).NotNull().WithMessage("Please ensure you have entered your {PropertyName}");
在这个例子中,Surname会替代{PropertyName}这个占位符。
配置错误消息参数(占位符)
就像上面的例子中看到的一样,你可以在WithMessage方法中配置一些特殊的占位符,这些占位符会被一些特殊的值取代。每一个内置的验证规则(validator)都有一些占位符列表:
针对所有的验证规则:
- ‘{PropertyName}’:被验证的属性的名字
- ‘{PropertyValue}’:被验证的属性的值,包括那些断言验证(predicate validator)比如Must,email和regex验证。
针对比较验证规则(Equal, NotEqual, GreaterThan, GreaterThanOrEqual, etc.):
- {ComparisonValue}:被用来和属性比较的值。
针对长度验证规则:
- {MinLength} = 最小长度
- {MaxLength} = 最大长度
- {TotalLength} = 输入的字符数量
要查看完整的占位符列表请查看这个列表。每一个内置的规则器都有它自己的占位符列表。
实际上通过WithMessage 的另一个重载的方法,向这个方法传入一个lambda表达式就可以自定义你自己的验证消息。
//Using static values in a custom message: RuleFor(customer => x.Surname).NotNull().WithMessage(customer => string.Format("This message references some constant values: {0} {1}", "hello", 5)); //Result would be "This message references some constant values: hello 5" //Referencing other property values: RuleFor(customer => customer.Surname) .NotNull() .WithMessage(customer => $"This message references some other properties: Forename: {customer.Forename} Discount: {customer.Discount}"); //Result would be: "This message references some other properties: Forename: Jeremy Discount: 100"
重写属性的名称
默认的验证规则中包含了属性的默认名称:
RuleFor(customer => customer.Surname).NotNull();
你可以重写属性名称:
RuleFor(customer => customer.Surname).NotNull().WithName("Last name");
这会导致{PropertyName}的值有所改变。
需要注意的是这个改变只会在错误消息中展示,在ValidationResult中,这个值仍然是属性原来的名称Surname而不是Last name。如果你想要完全改变属性名,你可以使用OverridePropertyName方法。这个方法将ValidationResult中Erros中的属性名也改了(没验证)。
这也是WithName的一个重载方法,大部分时候你使用这个方法意味着你在使用WithName。
条件
When和Unless验证器可以指定规则验证的条件。比如,CustomerDiscount 属性上的验证只有在IsPreferredCustomer为true时执行。
RuleFor(customer => customer.CustomerDiscount).GreaterThan(0).When(customer => customer.IsPreferredCustomer);
Unless正好相反。
如果你想在多个验证上面应用相同的条件,那你也可以使用When,不过用法是这样的:
When(customer => customer.IsPreferred, () => { RuleFor(customer => customer.CustomerDiscount).GreaterThan(0); RuleFor(customer => customer.CreditCardNumber).NotNull(); });
When这个时候是在顶层执行而不是链式的调用了。
设置链式调用的规则
当你链式的对一个属性应用多个规则时,你可以追加一个链式调用的模式/规则,这个规则用来说明在链式调用中的其中一个验证失败时应该如何处理。如下,你有两个验证器:
RuleFor(x => x.Surname).NotNull().NotEqual("foo");
上面这个表示有两个验证器作用于Surname属性上,第一个验证器验证是否为空,第二个验证是否等于foo。当第一个验证失败时,第二个仍然会执行,尽管注定也会失败。可以使用Cascade来改变这一默认行为:
RuleFor(x => x.Surname).Cascade(CascadeMode.StopOnFirstFailure).NotNull().NotEqual("foo");
现在,当第一个验证器失败时,第二个就不会执行了。这对于后一个验证器依赖于前一个验证器成功的情况下是非常有用的。CascadeMode有两种模式:①
Continue(默认):总会全部执行。②
StopOnFirstFailure:
当第一个验证器失败时,第二个就不会执行了。
可以在验证规则中设置这个:
public class PersonValidator : AbstractValidator<Person> { public PersonValidator() { // First set the cascade mode CascadeMode = CascadeMode.StopOnFirstFailure; RuleFor(...) RuleFor(...) } }
依赖的规则
默认情况下FluentValidation 里面的所有规则都是互不影响的。这对于异步验证来说是有效的,并且针对异步验证来说这也是有意而为之。然而,在一些情况下,你需要确定在一些验证完成的情况下另一些验证才能开始执行,这通过DependentRules来完成。
将DependentRules添加在一个规则后面,DependentRules指定的这个规则依赖于前者。只有当前者执行完成后它才会执行:
RuleFor(x => x.Surname).NotNull().DependentRules(() => { RuleFor(x => x.Forename).NotNull(); });
现在只有当Surname 的验证通过了才会执行Forname的验证。
通常情况下这个验证器不推荐使用,可以用When来替代它。
回调
验证失败后你可以用OnFailure或者OnAnyFailure来进行回调。
RuleFor(x => x.Surname).NotNull().Must(surname => surname != null && surname.Length <= 200) .OnAnyFailure(x => { Console.WriteLine("At least one validator in this rule failed"); })
RuleFor(x => x.Surname).NotNull().OnFailure(x => Console.WriteLine("Nonull failed")) .Must(surname => surname != null && surname.Length <= 200) .OnAnyFailure(x => Console.WriteLine("Must failed"));