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

基于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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.14 08:40:03