正确写法是:对可能为null的导航属性用NotNull()守卫顶层+When()显式判空,值类型用Must()判断;异步校验需用MustAsync()或SetValidator()指向含异步规则的验证器;注册时须AddControllers().AddFluentValidation()并确保特性与泛型匹配。

FluentValidation 的 AbstractValidator<T> 怎么写才不踩空引用?
直接继承 AbstractValidator<T> 时,最常见的崩溃是校验器内部访问了 null 属性却没提前判空——比如 RuleFor(x => x.Address.Street),但 Address 是 null,就会抛 NullReferenceException。
正确做法是:所有可能为 null 的导航属性,必须用 When() 或链式 NotNull() + SetValidator() 分层守卫:
RuleFor(x => x.Address).NotNull().WithMessage("地址不能为空");
RuleFor(x => x.Address.Street)
.NotEmpty().When(x => x.Address != null); // 显式守卫
- 别依赖
NotNull()自动跳过子属性校验——它只拦自己,不拦后续链式调用 - 如果
Address是值类型(如struct),NotNull()无效,得用Must()手动判断 - 泛型校验器注册到 DI 容器时,确保生命周期是
Scoped或Transient,避免跨请求状态污染
怎么让 ValidateAsync() 真正异步,而不是假异步?
ValidateAsync() 默认是同步执行的,除非你显式用了异步规则(比如调用外部 API、数据库查重)。很多人误以为只要方法签名带 Async 就自动异步,结果压测时线程池被卡死。
真正触发异步的只有两类规则:
-
MustAsync():传入Func<T, CancellationToken, Task<bool>> -
SetValidator()指向一个继承自AbstractValidator<T>且内部含异步规则的验证器
示例(邮箱唯一性检查):
RuleFor(x => x.Email)
.MustAsync(async (model, email, ct) =>
{
return !await _userRepository.ExistsByEmailAsync(email, ct);
})
.WithMessage("该邮箱已被注册");
注意:MustAsync 内部不能混用同步 I/O(如直接调用 HttpClient.Send()),否则会阻塞线程;也别在异步规则里 throw 异常,要用 return false + WithMessage 控制错误输出。
为什么 AddFluentValidation() 后模型绑定仍不触发校验?
ASP.NET Core 中,仅注册 FluentValidation 不等于开启自动校验。常见遗漏点:
- 没在
Program.cs调用services.AddControllers().AddFluentValidation(...)(MVC)或AddFluentValidationClientsideAdapters()(Razor Pages) - 控制器参数没加
[ApiController]特性 —— 它启用了默认的ModelState自动验证拦截 - 参数是
object或基类类型(如PersonBase),但注册时只配了子类PersonValidator,需手动指定validatorType或用RegisterValidatorsFromAssemblyContaining<PersonValidator>()
验证失败时,默认返回 400 BadRequest 并附带 ModelState 错误。若想统一响应格式,要替换 InvalidModelStateResponseFactory,而不是重写 ActionFilter。
自定义错误消息里的占位符为什么不替换?
{PropertyName}、{PropertyValue} 这些占位符失效,通常是因为:
- 手误写成大括号全角字符(中文输入法下容易中招)
- 在
WithMessage()中用了字符串插值($"xxx {x.Name}"),导致占位符被提前求值,变成字面量 - 使用了
WithName()但没配合OverridePropertyName,导致{PropertyName}仍显示原始属性名
正确写法:
RuleFor(x => x.Age)
.GreaterThan(0)
.WithMessage("{PropertyName} 必须大于 {ComparisonValue}") // ✅ 占位符原样传入
.WithName("年龄"); // ✅ 配合生效
复杂消息建议用资源文件 + WithLocalizedMessage(),避免硬编码和多语言维护困难。
null 导航属性的隐式链式访问、以及 ASP.NET Core 中验证触发链的完整闭环——缺一环,就只是个漂亮的装饰器。


















