如果你在 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 的选项系统很有帮助:
创建扩展方法以便集成
现在,我们创建几个扩展方法,让验证更容易使用:
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()
确保验证在应用启动时进行。