使用C# Fluent Validation如何生成集合成员校验失败的自定义错误消息
解决方案
完全不需要弃用Fluent Validation,你遇到的问题是两个基础使用误区导致的:
- 注入使用的Validator默认是单例,用实例字段存储单次校验的错误数据会有严重的线程安全问题,多请求同时校验时数据会互相覆盖
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
相关产品推荐
相关产品推荐

