前言

之前就有写过学习笔记: Asp.net core 学习笔记 Fluent Validation

但都是用一点记入一点,零零散散不好读, 这一篇来稍微整理一下.

主要参考:

Fluent Validation 官网

安装

dotnet add package FluentValidation
dotnet add package FluentValidation.DependencyInjectionExtensions
dotnet add package FluentValidation.AspNetCore

注:FluentValidation.AspNetCore 已经废弃了。

Simple Use

要验证的类

public class Person
{
public string Email { get; set; } = "";
}

对应这个类的 Validator

public class PersonValidator : AbstractValidator<Person>
{
public PersonValidator()
{
RuleFor(e => e.Email).EmailAddress();
}
}

继承 AbstractValidator, 然后再构造函数里添加上验证逻辑.

调用验证方式

public static async Task Main()
{
var person = new Person { Email = "test..." };
var personValidator = new PersonValidator();
var validationResult = personValidator.Validate(person);
if (!validationResult.IsValid)
{
foreach (var error in validationResult.Errors)
{
Console.WriteLine(error.PropertyName); // Email
Console.WriteLine(error.ErrorMessage); // 'Email' is not a valid email address.
}
}
}

创建 validator 然后调用 validate, 把实例丢进去就会返回验证结果了.

如果是在 Web API controller 还可以直接 add to ModelState 哦

validationResult.AddToModelState(ModelState, prefix: null);

注:这个功能依赖废弃的 FluentValidation.AspNetCore,因此,最好自己重新实现。

public static class ValidationResultExtensions
{
// FluentValidation.AspNetCore 废弃后,我从它源码抄出来的
public static void AddToModelState(this ValidationResult result, ModelStateDictionary modelState)
{
if (!result.IsValid)
{
foreach (var error in result.Errors)
{
modelState.AddModelError(error.PropertyName, error.ErrorMessage);
}
}
} // FluentValidation.AspNetCore 废弃后,我从它源码抄出来的
public static void AddToModelState(this ValidationResult result, ModelStateDictionary modelState, string prefix)
{
if (!result.IsValid)
{
foreach (var error in result.Errors)
{
string key = string.IsNullOrEmpty(prefix)
? error.PropertyName
: string.IsNullOrEmpty(error.PropertyName)
? prefix
: prefix + "." + error.PropertyName;
modelState.AddModelError(key, error.ErrorMessage);
}
}
}
}

常用 Validator (Build-in)

参考: Built-in Validators

RuleFor(e => e.Email).Null();    // == null
RuleFor(e => e.Email).NotNull(); // != null

常用 1

RuleFor(e => e.Email).Equal("some value");    // == "some value"
RuleFor(e => e.Email).NotEqual("some value"); // != "some value"
RuleFor(e => e.Email).Matches("regex expression", RegexOptions.IgnoreCase); // 正则表达式
RuleFor(e => e.Email).EmailAddress(); // 封装好的 email 正则, empty string 也是 invalid 哦

常用 2

RuleFor(e => e.Salary).LessThan(1);                         // < 1
RuleFor(e => e.Salary).LessThanOrEqualTo(1); // <= 1
RuleFor(e => e.Salary).GreaterThan(1); // > 1
RuleFor(e => e.Salary).GreaterThanOrEqualTo(1); // >= 1
RuleFor(e => e.Salary).ExclusiveBetween(from: 1, to: 100); // > 1 and < 100
RuleFor(e => e.Salary).InclusiveBetween(from: 1, to: 100); // >= 1 and <= 100

常用 3

RuleFor(e => e.Email).Length(10);        // .Length == 10 (string, Array 都可以)
RuleFor(e => e.Email).MinimumLength(10); // .Length > 10
RuleFor(e => e.Email).MaximumLength(10); // .Length < 10

decimal 专用

RuleFor(e => e.Salary).ScalePrecision(scale: 19, precision: 2, ignoreTrailingZeros: true);

允许 19 位数, 有 2 个位数可以是小数. ignoreTrailingZeros 指 15.0000 结尾 4 个 0 不会占据位数

理解 Emtpty

RuleFor(e => e.Email).Empty();
RuleFor(e => e.Email).NotEmpty();

Empty 的意思是, 不能是 default value, 不能 length = 0 (string 会先 trim 才看 length 哦)

int = 0  – failed

enum = first enum value – failed

int? = null – failed

string = "" – failed

string = "   " – failed

List<string> = new() – failed

Date = default – failed

不常用的

// Credit Card Validator
// Enum Validator
// Enum Name Validator
// Predicate Validator

Cross Field

直接用就可以了. 很直观

RuleFor(e => e.Salary).GreaterThan(e => e.Age);

Conditional

参考: Conditions

有 2 种 conditional

1. 当满足条件时才验证

RuleFor(e => e.Salary).LessThan(10).When(e => e.Email == "test");

2. if ... else 配置

When(e => e.Email == "test", () =>
{
// 这里不要乱放代码, 只放 setup validation 代码就好, 因为它一定会执行
RuleFor(e => e.Salary).LessThan(500);
}).Otherwise(() =>
{
RuleFor(e => e.Salary).InclusiveBetween(from: 1, to: 100);
});

很可惜, 它没有提供 swtich 和 else if, 写起来不那么直观.

注: 它的运行机制是, validation setup 一定会跑 (和 if 概念不同哦, 所以不要乱吧代码放进 setup validation scope 里面), 在做 validation 的时候才调用 when 去判断是否要执行

Include Properties

参考: Validator customization

var validationResult = personValidator.Validate(
person,
options => options.IncludeProperties("Email", "Salary") // params string[] properties
);

适用于 partial update 场景.

Child Validation

public class Address
{
public string Line1 { get; set; } = "";
} public class Person
{
public string Name { get; set; } = "";
public int Age { get; set; }
public Address? Address { get; set; }
}

Person 里面有 Address 要如何写 validation 呢?

public class PersonValidator : AbstractValidator<Person>
{
public PersonValidator()
{
RuleFor(e => e.Name).NotEmpty(); When(e => e.Address != null, () =>
{
RuleFor(e => e.Address!.Line1).NotEmpty();
});
}
}

直接在 PersonValidator 里面定义验证规则。

注:记得加上 When 判断 null,不然当 Address is null 时会报错。

还有另一种方式是定义 AddressValidator

public class AddressValidator : AbstractValidator<Address>
{
public AddressValidator()
{
RuleFor(e => e.Line1).NotEmpty();
}
}

然后在 PersonValidator 里配置 Address 和 AddressValidator

public class PersonValidator : AbstractValidator<Person>
{
public PersonValidator()
{
RuleFor(e => e.Name).NotEmpty();
RuleFor(e => e.Address!).SetValidator(new AddressValidator());
}
}

当 Address is null 它会自动 skip 验证,我们不需要写 When 判断。

如果是 List<Address> 也差不多

public class Person
{
public string Name { get; set; } = "";
public int Age { get; set; }
public List<Address> Addresses { get; set; } = [];
} public class PersonValidator : AbstractValidator<Person>
{
public PersonValidator()
{
RuleFor(e => e.Name).NotEmpty();
RuleForEach(e => e.Addresses).SetValidator(new AddressValidator());
}
}

用到了 RuleForEach。

要在 PersonValidator 里面定义验证规则也行

RuleForEach(e => e.Addresses).ChildRules(address =>
{
address.RuleFor(e => e.Line1).NotEmpty();
});

使用 ChildRules 就可以了。

Custom Validator

参考: Custom Validators

除了使用 build-in 的 validator, 想要自己写逻辑验证有 2 个方法.

1. Must

RuleFor(e => e.Email).Must((rootObject, propertyValue) => {
return rootObject.Salary == 1 && propertyValue == "email@email.com";
});

直接写验证逻辑. 通过实例和属性值做判断.

2. PropertyValidator

public class MoneyValidator<T> : PropertyValidator<T, decimal> where T : Person
{
public override string Name => "MoneyValidator";
public override bool IsValid(ValidationContext<T> context, decimal propertyValue)
{
var person = context.InstanceToValidate; // 可以拿到 instance, 如果没有用到, 泛型 T 就好了, 不需要 where
return propertyValue == 1;
}
}

使用

RuleFor(e => e.Salary).SetValidator(new MoneyValidator<Person>());

Friendly call

RuleFor(e => e.Salary).Money();

extension method

public static class ValidatorExtensions
{
public static IRuleBuilderOptions<T, decimal> Money<T>(this IRuleBuilder<T, decimal> ruleBuilder)
{
return ruleBuilder.SetValidator(new MoneyValidator<T>());
} public static IRuleBuilderOptions<T, decimal?> Money<T>(this IRuleBuilder<T, decimal?> ruleBuilder,)
{
return ruleBuilder.SetValidator(new MoneyValidator<T>());
}
}

注: decimal 和 decimal? 要 2 个方法重载. 内部 set 同一个 validator 就可以了, 当遇到 null 的时候它会直接 pass, 估计内部有做了处理, 这个方式是源码学来的.

Dependency Injection

参考: Dependency Injection

和 EF Core 类似的做法, 通过反射 Assembly 找出 Validator 然后 AddScope.

mvcBuilder.AddFluentValidation(options =>
{
options.RegisterValidatorsFromAssembly(assembly);
});

Web API Controller

private readonly CreateProjectDtoValidator _createProjectDtoValidator;public ProjectController(CreateProjectDtoValidator createProjectDtoValidator)
{
_createProjectDtoValidator = createProjectDtoValidator
}

这样 Validator 就可以注入 DbContext 和其它 service 了.

Asynchronous

参考: Asynchronous Validation

有几个方法都可以异步.

1. WhenAsync

WhenAsync((person, cancellationToken) => {
return Task.FromResult(true);
}, () => {
RuleFor(e => e.Email).EmailAddress();
});
RuleFor(p => p.Email).EmailAddress().WhenAsync((person, cancellationToken) => Task.FromResult(true));

2. MustAsync

RuleFor(e => e.Email).MustAsync((rootObject, propertyValue, context, cancellationToken) => {
return Task.FromResult(true);
});

3. Customer AsyncValidator

public class MoneyAsyncValidator<T> : AsyncPropertyValidator<T, decimal>
{
public override string Name => "MoneyValidator";
public override Task<bool> IsValidAsync(ValidationContext<T> context, decimal propertyValue, CancellationToken cancellation)
{
return Task.FromResult(propertyValue == 1);
}
}

调用

RuleFor(e => e.Salary).SetAsyncValidator(new MoneyAsyncValidator<Person>());

4. ValidateAsync

如果验证规则里用到了 async, 那在调用 Validate 的时候要用 Async 版本哦.

var validationResult = await personValidator.ValidateAsync(person);

PropertyName, DisplayName

参考: Overriding the Property Name

参考之前的: ASP.NET Core – Case Style Conversion FluentValidation 的部分.

Manually set error with property name and display name

Fluent Validation 当有 Children 的时候, 它的返回是这样的

property name 会是一个 path 的形式. array 就配上 [n].

如果我们有需求动态添加 error 的话, 就必须符合它的格式哦. 比如:

var validator = new PersonValidator();
var person = new Person { Children = new List<Child> { new Child(), new Child() } };
var personResult = validator.Validate(person);
for (int i = 0; i < person.Children.Count; i++)
{
var child = person.Children[i];
var childValidator = new ChildValidator();
var childResult = childValidator.Validate(child);
foreach (var error in childResult.Errors)
{
var eExp = Expression.Parameter(person.GetType(), "e");
var eDotNameExp = Expression.Property(eExp, nameof(person.Children));
var lambda = Expression.Lambda(eDotNameExp, eExp);
var propertyName = ValidatorOptions.Global.PropertyNameResolver(person.GetType(), person.GetType().GetProperty(nameof(person.Children)), lambda);
error.PropertyName = $"{propertyName}[{i}].{error.PropertyName}";
personResult.Errors.Add(error);
}
}
Console.WriteLine(JsonSerializer.Serialize(personResult.Errors.Select(e => new { e.PropertyName, e.ErrorMessage }), new JsonSerializerOptions { WriteIndented = true } ));

需要特别注意的是, PropertyName 必须经过正确的 ValidatorOptions.Global.PropertyNameResolver 处理.

第 1 个参数是 root class type, 第 2 个参数是 last depth PropertyInfo, 最后一个是从 Root 到 deepest propertyInfo 的路径 lambda 表达式

这样它才能 generate 到对的 Property Name

FluentValidation parse expression 的源码是这样的

就这样看的话, 应该是没有 cover 到 Children[0].Name 这种 [0] 的处理的. 所以估计它是通过外部累加做到的. 所以使用 PropertyNameResolver 的时候, 可不要放入 [0] 这种 expression 哦.

Cascade mode

参考: Setting the Cascade mode

默认情况下, 当一个错误发生以后, 其它的验证依然会执行, 然后返回所有的错误.

有时候这不一定是我们期望的模式.

举例, email address 正则验证

当 empty string 的时候, 算不算 invalid email address ?

通常是不算的, 都没有填, 验个毛. 应该要跑错 required 必填.

那怎样处理?

3 个思路.

1. email validator 遇到 emtpy string 算 pass

2. email validator + when string.IsNullOrEmpty(value)

3. 当 1 个 error 发生, 停止后续的验证.

cascade mode 就是只第 3 种情况.

RuleFor(e => e.Email).Cascade(CascadeMode.Stop).NotEmpty().EmailAddress();

另一种写法是 depend rule, 当 a rule ok 了才执行 b rule, 这也可以算一种 Conditional 的手法.

RuleFor(e => e.Email).NotEmpty().DependentRules(() =>
{
RuleFor(e => e.Email).EmailAddress();
});

要 set global 或者 by validator 就这样:

Error Message

参考: Overriding the Message

WithMessage

RuleFor(e => e.EmailSalary).EmailAddress().WithMessage("{PropertyName} {PropertyValue} is no ok!");

ValidationContext

在 Must, CustomPropertyValidator 内操作 context 也可以设置更多的参数.

RuleFor(e => e.Email).Must((rootObject, propertyValue, context) =>
{
context.MessageFormatter.AppendArgument("MyValue", "value");
return false;
}).WithMessage("{MyValue}");

customer property validator default message template

public class MoneyValidator<T> : PropertyValidator<T, decimal>
{
public override string Name => "MoneyValidator";
public override bool IsValid(ValidationContext<T> context, decimal value)
{
return true;
}
protected override string GetDefaultMessageTemplate(string errorCode) // errorCode 都是 null, 不清楚怎么用
{
return "{MyValue} is wrong.";
}
}

Migrations

记入一些我遇到的 migrations, FluentValidation 的 migrations 维护到很好. 所有提示都给的很到位

打开 Github 会看见 before & after 的 step

另外 RegisterValidatorsFromAssembly 也是 deprecated 勒

打开 Github 会看见 before & after 的 step

其它大版本的 Migrations 可以在 Docs – 11.0 Upgrade Guide 里看到.

ASP.NET Core Library – FluentValidation的更多相关文章

  1. 用ASP.NET Core 2.0 建立规范的 REST API -- DELETE, UPDATE, PATCH 和 Log

    本文所需的一些预备知识可以看这里: http://www.cnblogs.com/cgzl/p/9010978.html 和 http://www.cnblogs.com/cgzl/p/9019314 ...

  2. 《ASP.NET Core 高性能系列》致敬伟大的.NET斗士甲骨文!

    写在开始 三年前,曾写过一篇文章:从.NET和Java之争谈IT这个行业,当时遭到某些自认为懂得java就了不起的Javaer抨击, 现在可以致敬伟大的.NET斗士甲骨文了 (JDK8以上都需要收费, ...

  3. ASP.NET CORE MVC 2.0 项目中引用第三方DLL报错的解决办法 - InvalidOperationException: Cannot find compilation library location for package

    目前在学习ASP.NET CORE MVC中,今天看到微软在ASP.NET CORE MVC 2.0中又恢复了允许开发人员引用第三方DLL程序集的功能,感到甚是高兴!于是我急忙写了个Demo想试试,我 ...

  4. ASP.NET Core WebApi中使用FluentValidation验证数据模型

    原文链接:Common features in ASP.NET Core 2.1 WebApi: Validation 作者:Anthony Giretti 译者:Lamond Lu 介绍 验证用户输 ...

  5. 【翻译】asp.net core中使用FluentValidation来进行模型验证

    asp.net core中使用FluentValidation FluentValidation 可以集成到asp.net core中.一旦启用,MVC会在通过模型绑定将参数传入控制器的方法上时使用F ...

  6. 基于 ASP.NET Core 2.1 的 Razor Class Library 实现自定义错误页面的公用类库

    注意:文中使用的是 razor pages ,建议使用 razor views ,使用 razor pages 有一个小坑,razor pages 会用到 {page} 路由参数,如果应用中也用到了这 ...

  7. ASP.NET Core and .NET Core Library Support

    ASP.NET Core and .NET Core Library Support 详情参见:https://github.com/linezero/NETCoreLibrary/blob/mast ...

  8. ASP.NET Core 1.0 开发记录

    官方资料: https://github.com/dotnet/core https://docs.microsoft.com/en-us/aspnet/core https://docs.micro ...

  9. ASP.NET Core: You must add a reference to assembly mscorlib, version=4.0.0.0

    ASP.NET Core 引用外部程序包的时候,有时会出现下面的错误: The type 'Object' is defined in an assembly that is not referenc ...

  10. ASP.NET Core中的依赖注入(1):控制反转(IoC)

    ASP.NET Core在启动以及后续针对每个请求的处理过程中的各个环节都需要相应的组件提供相应的服务,为了方便对这些组件进行定制,ASP.NET通过定义接口的方式对它们进行了"标准化&qu ...

随机推荐

  1. 权威技术社区InfoQ列网易数帆为年度最有价值技术团队,技术实力获广泛认可

    近日,权威技术社区InfoQ公布数字化转型技术服务商网易数帆为2020年最有价值技术团队,并被授予"最佳技术社区驱动力奖",认可网易数帆在技术领域的突出成就和实力.     Inf ...

  2. 暑假Java自学每日进度总结1

    今日所学: 一.常用的cmd命令: 1>盘符: 2>dir(显示当前文件所有目录) 3>cd 目录(打开该目录) 4>cd..(回到上一目录) 5>cd(回到当前盘符初始 ...

  3. [oeasy]python0140_导入_import_from_as_namespace_

    导入import 回忆上次内容 上次学习了 try except   注意要点 半角冒号 缩进 输出错误信息   有错就报告 不要隐瞒 否则找不到出错位置 还可以用traceback把 系统报错信息原 ...

  4. Known框架实战演练——进销存系统需求

    概述 该项目是一个开源.简易.轻量级的进销存管理系统,作为Known框架的实战演练项目. 项目代码:JxcLite 开源地址: https://gitee.com/known/JxcLite 功能模块 ...

  5. Python 基于xml.etree.ElementTree实现XML对比

    测试环境 Python 3.6 Win10 代码实现 #!/usr/bin/env python 3.4.0 #-*- encoding:utf-8 -*- __author__ = 'shouke' ...

  6. sqoop 从数据库导入数据到hdfs

    前提 配置hadoop配置文件 前提 启动hadoop 配置hive 改名进入sqoop/conf 增加环境变量 tar xf sqoop-1.4.7.bin__hadoop-2.6.0.tar.gz ...

  7. 【Mybatis-Plus】01 快速上手

    [官网快速上手地址] https://mp.baomidou.com/guide/quick-start.html#%E5%88%9D%E5%A7%8B%E5%8C%96%E5%B7%A5%E7%A8 ...

  8. 2024年 智能机器人元年 —— 国内的智能机器人(humanoid)公司当下最大的压力(最为急迫的任务)是什么?

    可以说,2024年是人形机器人的元年.我国在去年年底将发展智能机器人立为了第一线的重要科技发展方向,并计划在2024年.2025年建立出完整的产业链条,并培育出几家成熟的行业领先的智能机器人公司.而我 ...

  9. 中国特供版4090D已经开始发售

    由于美国政府的限制,NVIDIA公司等美国公司不允许向中国出口4090显卡,但是为了绕过美国政府的限制NVIDIA公司推出了中国特供版的4090D显卡. 4090d显卡和4090显卡区别大吗?可以说其 ...

  10. 始智AI —— https://wisemodel.cn/ —— 试用

    清华大学的合资企业推出的服务: 始智AI -- https://wisemodel.cn/ 链接: 始智AI -- https://wisemodel.cn/ 和modelscope比相对简约,毕竟功 ...