基于FluentValidation实现BookDto多场景差异化验证方案问询
解决方案
一、修复JSON Schema的anyOf验证逻辑
原Schema的anyOf分支未生效,核心问题是条件判断逻辑不准确,且未匹配C#可空字符串的JSON序列化规则。以下是修正后的Schema(适配Newtonsoft JSON Schema的draft-07版本,兼容性更好):
{ "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { "id": { "type": ["string", "null"], "minLength": 1 }, "externalBookId": { "type": ["string", "null"], "minLength": 1 }, "name": { "type": "string", "minLength": 1 }, "description": { "type": ["string", "null"] }, "cost": { "type": ["number", "null"] }, "externalClientId": { "type": "string", "minLength": 1 } }, "oneOf": [ { "required": ["id"], "properties": { "externalBookId": { "enum": [null] } } }, { "required": ["externalBookId"], "properties": { "id": { "enum": [null] } } } ], "required": ["name", "externalClientId"] }
修正说明:
- 用
oneOf替代anyOf:确保Id和ExternalBookId必须且只能存在一个,符合你FluentValidator的逻辑; - 明确可空字段类型:
["string", "null"]对应C#的string?,避免JSON中null值导致类型验证失败; - 用
enum: [null]强制另一个字段为空:Newtonsoft对const关键字支持有限,enum兼容性更强。
二、确保Newtonsoft JSON Schema版本支持
确认已安装最新稳定版的Newtonsoft.Json.Schema NuGet包,旧版本对oneOf、enum等关键字的支持存在缺陷。
三、数据库存储Schema并动态加载
1. 数据库表设计
创建存储不同请求类型验证规则的表:
CREATE TABLE ValidationSchemas ( Id INT PRIMARY KEY IDENTITY, RequestType VARCHAR(50) NOT NULL UNIQUE, -- 例如"API"、"FILE" SchemaJson NVARCHAR(MAX) NOT NULL, CreatedAt DATETIME DEFAULT GETDATE(), UpdatedAt DATETIME DEFAULT GETDATE() );
向表中插入API和文件来源对应的Schema记录。
2. 动态加载验证逻辑
修改原验证方法,从数据库按需加载对应Schema:
public async Task<T?> ValidateAsync(string jsonData, string requestType) { // 从数据库获取对应请求类型的Schema var schemaJson = await _dbContext.ValidationSchemas .Where(s => s.RequestType == requestType) .Select(s => s.SchemaJson) .FirstOrDefaultAsync(); if (string.IsNullOrEmpty(schemaJson)) throw new InvalidOperationException($"未找到请求类型[{requestType}]对应的验证规则"); JSchema schema = JSchema.Parse(schemaJson); JObject jObject = JObject.Parse(jsonData); if (!jObject.IsValid(schema, out IList<ValidationError> validationErrors)) { throw new JsonValidationException(validationErrors); } return jObject.ToObject<T>(); }
3. 缓存优化
避免重复查询数据库,加入内存缓存:
private readonly IMemoryCache _cache; private const string SchemaCacheKeyPrefix = "ValidationSchema_"; public async Task<T?> ValidateAsync(string jsonData, string requestType) { string cacheKey = $"{SchemaCacheKeyPrefix}{requestType}"; if (!_cache.TryGetValue(cacheKey, out JSchema schema)) { var schemaJson = await _dbContext.ValidationSchemas .Where(s => s.RequestType == requestType) .Select(s => s.SchemaJson) .FirstOrDefaultAsync(); if (string.IsNullOrEmpty(schemaJson)) throw new InvalidOperationException($"未找到请求类型[{requestType}]对应的验证规则"); schema = JSchema.Parse(schemaJson); _cache.Set(cacheKey, schema, TimeSpan.FromHours(1)); // 缓存1小时,可按需调整 } JObject jObject = JObject.Parse(jsonData); if (!jObject.IsValid(schema, out IList<ValidationError> validationErrors)) { throw new JsonValidationException(validationErrors); } return jObject.ToObject<T>(); }
四、替代方案:复用FluentValidator逻辑(类型更安全)
如果JSON Schema仍存在兼容性问题,可采用FluentValidator的复用策略,避免重复代码:
1. 基础公共验证器
public class BaseBookValidator : AbstractValidator<BookDto> { public BaseBookValidator() { RuleFor(x => x.Name).NotEmpty().MinimumLength(1); RuleFor(x => x.ExternalClientId).NotEmpty().MinimumLength(1); RuleFor(x => x.Description).Nullable(); RuleFor(x => x.Cost).Must(BeValidCost).When(x => !string.IsNullOrEmpty(x.Cost)); } private bool BeValidCost(string cost) { return decimal.TryParse(cost, out _); } }
2. API专用验证器
public class ApiBookValidator : BaseBookValidator { public ApiBookValidator() { RuleFor(x => x.Id).NotEmpty(); RuleFor(x => x.ExternalBookId).Empty(); } }
3. 文件专用验证器
public class FileBookValidator : BaseBookValidator { public FileBookValidator() { RuleFor(x => x).Must(HaveEitherIdOrExternalBookId) .WithMessage("必须且只能提供Id或ExternalBookId中的一个"); } private bool HaveEitherIdOrExternalBookId(BookDto dto) { return (!string.IsNullOrEmpty(dto.Id) && string.IsNullOrEmpty(dto.ExternalBookId)) || (string.IsNullOrEmpty(dto.Id) && !string.IsNullOrEmpty(dto.ExternalBookId)); } }
4. 动态加载验证器
用工厂模式根据请求类型获取对应验证器:
public interface IBookValidatorFactory { AbstractValidator<BookDto> GetValidator(string requestType); } public class BookValidatorFactory : IBookValidatorFactory { private readonly IServiceProvider _serviceProvider; public BookValidatorFactory(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } public AbstractValidator<BookDto> GetValidator(string requestType) { return requestType switch { "API" => _serviceProvider.GetRequiredService<ApiBookValidator>(), "FILE" => _serviceProvider.GetRequiredService<FileBookValidator>(), _ => throw new InvalidOperationException($"未找到请求类型[{requestType}]对应的验证器") }; } }
使用验证器:
public async Task<BookDto> ValidateAsync(string jsonData, string requestType) { var dto = JsonConvert.DeserializeObject<BookDto>(jsonData); var validator = _validatorFactory.GetValidator(requestType); var result = await validator.ValidateAsync(dto); if (!result.IsValid) { throw new ValidationException(result.Errors); } return dto; }
内容的提问来源于stack exchange,提问作者hackp0int
相关产品推荐
相关产品推荐

