- 后端
【免费下载链接】FluentValidation
A popular .NET validation library for building strongly-typed validation rules.
导读
本指南基于 FluentValidation 官方文档 docs/configuring.md,系统讲解如何定制验证失败时生成的错误消息与属性名:从最常用的WithMessage覆盖默认消息、占位符(Placeholder)机制,到WithName/OverridePropertyName的差别,再到集合校验时OverrideIndexer控制索引显示格式。读完本文,你将能够为每个验证规则产出面向用户的可读错误提示,并理解这些 API 在源码层面(DefaultValidatorOptions.cs、MessageFormatter.cs、PropertyChain.cs)是如何被实现和串联的。
一、用 WithMessage 覆盖默认错误消息
FluentValidation 的每个内置校验器都有默认的错误文案(多数来自本地化资源,详见 localization)。当默认文案不符合业务需要时,可以在规则链上调用WithMessage指定自定义消息:
RuleFor(customer => customer.Surname).NotNull().WithMessage("Please ensure that you have entered your Surname");在源码层面,WithMessage字符串重载定义于 DefaultValidatorOptions.cs:它通过Configurable(rule).Current.SetErrorMessage(errorMessage)将自定义消息挂到"紧随其后的那条规则组件"上,因此它只作用于链上直接相邻的校验器。例如NotNull().NotEmpty()之后调用WithMessage,只会影响NotEmpty那一条。
消息占位符机制
自定义消息中可以嵌入{PropertyName}这类占位符,运行时会被替换为实际值:
RuleFor(customer => customer.Surname).NotNull().WithMessage("Please ensure you have entered your {PropertyName}"); // 失败时输出:Please ensure you have entered your Surname占位符的替换由 MessageFormatter.cs 完成:BuildMessage内部用正则{([^{}:]+)(?::([^{}]+))?}扫描消息模板,并从PlaceholderValues字典中取值替换。值得一提的细节是:正则中的第二个分组支持格式说明符,即占位符可以写成{PropertyValue:0.00}这类带格式化字符串的形式,最终通过string.Format("{0:格式}", value)完成格式化(见 MessageFormatter.cs)。
各类校验器支持的占位符
所有校验器通用:
{PropertyName}—— 被验证属性的名称{PropertyValue}—— 被验证属性的值
这两者同样适用于谓词校验器(Must)、邮箱校验器和正则校验器。
比较类校验器(Equal、NotEqual、GreaterThan、GreaterThanOrEqual、LessThan、LessThanOrEqual、ExclusiveBetween、InclusiveBetween等)额外支持:
{ComparisonValue}—— 用于比较的值{ComparisonProperty}—— 被比较的属性名(如果存在)
Length 校验器专用:
{MinLength}—— 最小长度{MaxLength}—— 最大长度{TotalLength}—— 用户实际输入的长度
每个内置校验器支持哪些占位符的完整清单,可查阅 built-in-validators 中对应校验器的说明。
用 lambda 动态构造消息
WithMessage还提供两个 lambda 重载(DefaultValidatorOptions.cs),可以引用被校验对象上的其他属性或常量值:
// 在消息中引用常量: RuleFor(customer => customer.Surname) .NotNull() .WithMessage(customer => string.Format("This message references some constant values: {0} {1}", "hello", 5)) // 结果:"This message references some constant values: hello 5" // 引用对象上的其他属性(字符串插值): RuleFor(customer => customer.Surname) .NotNull() .WithMessage(customer => $"This message references some other properties: Forename: {customer.Forename} Discount: {customer.Discount}") // 结果:"This message references some other properties: Forename: Jeremy Discount: 100"lambda 重载在实现上通过SetErrorMessage((ctx, val) => ...)注册一个延迟执行的委托(DefaultValidatorOptions.cs),这意味着消息在验证失败时才求值,而非定义规则时求值。
注意:
WithMessage只会覆盖某一条规则的文案。如果你希望全局替换所有默认消息(例如统一走公司内部的多语言资源),应使用 FluentValidation 的本地化能力,参考 localization。
二、用 WithName 定制错误消息中的属性名
默认错误消息会把被验证的属性名嵌入其中。例如:
RuleFor(customer => customer.Surname).NotNull();失败时默认消息形如'Surname' must not be empty.。若只想替换消息里的属性名,可调用WithName:
RuleFor(customer => customer.Surname).NotNull().WithName("Last name"); // 失败时输出:'Last name' must not be empty.测试用例 AbstractValidatorTester.cs 验证了这一点:WithName("First Name")后,Forename的 NotNull 失败消息为'First Name' must not be empty.
WithName同样有 lambda 重载(DefaultValidatorOptions.cs),可动态计算显示名:
RuleFor(customer => customer.Surname).NotNull().WithName(customer => "Last name for customer " + customer.Id);测试 AbstractValidatorTester.cs 展示了用WithName(x => x.Surname)把显示名取自另一个属性的用法。
WithName 与 OverridePropertyName 的区别
WithName只影响消息中的显示名。当检查ValidationResult.Errors时,该失败项仍关联到原始属性Surname——也就是说PropertyName、PropertyName路径、校验选择器(validator selector)等逻辑都照旧工作。
OverridePropertyName则是完全重命名属性(DefaultValidatorOptions.cs),它直接改写规则底层记录的PropertyName:
RuleFor(customer => customer.Surname).NotNull().OverridePropertyName("foo");从源码看,OverridePropertyName通过Configurable(rule).PropertyName = propertyName设置规则属性名(DefaultValidatorOptions.cs),因此ValidationResult.Errors[0].PropertyName会变成foo(见测试 AbstractValidatorTester.cs)。它还有一个接收 lambda 表达式的重载,会自动提取表达式中成员名:
RuleFor(customer => customer.Surname).NotNull().OverridePropertyName(x => x.Forename); // Errors[0].PropertyName == "Forename"源码注释明确提醒:OverridePropertyName属于高级特性,大多数场景下你真正想要的是WithName(DefaultValidatorOptions.cs)。另有使用场景:当RuleFor无法从表达式推断属性名时(例如对IEnumerable调用RuleForEach),需要显式调用OverridePropertyName,否则运行时会抛出 "Could not infer property name..." 异常(该异常消息可见于测试 ForEachRuleTests.cs)。
全局可插拔的显示名解析器
属性显示名的解析逻辑本身是可替换的。默认情况下,FluentValidation 从传入RuleFor的MemberExpression中提取成员名,其默认实现是ValidatorConfiguration.DefaultDisplayNameResolver(返回 null,从而回退到成员名,见 ValidatorOptions.cs 与 DefaultValidatorExtensions.cs)。
你可以通过ValidatorOptions.Global.DisplayNameResolver全局修改这一行为:
ValidatorOptions.Global.DisplayNameResolver = (type, member, expression) => { if (member != null) { return member.Name + "Foo"; } return null; };上面的例子会把所有属性名都加上后缀Foo(这只是示意,不是真实业务场景),但它说明了属性显示名解析的扩展点。DisplayNameResolver属性定义于 ValidatorOptions.cs,签名是Func<Type, MemberInfo, LambdaExpression, string>,依次接收容器类型、成员信息与 lambda 表达式。
当显示名未显式设置时,最终显示名由 PropertyRule.cs 中的GetDisplayName决定:优先使用_displayNameFactory(即DisplayNameResolver的结果),其次使用_displayName(WithName设置的值),最后回退到按 PascalCase 拆分的属性名。
三、用 OverrideIndexer 定制集合校验的索引格式
用RuleForEach校验集合时,失败项的属性路径会包含方括号形式的索引,例如Foo.BarList[5].Baz。若希望调整这种格式(比如去掉方括号、改用元素的自有属性名),可以使用OverrideIndexer:
RuleForEach(x => x.BarList) .OverrideIndexer((foo, barList, bar, i) => bar.Name)回调签名是Func<T, IEnumerable<TCollectionElement>, TCollectionElement, int, string>,四个参数依次为:被校验对象、整个集合、当前元素、当前索引。返回值将作为属性链上的索引器文本。上面的例子去掉方括号,直接用元素属性名充当索引。
源码视角:索引器如何进入属性链
在 CollectionPropertyRule.cs 的集合遍历中,默认行为是indexer = index.ToString()且useDefaultIndexFormat = true;当设置了IndexBuilder(即OverrideIndexer注册的回调,见 DefaultValidatorOptions.cs)时,则调用回调并关闭默认格式。随后context.PropertyChain.AddIndexer(indexer, useDefaultIndexFormat)将其拼接到属性链尾部。
PropertyChain.cs 的AddIndexer决定拼接格式:默认(surroundWithBrackets = true)会生成属性[索引],例如NickNames[0];传false则直接拼接索引文本,不带方括号。整个链最终由ToString用ValidatorOptions.Global.PropertyChainSeparator(默认".",见 ValidatorOptions.cs)连接成完整路径。
测试 ForEachRuleTests.cs 验证了这一点:对NickNames集合({null, "foo", null})调用
RuleForEach(x => x.NickNames) .OverrideIndexer((x, collection, element, index) => "<" + index + ">") .NotNull()后,失败项的PropertyName分别为NickNames<0>与NickNames<2>,索引格式完全由回调决定。该测试还有一个异步版本(MustAsync),说明OverrideIndexer在同步与异步校验路径中行为一致。
四、常用定制组合示例
以下是一个综合示例,将消息覆盖、属性名覆盖与索引覆盖结合使用:
public class CustomerValidator : AbstractValidator<Customer> { public CustomerValidator() { // 1. 静态消息 + 占位符 RuleFor(c => c.Surname).NotNull().WithMessage("Please ensure you have entered your {PropertyName}"); // 2. 引用其他属性的动态消息 RuleFor(c => c.Surname).NotNull() .WithMessage(c => $"Forename is {c.Forename}, please fix the surname"); // 3. 仅覆盖消息中的显示名(PropertyName 仍是 Surname) RuleFor(c => c.Surname).NotNull().WithName("Last name"); // 4. 完全重命名属性(PropertyName 变为 "LastName") RuleFor(c => c.Surname).NotNull().OverridePropertyName("LastName"); // 5. 集合校验的索引格式定制 RuleForEach(c => c.Orders) .OverrideIndexer((c, orders, order, i) => order.OrderNumber) .SetValidator(new OrderValidator()); } }总结
WithMessage覆盖单条规则的错误消息,支持{PropertyName}、{PropertyValue}、比较类的{ComparisonValue}/{ComparisonProperty}以及 Length 类的{MinLength}/{MaxLength}/{TotalLength}等占位符,也可用 lambda 动态求值;WithName只替换消息中的显示名,不影响ValidationResult中的属性关联;OverridePropertyName才真正改写属性名,属于高级用法;OverrideIndexer通过回调重写集合校验的属性链索引文本,其拼接逻辑位于 PropertyChain.AddIndexer;- 属性显示名的全局解析逻辑可通过
ValidatorOptions.Global.DisplayNameResolver扩展; - 如需全局替换所有默认消息,请参考 localization 而非逐条
WithMessage。
以上能力对应的完整 API 定义集中在 DefaultValidatorOptions.cs,消息格式化核心在 MessageFormatter.cs,相关行为均有测试覆盖(如 AbstractValidatorTester.cs、ForEachRuleTests.cs),可据此进一步深入阅读。
- 后端
【免费下载链接】FluentValidation
A popular .NET validation library for building strongly-typed validation rules.
相关推荐
FluentValidation 本地化完全指南:默认多语言消息、WithMessage 自定义与 LanguageManager 深度解析
FluentValidation 本地化完全指南:默认多语言消息、WithMessage 自定义与 LanguageManager 深度解析 导读 本文围绕 F
后端FluentValidation自定义错误消息实战:WithMessage占位符与本地化国际化完整指南
FluentValidation自定义错误消息实战:WithMessage占位符与本地化国际化完整指南 FluentValidation 是一款主流的 .NET
后端FluentValidation 自定义错误码(ErrorCode)完全指南:从 WithErrorCode 到消息解析原理
FluentValidation 自定义错误码(ErrorCode)完全指南:从 WithErrorCode 到消息解析原理 本文面向使用 FluentVali
后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考