You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 17:37:32