前言

之前就有写过学习笔记: 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. mysql Using join buffer (Block Nested Loop) join连接查询优化

    最近在优化链表查询的时候发现就算链接的表里面不到1w的数据链接查询也需要10多秒,这个速度简直不能忍受 通过EXPLAIN发现,extra中有数据是Using join buffer (Block N ...

  2. Profibus_DP转ModbusTCP网关模块接马保通讯案例

    某工业企业为了提高生产效率和管理水平,决定对其生产线进行智能化改造.在该项目中,利用巴图自动化Profibus_DP转ModbusTCP网关模块(BT-ETHPB20)连接了不同生产设备,实现了设备之 ...

  3. es6高级~promise

    1.Promise对象 Promise 对象用于表示一个异步操作的最终完成 (或失败)及其结果值.其作用是为了解决回调地狱 回调地狱:回调函数的结果作为下一个回调函数的参数时,产生回调链,也称之为回调 ...

  4. 推荐几款.NET开源且功能强大的实用工具,助你提高工作开发效率!

    前言 俗话说得好"工欲善其事,必先利其器",今天大姚给大家推荐8款.NET开源且功能强大的实用工具,助你提高工作开发效率! DevToys 一款基于C#开源(MIT License ...

  5. RHCA cl210 013 制作镜像 轮转key rabbitmq追踪 写时复制 keystone多域登录图形界面

    undercloud 部署 overcloud overcloud控制节点上的组建rabbitmq 排错需要rabbitmq,开启追踪则会更详细,会消耗性能 环境问题 登录一下classroom os ...

  6. 【Vue】Re11 Vue 与 Webpack

    一.案例环境前置准备: 创建一个空目录用于案例演示 mkdir vue-sample 初始化案例和安装webpack cd vue-sample npm install webpack@3.6.0 - ...

  7. 【转载】 HTTP中的响应协议及302、304的含义

    原文地址: https://www.cnblogs.com/chenyablog/p/9197305.html ============================== 响应协议 HTTP/1.1 ...

  8. H5页面\PC端实现QQ客服功能

    1.背景 很多应用都有在线客服,最简单是实现就是利用人们常用的QQ 2.实现 步骤一:授权QQ通讯组件(普通QQ都是可以的) 授权链接:https://shang.qq.com/v3/widget.h ...

  9. Jenkins部署架构概述

    1.Jenkins是什么 Jenkins是一个开源的.提供友好操作界面的持续集成(CI)工具,起源于Hudson,主要用于持续.自动的构建/测试软件项目.监控外部任务的运行. Jenkins用Java ...

  10. AI阅读助手ChatDOC:基于 AI 与文档对话、重新定义阅读方式的AI文献阅读和文档处理工具

    让 AI 真正成为你的生产力超级助手 AI 时代降临,我们需要积极拥抱 AI 工具 在过去的 2 个多月里,以 ChatGPT 为代表的 AI 风靡全球.随着 GPT 模型的不断优化,ChatGPT ...