FluentValidation是一个流行的开源验证库,用于在.NET应用程序中验证输入数据。
它有如下特点
- 语法简洁
- 强大的验证功能
- 支持多语言和本地化
- 可重用性和模块化
- 易于集成
- 详细的错误消息
- 扩展性高
借助FluentValidation,我们可以达到优雅地校验参数的目的。
环境准备:Install-Package FluentValidation -version 11.9.1
目标框架:.Net8
-
创建验证器类: 创建一个继承自
AbstractValidator<T>
的类,其中T
是要验证的模型类型。csharppublic class AccountModel { public string Name { get; set; } } public class AccountModelValidator : AbstractValidator<AccountModel> { public AccountModelValidator() { RuleFor(e => e.Name).NotNull(); } }
-
在应用程序中使用验证器: 在需要验证输入的地方,实例化验证器类并调用
Validate
方法。csharp[HttpPost] public dynamic GetData(AccountModel dto) { AccountModelValidator validator = new AccountModelValidator(); ValidationResult result = validator.Validate(dto); return Ok(result); }
完成这两步,就可以创建第一个简单例子了。
当AccountModel.Name=null时,查看最终ValidationResult结果。FluentValidation会根据规则校验参数不能为空,并返回多项校验信息
json{ "isValid": false, "errors": [ { "propertyName": "Name", "errorMessage": "'Name' must not be empty.", "attemptedValue": null, "customState": null, "severity": 0, "errorCode": "NotNullValidator", "formattedMessagePlaceholderValues": { "PropertyName": "Name", "PropertyValue": null, "PropertyPath": "Name" } } ], "ruleSetsExecuted": [ "default" ] }
-
注册:手动new validator不是一个好思想,可以注册后使用依赖注入
手动注册
csharpbuilder.Services.AddTransient<IValidator<AccountModel>, AccountModelValidator>(); //或 builder.Services.AddValidatorsFromAssemblyContaining<AccountModelValidator>(); //或 builder.Services.AddValidatorsFromAssemblyContaining(typeof(AccountModelValidator));
或自动注册
需要引入nuget包FluentValidation.AspNetCore
Install-Package FluentValidation.AspNetCore
csharpbuilder.Services.AddValidatorsFromAssembly(Assembly.Load("SomeAssembly")); //或 builder.Services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
-
依赖注入
csharpprivate readonly IValidator<AccountModel> _validator; public ExampleController(IValidator<AccountModel> validator) { this._validator = validator; } [HttpPost] public dynamic GetData(AccountModel dto) { ValidationResult result = _validator.Validate(dto); return Ok(result); }
-
自定义验证错误消息: 使用
WithMessage
方法指定自定义错误消息。自定义错误消息
csharpRuleFor(e => e.Name).NotNull().WithMessage("名字不能为空");
使用占位符
csharpRuleFor(address => address.Postcode).NotEmpty().WithMessage(address => string.Format("国家:{0},城市:{1}的邮编不能为空", address.Country, address.City)); //或 RuleFor(address => address.Postcode).NotEmpty().WithMessage(address => $"国家:{address.Country},城市:{address.City}的邮编不能为空"); //"errorMessage": "国家:China,城市:深圳的邮编不能为空",
重写属性名
csharpRuleFor(address => address.Postcode).NotEmpty().WithName("邮编"); //"errorMessage": "'邮编' must not be empty."
全局重写属性名
csharp//全部被验证的类Country属性名都会被替换 ValidatorOptions.Global.DisplayNameResolver = (type, member, expression) => { if (member?.Name=="Country") return "国家"; return null; };//'国家' must not be empty.
-
校验失败时抛异常:异常需要捕捉
csharp_validator.ValidateAndThrow(dto);
-
级联验证: 在一个属性的验证规则中调用其他属性的验证规则。
csharpRuleFor(x => x.EndDate).GreaterThan(x => x.StartDate).WithMessage("结束日期必须大于开始日期.");
默认情况下,验证规则会执行所有验证规则,如果失败时要停止执行后续的验证,则可使用CascadeMode.Stop模式
csharpRuleFor(x => x.Country).Cascade(CascadeMode.Stop).NotNull().NotEqual("China");//任何失败都会停止执行,只会返回第一个失败结果
-
自定义规则集:规则集允许将验证规则组合在一起
假设,如果同时有两个接口需要校验同一个类,但规则不同怎么办呢。
举例,A接口要求Name参数长度必须大于5小于20,B接口要求Name长度在2-10之间。
这时可以定义两套规则集,分别指定不同命名。在不同接口使用不同规则集时,根据名称指定使用。
csharppublic class AccountModelValidator : AbstractValidator<AccountModel> { public AccountModelValidator() { RuleSet("short", () => { RuleFor(e => e.Name).NotEmpty().Must(x => x.Length >= 2 && x.Length <= 10).WithMessage("名字长度必须在2-10之间"); }); RuleSet("long", () => { RuleFor(e => e.Name).NotEmpty().Must(x => x.Length > 5 && x.Length < 20).WithMessage("名字长度必须大于5小于20"); }); RuleFor(e => e.Age).NotEmpty().LessThan(18);//规则集外的 } }
具体使用时可以指定规则集:
只执行指定规则集,非指定规则集、和规则集外的均不执行
csharpvar result = _validator.Validate(dto, options =>options.IncludeRuleSets("short");//A接口 var result = _validator.Validate(dto, options =>options.IncludeRuleSets("long");//B接口
多个规则集一起执行
csharpvar result = _validator.Validate(dto, options =>options.IncludeRuleSets("short", "long"));
执行涉及某个属性的所有规则集
csharpvar result = _validator.Validate(dto, options => options.IncludeProperties(e=>e.Name);//只要规则集内有对Name的约束,这些规则都会执行校验
执行所有规则集合
csharpvar result = _validator.Validate(dto, options => options.IncludeAllRuleSets();
执行指定规则集和规则集外的验证规则
csharpvar result = _validator.Validate(dto, options => options.IncludeRuleSets("short").IncludeRulesNotInRuleSet());
-
条件验证:
使用顶级语句When
csharpWhen(e => e.Country == "China", () => RuleFor(addr => addr.Postcode).Must(addr => addr.Length == 6).WithMessage(address => "中国的邮编长度须为6")); When(e => e.Country == "America", () => RuleFor(addr => addr.Postcode).Must(addr => addr.Length == 9).WithMessage(address => "美国的邮编长度须为9")); //其他例子 RuleFor(customer => customer.Address.Postcode).NotNull().When(customer => customer.Address != null); RuleFor(x => x.Age).GreaterThan(18).When(x => x.IsAdult);
使用Otherwise表示
否则
条件csharpWhen(e => e.Country == "China", () => RuleFor(addr => addr.Postcode).NotEmpty().Must(addr => addr.Length == 6).WithMessage(address => "中国的邮编长度须为6")) .Otherwise(()=> RuleFor(addr => addr.Postcode).NotEmpty().WithName("邮编"));
仅将条件应用与前述验证器,则要添加ApplyConditionTo.CurrentValidator,否则条件的执行顺序和最终结果,会与意想中不一致
csharpRuleFor(address => address.Postcode).NotEmpty()//, ApplyConditionTo.CurrentValidator .Must(e => e.Length == 6).When(e => e.Country == "China", ApplyConditionTo.CurrentValidator).WithMessage(address => "中国的邮编长度须为6") .Must(e => e.Length == 9).When(e => e.Country == "America", ApplyConditionTo.CurrentValidator).WithMessage(address => "美国的邮编长度须为9");
-
复合属性验证:当出现复合的类属性,且类属性自定义了验证器,一个验证器中可以复用另一个验证器
csharppublic class Address { public string City { get; set; } public string Town { get; set; } public string Postcode { get; set; }//邮编 } public class AddressValidator : AbstractValidator<Address> { public AddressValidator() { RuleFor(address => address.Postcode).NotNull(); } } public class AccountModel { public string Name { get; set; } public Address Address { get; set; }//地址 } public class AccountModelValidator : AbstractValidator<AccountModel> { public AccountModelValidator() { RuleFor(e => e.Name).NotNull(); //RuleFor(e => e.Address).NotNull();//如需先校验Address是否为空,则需添加此句 RuleFor(e => e.Address).SetValidator(new AddressValidator());//相当于RuleFor(e => e.Address.Postcode).NotNull().When(e => e.Address != null); } }
-
集合元素验证:可以对集合中每个元素进行验证
csharppublic class AccountModel { public string Name { get; set; } public int? Age { get; set;} public List<string> PhoneNumbers { get; set; } }
csharppublic class AccountModelValidator : AbstractValidator<AccountModel> { public AccountModelValidator() { RuleForEach(x => x.PhoneNumbers).NotEmpty().WithMessage("手机号码不能为空"); } }
也可以配合复合属性,复用验证器;同时也可以使用where条件过滤筛选需要验证的元素
csharppublic class Address { public string Country { get; set; } public string City { get; set; } public string Town { get; set; } public string Postcode { get; set; }//邮编 } public class AccountModel { public List<Address> Address { get; set; } } public class AccountModelValidator : AbstractValidator<AccountModel> { public AccountModelValidator() { //筛选集合中国家为China的元素,验证邮编长度必须为6位 RuleForEach(x => x.Address).Where(x => x.Country == "China").SetValidator(new AddressValidator()); } }
-
相等验证:
相等或不等判断
csharpRuleFor(address => address.Country).Equal("China"); RuleFor(address => address.Country).NotEqual("China");
忽略大小写
csharpRuleFor(address => address.Country).NotEqual("China", StringComparer.OrdinalIgnoreCase);
-
长度验证:
csharp//长度限制 RuleFor(address => address.Country).Length(1, 250); //最大长度 RuleFor(address => address.Country).MaximumLength(250); //最小长度 RuleFor(address => address.Country).MinimumLength(1);
-
大小验证:
csharpRuleFor(product => product.Price).LessThan(100);//小于 RuleFor(product => product.Price).GreaterThan(0);//大于 RuleFor(product => product.Price).LessThanOrEqualTo(100);//小于等于 RuleFor(product => product.Price).GreaterThanOrEqualTo(0);//大于等于 RuleFor(product => product.Price).GreaterThan(product => product.MinPriceLimit);//与其他属性比较大小
-
正则表达式:
csharpRuleFor(address => address.Country).Matches(@"^[A-Za-z]+$").WithMessage("国家必须为纯英文字母");
-
自带邮箱验证:
csharpRuleFor(address => address.Email).EmailAddress();
-
枚举验证:
csharpRuleFor(x => x.ConsumerLevel).IsInEnum();//如果值在非枚举之外,则抛错
-
区间验证:
csharp//不含边界值,即大于1、小于100 RuleFor(x => x.Id).ExclusiveBetween(1,100);//'Id' must be between 1 and 100 (exclusive). You entered 100. //含边界值,即大于等于1、小于等于100 RuleFor(x => x.Id).InclusiveBetween(1,100);//'Id' must be between 1 and 10. You entered 0.
-
小数位数验证:
csharpRuleFor(x => x.Price).PrecisionScale(4, 2, false).WithMessage(""单价"的位数不得超过 4 位,允许小数点后 2 位。不允许出现5个及以上数字,不允许出现3位及以上小数"); //ignoreTrailingZeros 为false时,小数 123.4500 将被视为具有 7 的精度和 4 的刻度 //ignoreTrailingZeros 为true时,小数 123.4500 将被视为具有 5 的精度和 2 的刻度。
-
扩展方法自定义占位符
csharppublic static class ValidatorExtension { public static IRuleBuilderOptions<T, IList<TElement>> ListCountMustFewerThan<T, TElement>(this IRuleBuilder<T, IList<TElement>> ruleBuilder, int num) { return ruleBuilder.Must((rootObject, list, context) => { context.MessageFormatter.AppendArgument("MaxCount", num); return list.Count < num; }) .WithMessage("{PropertyName}的元素数量必须少于{MaxCount}个."); } } //使用时就方便很多 RuleFor(x => x.PhoneNumbers).ListCountMustFewerThan(3);
-
更高控制度的自定义验证器:
csharpRuleFor(x => x.Country).Custom((element, context) => { if (element != "中国" && element != "美国") { context.AddFailure("其他国家不支持"); } });
-
组合验证器:使用Include组合多个验证器
csharppublic class Address { public int Id { get; set; } public string Country { get; set; } public string Postcode { get; set; }//邮编 }
csharppublic class PostcodeValidator : AbstractValidator<Address>//注意继承自同一个AbstractValidator { public PostcodeValidator() { RuleFor(address => address.Postcode).NotEmpty().WithMessage("邮编不能为空"); RuleFor(address => address.Postcode).MaximumLength(10).WithMessage("邮编长度不能超过10"); } } public class CountryValidator : AbstractValidator<Address>//注意继承自同一个AbstractValidator { public CountryValidator() { RuleFor(address => address.Country).NotEmpty().WithMessage("国家不能为空"); } } public class AddressValidator : AbstractValidator<Address> { public AddressValidator() { Include(new PostcodeValidator()); Include(new CountryValidator()); } }
-
验证指定属性:验证与指定属性有关的规则,其他的不验证
csharpvar result = _addressValidator.Validate(addr, opt => opt.IncludeProperties(x => x.Country));
验证集合中的指定元素的规则:使用通配符索引器 (
[]
) 来指定集合中的元素项csharp//验证每个订单的 Cost 属性 _validator.Validate(customer, options => options.IncludeProperties("Orders[].Cost"));
-
异步:调用 ValidateAsync 将同时运行同步和异步规则。
如果验证程序包含异步验证程序或异步条件,请务必始终在验证程序上调用 ValidateAsync,而切勿调用 Validate。如果调用 Validate,则会引发异常。
csharpvar result = await validator.ValidateAsync(customer);
建议:不要使用异步规则,因为 ASP.NET 的验证管道不是异步的。如果将异步规则与 ASP.NET 的自动验证一起使用,则它们将始终同步运行(FluentValidation 10.x 及更早版本)或引发异常(FluentValidation 11.x 及更高版本)。
-
设置严重性级别:
csharpRuleFor(product => product.Price).NotNull().WithSeverity(person => Severity.Warning);
全局设置严重性级别
csharpValidatorOptions.Global.Severity = Severity.Info;
-
设置错误码:
csharpRuleFor(product => product.Price).NotNull().WithErrorCode("502");
-
自定义状态:
csharpRuleFor(person => person.Name).NotNull().WithState(person => 1);
-
本地化多语言:
csharppublic class ProductValidator : AbstractValidator<Product> { public ProductValidator(IStringLocalizer<Product> localizer) { //根据key值以特定语言显示 RuleFor(product => product.Price).NotNull().WithMessage(x => localizer["PriceNotNullKey"]); } } public class CustomLanguageManager : FluentValidation.Resources.LanguageManager { public CustomLanguageManager() { AddTranslation("en", "PriceNotNullKey", "'{PropertyName}' is required."); AddTranslation("zh", "PriceNotNullKey", "'{PropertyName}' 是必填项."); } }
csharp//通过设置 LanguageManager 属性来替换默认的 LanguageManager ValidatorOptions.Global.LanguageManager = new CustomLanguageManager(); //是否禁用本地化 ValidatorOptions.Global.LanguageManager.Enabled = false; //强制默认消息始终以特定语言显示 ValidatorOptions.Global.LanguageManager.Culture = new CultureInfo("zh");
-
依赖规则:
默认情况下,FluentValidation 中的所有规则都是独立的,不能相互影响。这是异步验证工作的有意和必要条件。但是,在某些情况下,我们希望确保某些规则仅在另一个规则完成后执行。
这时可以使用
DependentRules
来执行此操作csharpRuleFor(x => x.Name).NotNull().DependentRules(() => { RuleFor(x => x.Id).NotNull(); });
在许多情况下,结合使用
When
条件CascadeMode
来防止规则运行可能更简单 -
自定义异常:
csharppublic class AddressValidator : AbstractValidator<Address> { public AddressValidator() { RuleFor(address => address.Postcode).NotEmpty().WithMessage("邮编不能为空"); } //自定义异常 protected override void RaiseValidationException(ValidationContext<Address> context, ValidationResult result) { var ex = new ValidationException(result.Errors); throw new CustomException(ex.Message); } }
捕捉异常
csharptry { _addressValidator.ValidateAndThrow(addr); } catch (CustomException ex) { //验证不通过时,就会捕捉到自定义异常 }
总结:
- 在我们的编程习惯中,往往把校验逻辑嵌入到业务代码中,这样其实违背了单一原则。
- 在设计项目框架时,应考虑把校验逻辑分离核心业务,虽然校验也是业务的一环,但项目的焦点应该放在核心业务代码上 ,校验逻辑不应干扰到核心业务。
- 借助FluentValidation,我们可以很方便地分离校验逻辑,同时由于校验逻辑的分离,可以自由组合、复用模块。因为很多校验逻辑其实是重复的。
- 而且模块的复用,也达到了统一校验标准的目的。