.NET中FluentValidation实现选项模式验证

banq

如果你在 ASP.NET Core 里用过 Options Pattern(选项模式),你可能对用 Data Annotations(数据注解)做验证很熟悉。虽然 Data Annotations 能用,但在处理复杂的验证场景时,它的功能就有点不够用了。

Options Pattern 可以让你在运行时用类来获取强类型的配置对象。

问题是啥呢?你在用这些配置之前,没法确定它们是不是有效的。

那为啥不在应用启动的时候就验证呢?

在这篇文章里,我们会探讨如何把更强大的 FluentValidation 库和 ASP.NET Core 的 Options Pattern 结合起来,构建一个在应用启动时执行的强大验证方案。

为啥选 FluentValidation 而不是 Data Annotations?

Data Annotations 适合简单的验证,但 FluentValidation 有以下几个优势:

  • - 更灵活、表达能力更强的验证规则
  • - 更好地支持复杂的条件验证
  • - 更清晰的职责分离(验证逻辑和模型分开)
  • - 更容易测试验证规则
  • - 更好地支持自定义验证逻辑
  • - 允许在验证器里注入依赖

理解 Options Pattern 的生命周期

在深入验证之前,先了解下 ASP.NET Core 里选项的生命周期:

1. 选项被注册到依赖注入(DI)容器里
2. 配置值被绑定到选项类
3. 验证发生(如果配置了的话)
4. 选项在通过

IOptions
IOptionsSnapshot
IOptionsMonitor
请求时被解析

ValidateOnStart()
方法会强制在应用启动时进行验证,而不是在选项第一次被解析时。

没有验证时常见的配置问题

如果没有验证,配置问题可能会以几种方式出现:

  • - 静默失败:配置错误的选项可能会导致默认值被使用,而且没有任何警告
  • - 运行时异常:配置问题可能只在应用尝试使用无效值时才会暴露
  • - 连锁故障:一个配置错误的组件可能会导致依赖它的系统也出问题

通过在启动时验证,你可以快速发现问题,避免这些情况。

搭建基础

首先,我们把 FluentValidation 包加到项目里:


Install-Package FluentValidation 基础包
Install-Package FluentValidation.DependencyInjectionExtensions 用于 DI 集成


在我们的例子里,我们会用一个需要验证的

GitHubSettings
类:


public class GitHubSettings
{
    public const string ConfigurationSection = "GitHubSettings";

    public string BaseUrl { get; init; }
    public string AccessToken { get; init; }
    public string RepositoryName { get; init; }
}


创建一个 FluentValidation 验证器

接下来,我们为这个设置类创建一个验证器:


public class GitHubSettingsValidator : AbstractValidator<GitHubSettings>
{
    public GitHubSettingsValidator()
    {
        RuleFor(x => x.BaseUrl).NotEmpty();

        RuleFor(x => x.BaseUrl)
            .Must(baseUrl => Uri.TryCreate(baseUrl, UriKind.Absolute, out _))
            .When(x => !string.IsNullOrWhiteSpace(x.BaseUrl))
            .WithMessage($"{nameof(GitHubSettings.BaseUrl)} 必须是一个有效的 URL");

        RuleFor(x => x.AccessToken)
            .NotEmpty();

        RuleFor(x => x.RepositoryName)
            .NotEmpty();
    }
}


构建 FluentValidation 集成

为了把 FluentValidation 和 Options Pattern 集成起来,我们需要创建一个自定义的

IValidateOptions
实现:


using FluentValidation;
using Microsoft.Extensions.Options;

public class FluentValidateOptions<TOptions>
    : IValidateOptions<TOptions>
    where TOptions : class
{
    private readonly IServiceProvider _serviceProvider;
    private readonly string? _name;

    public FluentValidateOptions(IServiceProvider serviceProvider, string? name)
    {
        _serviceProvider = serviceProvider;
        _name = name;
    }

    public ValidateOptionsResult Validate(string? name, TOptions options)
    {
        if (_name is not null && _name != name)
        {
            return ValidateOptionsResult.Skip;
        }

        ArgumentNullException.ThrowIfNull(options);

        using var scope = _serviceProvider.CreateScope();

        var validator = scope.ServiceProvider.GetRequiredService<IValidator<TOptions>>();

        var result = validator.Validate(options);
        if (result.IsValid)
        {
            return ValidateOptionsResult.Success;
        }

        var type = options.GetType().Name;
        var errors = new List<string>();

        foreach (var failure in result.Errors)
        {
            errors.Add($"验证失败:{type}.{failure.PropertyName},错误信息:{failure.ErrorMessage}");
        }

        return ValidateOptionsResult.Fail(errors);
    }
}


关于这个实现,有几个重要的点:

  • - 我们创建了一个作用域内的服务提供者,以便正确解析验证器(因为验证器通常注册为作用域服务)
  • - 我们通过
    _name
    属性处理命名选项
  • - 我们构建了包含属性名和错误信息的详细错误消息

FluentValidation 集成的工作原理

在添加自定义的 FluentValidation 集成时,了解它如何连接到 ASP.NET Core 的选项系统很有帮助:

  • -
    IValidateOptions
    接口是 ASP.NET Core 提供的用于选项验证的钩子
  • - 我们的
    FluentValidateOptions
    类实现了这个接口,以便桥接到 FluentValidation
  • - 当调用
    ValidateOnStart()
    时,ASP.NET Core 会解析所有
    IValidateOptions
    实现并运行它们
  • - 如果验证失败,会抛出
    OptionsValidationException
    ,阻止应用启动

创建扩展方法以便集成

现在,我们创建几个扩展方法,让验证更容易使用:


public static class OptionsBuilderExtensions
{
    public static OptionsBuilder<TOptions> ValidateFluentValidation<TOptions>(
        this OptionsBuilder<TOptions> builder)
        where TOptions : class
    {
        builder.Services.AddSingleton<IValidateOptions<TOptions>>(
            serviceProvider => new FluentValidateOptions<TOptions>(
                serviceProvider,
                builder.Name));

        return builder;
    }
}


这个扩展方法让我们在配置选项时可以调用

.ValidateFluentValidation()
,类似于内置的
.ValidateDataAnnotations()
方法。

为了更方便,我们可以再创建一个扩展方法来简化整个配置过程:


public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddOptionsWithFluentValidation<TOptions>(
        this IServiceCollection services,
        string configurationSection)
        where TOptions : class
    {
        services.AddOptions<TOptions>()
            .BindConfiguration(configurationSection)
            .ValidateFluentValidation() // 配置 FluentValidation 验证
            .ValidateOnStart();
// 在应用启动时验证选项

        return services;
    }
}


注册和使用验证

有几种方式可以使用我们的 FluentValidation 集成:

选项 1:标准注册,手动注册验证器


// 注册验证器
builder.Services.AddScoped<IValidator<GitHubSettings>, GitHubSettingsValidator>();

// 配置选项并验证
builder.Services.AddOptions<GitHubSettings>()
    .BindConfiguration(GitHubSettings.ConfigurationSection)
    .ValidateFluentValidation()
// 配置 FluentValidation 验证
    .ValidateOnStart();


选项 2:使用方便的扩展方法


// 注册验证器
builder.Services.AddScoped<IValidator<GitHubSettings>, GitHubSettingsValidator>();

// 使用方便的扩展方法
builder.Services.AddOptionsWithFluentValidation<GitHubSettings>(GitHubSettings.ConfigurationSection);


选项 3:自动注册验证器

如果你有很多验证器,想一次性注册它们,可以用 FluentValidation 的程序集扫描功能:


// 从程序集注册所有验证器
builder.Services.AddValidatorsFromAssembly(typeof(Program).Assembly);

// 使用方便的扩展方法
builder.Services.AddOptionsWithFluentValidation<GitHubSettings>(GitHubSettings.ConfigurationSection);


运行时会发生什么?

用了

.ValidateOnStart()
后,如果任何验证规则失败,应用会在启动时抛出异常。比如,如果你的
appsettings.json
里缺少必需的
AccessToken
,你会看到类似这样的错误:


Microsoft.Extensions.Options.OptionsValidationException:
    验证失败:GitHubSettings.AccessToken,错误信息:'Access Token' 不能为空。


这会阻止应用在配置无效时启动,确保问题尽早被发现。

处理不同的配置源

ASP.NET Core 的配置系统支持多种来源。当使用 Options Pattern 和 FluentValidation 时,记住验证对任何来源都有效:

  • - 环境变量
  • - Azure Key Vault
  • - 用户机密
  • - JSON 文件
  • - 内存配置

这对容器化应用特别有用,因为配置通常来自环境变量或挂载的机密。

测试你的验证器

使用 FluentValidation 的一个好处是验证器很容易测试:


// 使用 FluentValidation.TestHelper 的辅助方法
[Fact]
public void GitHubSettings_WithMissingAccessToken_ShouldHaveValidationError()
{
   
// 准备
    var validator = new GitHubSettingsValidator();
    var settings = new GitHubSettings { RepositoryName =
"test-repo" };

   
// 执行
    var result = validator.TestValidate(settings);

   
// 断言
    result.ShouldHaveValidationErrorFor(x => x.AccessToken);
}


总结

通过把 FluentValidation 和 Options Pattern 以及

ValidateOnStart()
结合起来,我们创建了一个强大的验证系统,确保应用在启动时就有正确的配置。

这种方案:

  • - 提供了比 Data Annotations 更强大的验证规则
  • - 把验证逻辑和配置模型分开
  • - 在应用启动时捕获配置错误
  • - 支持复杂的验证场景
  • - 易于测试

这种模式在微服务架构或容器化应用中特别有价值,因为配置错误应该立即被发现,而不是在运行时。

记得正确注册你的验证器,并使用

.ValidateOnStart()
确保验证在应用启动时进行。