- 后端
【免费下载链接】FluentValidation
A popular .NET validation library for building strongly-typed validation rules.
FluentValidation 是 .NET 生态中用于构建强类型验证规则的流行库,它可以被无缝接入 ASP.NET Core 应用程序,对进入系统的模型进行校验。本篇技术指南以 docs/aspnet.md 为骨架,结合本仓库的 DI 扩展与核心源码,系统讲解在 ASP.NET Core(MVC / Razor Pages / Minimal APIs)中使用 FluentValidation 的三种主流方式:手动验证、基于 ASP.NET 验证管道的自动验证、基于 Action Filter 的自动验证,并深入剖析验证器注册原理、ModelState 集成、客户端验证元数据以及ToDictionary等关键 API。读完本文,你将能根据项目形态(传统 MVC 还是 Minimal API)正确选型并落地一套可维护、可调试的验证方案。
三种验证方式概览
在 ASP.NET Core 应用中,FluentValidation 验证传入模型主要有三条路径,各有权衡:
| 方式 | 触发时机 | 优点 | 局限 |
|---|---|---|---|
| 手动验证(Manual validation) | 在控制器/API 端点内部显式调用 | 最直观,逻辑完全可见,易于调试与测试 | 需要在每个入口重复调用 |
| 自动验证(ASP.NET 验证管道) | 模型绑定阶段、控制器 Action 执行之前 | 无缝接入,减少样板代码 | 非异步(异步规则会抛异常)、仅支持 MVC/Razor Pages、难以调试,官方已不推荐新项目使用 |
| 自动验证(Action Filter) | 通过过滤器在端点执行前拦截 | 支持异步,弥补管道方案的同步缺陷 | 官方不内置,需借助第三方包 |
手动验证时,验证器被注入到控制器(或 API 端点)中,由开发者显式调用并处理结果——这是最直接、最容易看清执行过程的方案;而自动验证则由 ASP.NET 在管道更早的阶段自动调用 FluentValidation,使模型在进入控制器 Action 之前就已校验完毕。
起步:定义一个验证器
后续所有示例都围绕一个Person对象及其验证器PersonValidator展开,定义如下:
public class Person { public int Id { get; set; } public string Name { get; set; } public string Email { get; set; } public int Age { get; set; } } public class PersonValidator : AbstractValidator<Person> { public PersonValidator() { RuleFor(x => x.Id).NotNull(); RuleFor(x => x.Name).Length(0, 10); RuleFor(x => x.Email).EmailAddress(); RuleFor(x => x.Age).InclusiveBetween(18, 60); } }验证规则定义在验证器构造器中,通过RuleFor传入属性选择表达式来声明式地组合规则。关于如何创建第一个验证器、链式调用多个验证器、使用SetValidator组合子验证器等基础概念,可参考 docs/start.md。
如果你在使用 MVC、Web API 或 Razor Pages,需要把验证器注册到Startup.ConfigureServices中的 Service Provider(Minimal APIs 的注册方式见下文专节):
public void ConfigureServices(IServiceCollection services) { // 如果使用 MVC 或 WebApi,通常已有 AddMvc() 或 AddControllers() 调用 services.AddMvc(); // ... 其他配置 ... services.AddScoped<IValidator<Person>, PersonValidator>(); }这里通过AddScoped将PersonValidator注册到服务容器中。
注意:每个验证器必须注册为
IValidator<T>,其中T是被验证的类型。即PersonValidator继承自AbstractValidator<Person>,就应注册为IValidator<Person>。
IValidator<T>接口正是 FluentValidation 的核心契约,定义见 src/FluentValidation/IValidator.cs,它提供同步的Validate(T instance)、异步的ValidateAsync(T instance, CancellationToken)以及CreateDescriptor()等成员;AbstractValidator<T>则实现了该接口并托管规则集合与执行逻辑,见 src/FluentValidation/AbstractValidator.cs。
自动注册:扫描程序集中的全部验证器
手动逐个AddScoped在验证器数量庞大时非常繁琐。此时可引入FluentValidation.DependencyInjectionExtensions包(本仓库中对应工程为 src/FluentValidation.DependencyInjectionExtensions,入口见 ServiceCollectionExtensions.cs),借助其扩展方法一次性注册指定程序集中的所有验证器:
public void ConfigureServices(IServiceCollection services) { services.AddMvc(); // ... 其他配置 ... services.AddValidatorsFromAssemblyContaining<PersonValidator>(); }AddValidatorsFromAssemblyContaining<PersonValidator>()会以PersonValidator所在程序集为扫描范围,自动将所有验证器注册进服务容器。更完整的 DI 集成说明(包括生命周期参数、过滤、单例注意事项等)可参考 docs/di.md。
源码级原理:扫描与注册是如何发生的
从源码看,自动注册分为“扫描”与“注册”两步:
扫描:
AssemblyScanner.FindValidatorsInAssembly通过assembly.GetExportedTypes()获取程序集中所有公开导出的类型(若传入includeInternalTypes: true则改用GetTypes()连内部类型一并纳入),然后筛选出所有非抽象、非泛型类型定义且实现了IValidator<>泛型接口的类型,产出AssemblyScanResult(含InterfaceType与ValidatorType),实现见 src/FluentValidation/AssemblyScanner.cs。注册:
AddScanResult为每个扫描结果注册两条服务描述——先用TryAddEnumerable将实现类注册到其IValidator<T>接口(支持解析IEnumerable<IValidator<T>>),再用TryAdd注册为自身类型。TryAdd语义保证重复调用扫描方法不会产生重复注册,见 ServiceCollectionExtensions.cs。
这一行为在测试中得到了验证:src/FluentValidation.Tests/DependencyInjectionExtensions/ServiceCollectionExtensionsTests.cs 中的Should_register_validator_service_types_only_once与Should_register_validators_as_enumerable_interface_type_only_once断言了重复调用AddValidatorsFromAssemblyContaining时服务仅注册一次、且同一IValidator<T>接口可解析出多个实现。
生命周期与过滤选项
AddValidatorsFromAssemblyContaining<T>的完整签名如下(默认注册为Scoped,即 Web 应用中按请求作用域解析):
public static IServiceCollection AddValidatorsFromAssemblyContaining<T>( this IServiceCollection services, ServiceLifetime lifetime = ServiceLifetime.Scoped, Func<AssemblyScanner.AssemblyScanResult, bool> filter = null, bool includeInternalTypes = false)lifetime:可显式指定Singleton或Transient。若注册为 Singleton,必须确保不注入任何 Transient 或请求作用域的依赖,否则会产生“单例持有非单例依赖”的经典 DI 陷阱;官方建议把验证器注册为 Transient 是最简单安全的选择。filter:提供过滤函数以排除部分验证器,例如跳过CustomerValidator:services.AddValidatorsFromAssemblyContaining<MyValidator>(ServiceLifetime.Scoped, filter => filter.ValidatorType != typeof(CustomerValidator));includeInternalTypes:默认false,即只扫描 public 类型。
此外还有多种重载:AddValidatorsFromAssemblyContaining(typeof(UserValidator))以类型实例扫描、AddValidatorsFromAssembly(Assembly.Load("SomeAssembly"))直接按程序集引用扫描、AddValidatorsFromAssemblies(IEnumerable<Assembly>)一次扫描多个程序集(见 ServiceCollectionExtensions.cs)。
手动验证:把结果写入 ModelState
手动验证的核心思路是:把验证器注入控制器(或 Razor Page),在 Action 中显式调用并处理结果。以创建Person的控制器为例:
public class PeopleController : Controller { private IValidator<Person> _validator; private IPersonRepository _repository; public PeopleController(IValidator<Person> validator, IPersonRepository repository) { // 注入验证器,同时注入用于持久化的 DB 上下文 _validator = validator; _repository = repository; } public ActionResult Create() { return View(); } [HttpPost] public async Task<IActionResult> Create(Person person) { ValidationResult result = await _validator.ValidateAsync(person); if (!result.IsValid) { // 将验证结果复制进 ModelState。 // ASP.NET 使用 ModelState 集合向视图填充错误信息。 result.AddToModelState(this.ModelState); // 验证失败时重新渲染视图 return View("Create", person); } _repository.Save(person); // 保存 person 到数据库,或执行其他逻辑 TempData["notice"] = "Person successfully created"; return RedirectToAction("Index"); } }因为验证器已注册到 Service Provider,它会通过构造函数注入到控制器中;随后在CreateAction 里用ValidateAsync触发校验。
验证失败时,需要把错误信息回传给视图。Flutter 的ValidationResult本身不直接感知 ASP.NET 的ModelState,因此文档给出了一个扩展方法,把错误集合复制进ModelStateDictionary:
public static class Extensions { public static void AddToModelState(this ValidationResult result, ModelStateDictionary modelState) { foreach (var error in result.Errors) { modelState.AddModelError(error.PropertyName, error.ErrorMessage); } } }ValidationResult的结构见 src/FluentValidation/Results/ValidationResult.cs:IsValid等价于Errors.Count == 0,Errors是ValidationFailure的集合,其中PropertyName、ErrorMessage、AttemptedValue、Severity等字段定义见 src/FluentValidation/Results/ValidationFailure.cs。该扩展方法正是逐条取出PropertyName/ErrorMessage写入 ModelState 的。
对应的 Razor 视图会从ModelState中拾取错误消息并渲染到对应属性旁:
@model Person <div asp-validation-summary="ModelOnly"></div> <form asp-action="Create"> Id: <input asp-for="Id" /> <span asp-validation-for="Id"></span> <br /> Name: <input asp-for="Name" /> <span asp-validation-for="Name"></span> <br /> Email: <input asp-for="Email" /> <span asp-validation-for="Email"></span> <br /> Age: <input asp-for="Age" /> <span asp-validation-for="Age"></span> <br /><br /> <input type="submit" value="submit" /> </form>提示:如果你写的是 API 控制器(返回 JSON 而非渲染视图),失败时应返回
ValidationProblemDetails或BadRequest,而不是视图结果。Minimal API 场景下则使用Results.ValidationProblem(...)(详见下文)。
自动验证:两种实现路径
自动验证会在控制器 Action 执行之前实例化并调用验证器,即当你的 Action 被调用时 ModelState 中已经填充好验证结果。官方文档给出了两条实现路径:
- 使用 ASP.NET 的验证管道(不再推荐)
- 使用 Action Filter(由第三方包支持)
路径一:ASP.NET 验证管道(不再推荐)
FluentValidation.AspNetCore包通过接入 ASP.NET Core MVC 内建的验证过程(模型绑定阶段)实现自动验证。这种方式更“无缝”,但存在几个明显缺点:
- 管道不支持异步:如果验证器包含异步规则,运行时将抛出异常——自动验证无法执行异步验证器。
- 仅限 MVC:只对 MVC Controllers 和 Razor Pages 生效,不适用于 Minimal APIs 或 Blazor 等更新的 ASP.NET 部件。
- 难以调试:自动验证的“魔法”特性使问题排查变得困难,大量逻辑在幕后自动完成。
警告:官方已不再建议新项目使用此方案,但它仍可供遗留项目使用。
FluentValidation.AspNetCore包的安装与使用说明在其独立项目页面,该包当前已停止维护,仅保持可用状态。
路径二:Action Filter
另一种自动验证方案是使用 Action Filter。由于过滤器支持异步执行,它规避了上述验证管道“不能跑异步规则”的同步限制。此方案官方不内置支持,可使用第三方包(如SharpGrip.FluentValidation.AutoValidation)实现——用法为在端点或全局配置中启用过滤器,由过滤器在 Action 执行前自动对模型参数执行验证并填充 ModelState,具体接入方式以该包文档为准。
客户端验证:元数据与 AJAX 两种思路
FluentValidation 本质上是服务端校验库,本身不提供任何客户端验证逻辑。但它能够像 ASP.NET 默认验证特性那样,为生成 HTML 元素提供可用于客户端框架(如 jQuery Validate)的验证元数据。
- 要使用该元数据,需要安装独立的
FluentValidation.AspNetCore包(注意:该包已不再支持,但仍可使用)。 - 另一种思路是放弃客户端验证,改用 AJAX 把完整服务端规则跑一遍(例如借助
FormHelper这类库)。这样既保留 FluentValidation 的全部能力,又维持了响应式交互体验。
Minimal APIs 中的验证
在 Minimal APIs 中使用 FluentValidation 时,同样可以把验证器注册进服务容器(若无依赖也可以直接实例化),然后在 API 端点内显式调用:
var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); // 注册验证器到服务容器(或用上文任一自动注册方法) builder.Services.AddScoped<IValidator<Person>, PersonValidator>(); // 为演示注册一个 DB 访问仓储 // 请替换为你的应用中实际使用的仓储实现 builder.Services.AddScoped<IPersonRepository, PersonRepository>(); app.MapPost("/person", async (IValidator<Person> validator, IPersonRepository repository, Person person) => { ValidationResult validationResult = await validator.ValidateAsync(person); if (!validationResult.IsValid) { return Results.ValidationProblem(validationResult.ToDictionary()); } repository.Save(person); return Results.Created($"/{person.Id}", person); });端点通过参数注入拿到IValidator<Person>,调用ValidateAsync校验,失败时用Results.ValidationProblem返回符合 RFC 7807 标准的ProblemDetails响应。
关键 API:ToDictionary
上面用到的ValidationResult.ToDictionary()方法会按属性名分组错误消息,输出IDictionary<string, string[]>(键为属性名、值为该属性关联的错误消息数组),这正是ValidationProblem所期望的数据形状。该方法的源码实现位于 ValidationResult.cs,以GroupBy(x => x.PropertyName)聚合Errors而成。
版本注意:ToDictionary自FluentValidation 11.1起才内置在ValidationResult上。若使用更早版本,需要自行实现等价扩展方法:
public static class FluentValidationExtensions { public static IDictionary<string, string[]> ToDictionary(this ValidationResult validationResult) { return validationResult.Errors .GroupBy(x => x.PropertyName) .ToDictionary( g => g.Key, g => g.Select(x => x.ErrorMessage).ToArray() ); } }除手动调用外,也可借助第三方包(如SharpGrip.FluentValidation.AutoValidation、ForEvolve.FluentValidation.AspNetCore.Http)为某个端点或一组端点挂上验证过滤器,实现 Minimal API 场景下的自动验证,避免在每个端点重复样板代码。
补充:同步校验与异常抛出的便捷入口
虽然本文聚焦 ASP.NET Core 集成,但了解IValidator<T>的便捷入口有助于在端点内灵活处理:
- 端点内如果不需要异步,可调用
validator.Validate(person)(同步重载定义于 AbstractValidator.cs)。 - 若希望校验失败时直接抛异常,可调用扩展方法
ValidateAndThrow/ValidateAndThrowAsync,其实现等价于Validate(instance, options => options.ThrowOnFailures()),见 DefaultValidatorExtensions_Validate.cs。这一模式在 MVC 集成中很少使用(通常要渲染错误给用户),但在命令/服务层边界、批处理或需要“校验失败即中断”的场景中非常实用。
小结与选型建议
综合以上内容,在 ASP.NET Core 中落地 FluentValidation 验证的推荐路径可归纳为:
- 注册:用
AddValidatorsFromAssemblyContaining<T>()批量注册(默认Scoped),复杂场景再按需调整生命周期与过滤规则;保持验证器无状态、依赖简单,避免 Singleton 陷阱。 - MVC / Razor Pages:优先手动注入
IValidator<T>并在 Action 中调用ValidateAsync,用AddToModelState扩展把错误同步进 ModelState 并回显到视图;旧项目可继续使用基于验证管道的自动验证,但需明确其不支持异步规则的约束。 - API / Minimal APIs:手动调用验证器,失败时以
Results.ValidationProblem(validationResult.ToDictionary())返回标准问题详情(注意ToDictionary需要 FluentValidation 11.1+);需要免样板时可选用第三方 Action Filter 包。 - 客户端验证:优先走 AJAX 复用服务端规则,或按需使用不再维护的
FluentValidation.AspNetCore元数据特性。
如需进一步深入,可继续阅读仓库中的 docs/aspnet.md(本文骨架来源)、docs/di.md(DI 与自动注册全解)与 docs/start.md(验证器基础语法)。
- 后端
【免费下载链接】FluentValidation
A popular .NET validation library for building strongly-typed validation rules.
相关推荐
FluentValidation在ASP.NET Core中的集成实践:手动验证、自动验证与过滤器全解析
FluentValidation在ASP.NET Core中的集成实践:手动验证、自动验证与过滤器全解析 FluentValidation 是 .NET 生态中
后端ASP.NET Boilerplate 数据验证指南:DTO 自动校验、自定义验证与 FluentValidation 集成
ASP.NET Boilerplate 数据验证指南:DTO 自动校验、自定义验证与 FluentValidation 集成 导读 在 ASP.NET Boil
后端Web框架依赖注入认证鉴权终极指南:FluentValidation与ASP.NET Core自动模型验证的完整教程
终极指南:FluentValidation与ASP.NET Core自动模型验证的完整教程 FluentValidation是一个功能强大的.NET库,它提供了
后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考