如何在FluentValidation中强制为所有规则定义ErrorCode?
强制FluentValidation规则必须定义ErrorCode的实现方案
针对你要求每条验证规则必须指定ErrorCode、禁止留空的需求,有以下几种可行的实现方式,同时能配合错误字典支持客户端本地化:
1. 自定义验证基类封装强制传参的规则方法
通过封装FluentValidation的原生规则方法,强制开发者在定义规则时必须传入ErrorCode,从编码层面避免遗漏。
实现代码
using FluentValidation; using System.Linq.Expressions; // 自定义验证器基类,封装必须传ErrorCode的规则方法 public abstract class AbstractValidatorWithRequiredErrorCode<T> : AbstractValidator<T> { // 替代原生RuleFor,强制传入ErrorCode public IRuleBuilderInitial<T, TProperty> RuleForWithErrorCode<TProperty>( Expression<Func<T, TProperty>> propertyExpression, string errorCode) { // 直接绑定ErrorCode,后续可链式添加验证逻辑 return RuleFor(propertyExpression).WithErrorCode(errorCode); } } // 使用示例 public class UserValidator : AbstractValidatorWithRequiredErrorCode<User> { public UserValidator() { // 必须传入ErrorCode,否则编译报错(若要彻底限制可隐藏原生RuleFor方法) RuleForWithErrorCode(x => x.Name, "ERR_USER_NAME_EMPTY") .NotEmpty(); RuleForWithErrorCode(x => x.Email, "ERR_USER_EMAIL_INVALID") .EmailAddress(); } }
优缺点
- 优点:编码阶段就能强制约束,实现简单,无额外依赖
- 缺点:无法完全阻止开发者直接调用原生
RuleFor方法(可通过修改基类权限或代码审查补充)
2. Roslyn静态代码分析器(最彻底的强制方案)
通过编写Roslyn分析器,在编译阶段检测所有FluentValidation规则是否设置了WithErrorCode,未设置则直接抛出编译错误,从构建层面彻底杜绝遗漏。
核心分析逻辑
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; [DiagnosticAnalyzer(LanguageNames.CSharp)] public class FluentValidationErrorCodeAnalyzer : DiagnosticAnalyzer { public const string DiagnosticId = "FV001"; private static readonly LocalizableString Title = "Missing Required ErrorCode"; private static readonly LocalizableString MessageFormat = "Validation rule for '{0}' must specify an ErrorCode via WithErrorCode()"; private static readonly LocalizableString Description = "All FluentValidation rules must define an explicit ErrorCode for client localization."; private const string Category = "FluentValidation"; private static readonly DiagnosticDescriptor Rule = new( DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Error, isEnabledByDefault: true, description: Description); public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule); public override void Initialize(AnalysisContext context) { context.EnableConcurrentExecution(); context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression); } private void AnalyzeInvocation(SyntaxNodeAnalysisContext context) { var invocation = (InvocationExpressionSyntax)context.Node; var methodSymbol = context.SemanticModel.GetSymbolInfo(invocation).Symbol as IMethodSymbol; // 仅检测FluentValidation的RuleFor调用 if (methodSymbol == null || !methodSymbol.ContainingType.FullName.StartsWith("FluentValidation.AbstractValidator") || methodSymbol.Name != "RuleFor") { return; } // 回溯链式调用,检查是否存在WithErrorCode var currentNode = invocation.Parent; bool hasErrorCode = false; string propertyName = string.Empty; // 获取验证的属性名称 if (invocation.ArgumentList.Arguments.First().Expression is LambdaExpressionSyntax lambda) { propertyName = ((MemberAccessExpressionSyntax)lambda.Body).Name.Identifier.Text; } while (currentNode is MemberAccessExpressionSyntax memberAccess) { if (memberAccess.Name.Identifier.Text == "WithErrorCode") { hasErrorCode = true; break; } currentNode = memberAccess.Parent; } if (!hasErrorCode) { context.ReportDiagnostic(Diagnostic.Create(Rule, invocation.GetLocation(), propertyName)); } } }
优缺点
- 优点:编译阶段强制约束,完全无法绕过,适合团队协作场景
- 缺点:需要编写并集成Roslyn分析器,有一定学习成本
3. 运行时校验(兜底方案)
在验证器初始化时,通过反射检查所有规则的ErrorCode是否为空,若存在未设置的情况直接抛出异常,在程序启动阶段暴露问题。
实现代码
using FluentValidation; using System.Reflection; public abstract class AbstractValidatorWithRequiredErrorCode<T> : AbstractValidator<T> { protected AbstractValidatorWithRequiredErrorCode() { // 在构造完成后校验所有规则的ErrorCode ValidateErrorCodePresence(); } private void ValidateErrorCodePresence() { // 反射获取验证器内部的规则集合 var rulesProperty = typeof(AbstractValidator<T>).GetProperty( "Rules", BindingFlags.Instance | BindingFlags.NonPublic); if (rulesProperty == null) return; var rules = (IEnumerable<IValidationRule>)rulesProperty.GetValue(this)!; foreach (var rule in rules) { foreach (var validator in rule.Validators) { var errorCodeProperty = validator.GetType().GetProperty("ErrorCode"); if (errorCodeProperty == null) continue; var errorCode = errorCodeProperty.GetValue(validator) as string; if (string.IsNullOrWhiteSpace(errorCode)) { throw new InvalidOperationException( $"Validation rule for property '{rule.Property.Name}' is missing required ErrorCode."); } } } } }
配合错误字典实现客户端本地化
定义全局错误码字典,API返回验证错误时仅返回ErrorCode,客户端根据自身本地化逻辑匹配对应消息:
错误字典示例
using System.Globalization; public static class ErrorCodeDictionary { // 默认英文消息 private static readonly Dictionary<string, string> _defaultMessages = new() { { "ERR_USER_NAME_EMPTY", "User name cannot be empty." }, { "ERR_USER_EMAIL_INVALID", "Invalid email format." } }; // 中文本地化消息 private static readonly Dictionary<string, string> _zhCnMessages = new() { { "ERR_USER_NAME_EMPTY", "用户名不能为空。" }, { "ERR_USER_EMAIL_INVALID", "邮箱格式无效。" } }; // 根据文化信息获取本地化消息 public static string GetLocalizedMessage(string errorCode, CultureInfo culture) { if (culture.Name.Equals("zh-CN", StringComparison.OrdinalIgnoreCase)) { return _zhCnMessages.TryGetValue(errorCode, out var msg) ? msg : GetDefaultMessage(errorCode); } return GetDefaultMessage(errorCode); } private static string GetDefaultMessage(string errorCode) { return _defaultMessages.TryGetValue(errorCode, out var msg) ? msg : $"Unknown error: {errorCode}"; } }
API返回示例
{ "errors": [ { "propertyName": "Name", "errorCode": "ERR_USER_NAME_EMPTY" } ] }
客户端可根据返回的errorCode,结合自身的本地化资源(如多语言文件)展示对应消息。
内容的提问来源于stack exchange,提问作者Ewerton
相关产品推荐
相关产品推荐

