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

使用C# Fluent Validation如何生成集合成员校验失败的自定义错误消息

解决方案

完全不需要弃用Fluent Validation,你遇到的问题是两个基础使用误区导致的:

  1. 注入使用的Validator默认是单例,用实例字段存储单次校验的错误数据会有严重的线程安全问题,多请求同时校验时数据会互相覆盖
  2. WithMessage直接传入方法返回值时,会在Validator构造阶段就执行方法,而不是在校验失败时动态计算消息

正确的实现方式是利用Fluent Validation内置的校验上下文(ValidationContext) 存储单次校验过程中产生的错误字段,再通过WithMessage的动态委托重载生成错误消息,完整实现如下:

核心修改点

1. 调整校验规则,使用带上下文的异步校验重载

2. 在校验方法中将错误字段存入当前校验上下文

3. 在校验失败时从上下文读取错误字段动态生成消息


调整后的校验规则代码

RuleFor(request => request)
    .MustAsync(AllReadableFieldsMustExist)
    // 用Func<ValidationContext<SalesforceDataRequestParameters>, string>重载,校验失败时才执行
    .WithMessage(ctx => 
    {
        var badFields = (HashSet<string>)ctx.RootContextData[$"BadFields_{ReadableFields}"];
        return $"Invalid {ReadableFields} names: {string.Join(",", badFields)}";
    });

// Writable、Indexed字段的规则同理修改
RuleFor(request => request)
    .MustAsync(AllWritableFieldsMustExist)
    .WithMessage(ctx => 
    {
        var badFields = (HashSet<string>)ctx.RootContextData[$"BadFields_{WritableFields}"];
        return $"Invalid {WritableFields} names: {string.Join(",", badFields)}";
    });

RuleFor(request => request)
    .MustAsync(AllIndexedFieldsMustExist)
    .WithMessage(ctx => 
    {
        var badFields = (HashSet<string>)ctx.RootContextData[$"BadFields_{IndexedFields}"];
        return $"Invalid {IndexedFields} names: {string.Join(",", badFields)}";
    });

调整后的校验方法代码

// 先调整三个校验方法的签名,传入上下文参数
private Task<bool> AllReadableFieldsMustExist(SalesforceDataRequestParameters parameters, ValidationContext<SalesforceDataRequestParameters> context, CancellationToken cancellationToken) =>
    AllMustExist(parameters.SObjectName, parameters.ReadableFieldNames, ReadableFields, context, cancellationToken);

private Task<bool> AllWritableFieldsMustExist(SalesforceDataRequestParameters parameters, ValidationContext<SalesforceDataRequestParameters> context, CancellationToken cancellationToken) =>
    AllMustExist(parameters.SObjectName, parameters.WritableFieldNames, WritableFields, context, cancellationToken);

private Task<bool> AllIndexedFieldsMustExist(SalesforceDataRequestParameters parameters, ValidationContext<SalesforceDataRequestParameters> context, CancellationToken cancellationToken)
{
    HashSet<string> indexFieldNames = parameters.IndexedFieldNames
        .SelectMany(x => x.Split(";"))
        .Select(x => x.Trim('-', '_'))
        .ToHashSet();

    return AllMustExist(parameters.SObjectName, indexFieldNames, IndexedFields, context, cancellationToken);
}

// 调整AllMustExist方法逻辑,直接计算错误字段存入上下文
private async Task<bool> AllMustExist(string sObjectName, HashSet<string> inputFieldNames, string purpose, ValidationContext<SalesforceDataRequestParameters> context, CancellationToken cancellationToken)
{
    (bool sObjectExists, IList<string> fieldNames) = await GetFieldNames(sObjectName, cancellationToken);
    if (!sObjectExists) 
    {
        // sObject不存在的情况已经有单独的规则校验,这里直接返回false即可
        return false;
    }
    // 直接计算错误字段,无需额外调用接口
    var badFields = inputFieldNames.Where(x => !fieldNames.Contains(x)).ToHashSet();
    if (badFields.Any())
    {
        // 存入当前校验的上下文,仅对本次校验生效
        context.RootContextData[$"BadFields_{purpose}"] = badFields;
        return false;
    }
    return true;
}

最后删掉原来的BadFieldsByPurposes实例字段和GetBadFieldNames方法即可,不存在线程安全问题,也不会提前执行消息拼接逻辑。

内容的提问来源于stack exchange,提问作者Brian Kessler

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.26 08:54:04