microsoft-extensions-configuration
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMicrosoft.Extensions Configuration Patterns
Microsoft.Extensions 配置模式
When to Use This Skill
何时使用该技能
Use this skill when:
- Binding configuration from appsettings.json to strongly-typed classes
- Validating configuration at application startup (fail fast)
- Implementing complex validation logic for settings
- Designing configuration classes that are testable and maintainable
- Understanding IOptions<T>, IOptionsSnapshot<T>, and IOptionsMonitor<T>
在以下场景中使用该技能:
- 将 appsettings.json 中的配置绑定到强类型类
- 在应用启动时验证配置(快速失败)
- 为设置实现复杂的验证逻辑
- 设计可测试、可维护的配置类
- 理解 IOptions<T>、IOptionsSnapshot<T> 和 IOptionsMonitor<T>
Why Configuration Validation Matters
配置验证的重要性
The Problem: Applications often fail at runtime due to misconfiguration - missing connection strings, invalid URLs, out-of-range values. These failures happen deep in business logic, far from where configuration is loaded, making debugging difficult.
The Solution: Validate configuration at startup. If configuration is invalid, the application fails immediately with a clear error message. This is the "fail fast" principle.
csharp
// BAD: Fails at runtime when someone tries to use the service
public class EmailService
{
public EmailService(IOptions<SmtpSettings> options)
{
var settings = options.Value;
// Throws NullReferenceException 10 minutes into production
_client = new SmtpClient(settings.Host, settings.Port);
}
}
// GOOD: Fails at startup with clear error
// "SmtpSettings validation failed: Host is required"问题: 应用程序经常因配置错误在运行时失败——缺少连接字符串、无效URL、超出范围的值。这些故障发生在业务逻辑深处,远离配置加载的位置,使得调试变得困难。
解决方案: 在启动时验证配置。如果配置无效,应用程序会立即失败并显示清晰的错误消息。这就是“快速失败”原则。
csharp
// 错误示例:当有人尝试使用服务时在运行时失败
public class EmailService
{
public EmailService(IOptions<SmtpSettings> options)
{
var settings = options.Value;
// 上线10分钟后抛出 NullReferenceException
_client = new SmtpClient(settings.Host, settings.Port);
}
}
// 正确示例:启动时失败并显示清晰错误
// "SmtpSettings 验证失败:Host 是必填项"Pattern 1: Basic Options Binding
模式1:基础Options绑定
Define a Settings Class
定义设置类
csharp
public class SmtpSettings
{
public const string SectionName = "Smtp";
public string Host { get; set; } = string.Empty;
public int Port { get; set; } = 587;
public string? Username { get; set; }
public string? Password { get; set; }
public bool UseSsl { get; set; } = true;
}csharp
public class SmtpSettings
{
public const string SectionName = "Smtp";
public string Host { get; set; } = string.Empty;
public int Port { get; set; } = 587;
public string? Username { get; set; }
public string? Password { get; set; }
public bool UseSsl { get; set; } = true;
}Bind from Configuration
从配置中绑定
csharp
// In Program.cs or service registration
builder.Services.AddOptions<SmtpSettings>()
.BindConfiguration(SmtpSettings.SectionName);
// appsettings.json
{
"Smtp": {
"Host": "smtp.example.com",
"Port": 587,
"Username": "user@example.com",
"Password": "secret",
"UseSsl": true
}
}csharp
// 在 Program.cs 或服务注册中
builder.Services.AddOptions<SmtpSettings>()
.BindConfiguration(SmtpSettings.SectionName);
// appsettings.json
{
"Smtp": {
"Host": "smtp.example.com",
"Port": 587,
"Username": "user@example.com",
"Password": "secret",
"UseSsl": true
}
}Consume in Services
在服务中使用
csharp
public class EmailService
{
private readonly SmtpSettings _settings;
// IOptions<T> - singleton, read once at startup
public EmailService(IOptions<SmtpSettings> options)
{
_settings = options.Value;
}
}csharp
public class EmailService
{
private readonly SmtpSettings _settings;
// IOptions<T> - 单例,启动时读取一次
public EmailService(IOptions<SmtpSettings> options)
{
_settings = options.Value;
}
}Pattern 2: Data Annotations Validation
模式2:数据注解验证
For simple validation rules, use Data Annotations:
csharp
using System.ComponentModel.DataAnnotations;
public class SmtpSettings
{
public const string SectionName = "Smtp";
[Required(ErrorMessage = "SMTP host is required")]
public string Host { get; set; } = string.Empty;
[Range(1, 65535, ErrorMessage = "Port must be between 1 and 65535")]
public int Port { get; set; } = 587;
[EmailAddress(ErrorMessage = "Username must be a valid email address")]
public string? Username { get; set; }
public string? Password { get; set; }
public bool UseSsl { get; set; } = true;
}对于简单的验证规则,使用数据注解:
csharp
using System.ComponentModel.DataAnnotations;
public class SmtpSettings
{
public const string SectionName = "Smtp";
[Required(ErrorMessage = "SMTP 主机是必填项")]
public string Host { get; set; } = string.Empty;
[Range(1, 65535, ErrorMessage = "端口必须在1到65535之间")]
public int Port { get; set; } = 587;
[EmailAddress(ErrorMessage = "用户名必须是有效的电子邮件地址")]
public string? Username { get; set; }
public string? Password { get; set; }
public bool UseSsl { get; set; } = true;
}Enable Data Annotations Validation
启用数据注解验证
csharp
builder.Services.AddOptions<SmtpSettings>()
.BindConfiguration(SmtpSettings.SectionName)
.ValidateDataAnnotations() // Enable attribute-based validation
.ValidateOnStart(); // Validate immediately at startupKey Point: is critical. Without it, validation only runs when the options are first accessed, which could be minutes or hours into application runtime.
.ValidateOnStart()csharp
builder.Services.AddOptions<SmtpSettings>()
.BindConfiguration(SmtpSettings.SectionName)
.ValidateDataAnnotations() // 启用基于特性的验证
.ValidateOnStart(); // 在启动时立即验证关键点: 至关重要。如果没有它,验证只会在首次访问选项时运行,这可能是在应用运行数分钟或数小时之后。
.ValidateOnStart()Pattern 3: IValidateOptions<T> for Complex Validation
模式3:使用 IValidateOptions<T> 进行复杂验证
Data Annotations work for simple rules, but complex validation requires :
IValidateOptions<T>数据注解适用于简单规则,但复杂验证需要 :
IValidateOptions<T>When to Use IValidateOptions
何时使用 IValidateOptions
| Scenario | Data Annotations | IValidateOptions |
|---|---|---|
| Required field | ✅ | ✅ |
| Range check | ✅ | ✅ |
| Regex pattern | ✅ | ✅ |
| Cross-property validation | ❌ | ✅ |
| Conditional validation | ❌ | ✅ |
| External service checks | ❌ | ✅ |
| Custom error messages with context | Limited | ✅ |
| Dependency injection in validator | ❌ | ✅ |
| 场景 | 数据注解 | IValidateOptions |
|---|---|---|
| 必填字段 | ✅ | ✅ |
| 范围检查 | ✅ | ✅ |
| 正则表达式 | ✅ | ✅ |
| 跨属性验证 | ❌ | ✅ |
| 条件验证 | ❌ | ✅ |
| 外部服务检查 | ❌ | ✅ |
| 带上下文的自定义错误消息 | 有限 | ✅ |
| 验证器中的依赖注入 | ❌ | ✅ |
Implementing IValidateOptions
实现 IValidateOptions
csharp
using Microsoft.Extensions.Options;
public class SmtpSettingsValidator : IValidateOptions<SmtpSettings>
{
public ValidateOptionsResult Validate(string? name, SmtpSettings options)
{
var failures = new List<string>();
// Required field validation
if (string.IsNullOrWhiteSpace(options.Host))
{
failures.Add("Host is required");
}
// Range validation
if (options.Port is < 1 or > 65535)
{
failures.Add($"Port {options.Port} is invalid. Must be between 1 and 65535");
}
// Cross-property validation
if (!string.IsNullOrEmpty(options.Username) && string.IsNullOrEmpty(options.Password))
{
failures.Add("Password is required when Username is specified");
}
// Conditional validation
if (options.UseSsl && options.Port == 25)
{
failures.Add("Port 25 is typically not used with SSL. Consider port 465 or 587");
}
// Return result
return failures.Count > 0
? ValidateOptionsResult.Fail(failures)
: ValidateOptionsResult.Success;
}
}csharp
using Microsoft.Extensions.Options;
public class SmtpSettingsValidator : IValidateOptions<SmtpSettings>
{
public ValidateOptionsResult Validate(string? name, SmtpSettings options)
{
var failures = new List<string>();
// 必填字段验证
if (string.IsNullOrWhiteSpace(options.Host))
{
failures.Add("Host 是必填项");
}
// 范围验证
if (options.Port is < 1 or > 65535)
{
failures.Add($"端口 {options.Port} 无效。必须在1到65535之间");
}
// 跨属性验证
if (!string.IsNullOrEmpty(options.Username) && string.IsNullOrEmpty(options.Password))
{
failures.Add("当指定用户名时,密码是必填项");
}
// 条件验证
if (options.UseSsl && options.Port == 25)
{
failures.Add("端口25通常不与SSL一起使用。请考虑使用端口465或587");
}
// 返回结果
return failures.Count > 0
? ValidateOptionsResult.Fail(failures)
: ValidateOptionsResult.Success;
}
}Register the Validator
注册验证器
csharp
builder.Services.AddOptions<SmtpSettings>()
.BindConfiguration(SmtpSettings.SectionName)
.ValidateDataAnnotations() // Run attribute validation first
.ValidateOnStart();
// Register the custom validator
builder.Services.AddSingleton<IValidateOptions<SmtpSettings>, SmtpSettingsValidator>();Order matters: Data Annotations run first, then IValidateOptions validators. All failures are collected and reported together.
csharp
builder.Services.AddOptions<SmtpSettings>()
.BindConfiguration(SmtpSettings.SectionName)
.ValidateDataAnnotations() // 先运行特性验证
.ValidateOnStart();
// 注册自定义验证器
builder.Services.AddSingleton<IValidateOptions<SmtpSettings>, SmtpSettingsValidator>();顺序很重要: 数据注解先运行,然后是 IValidateOptions 验证器。所有失败都会被收集并一起报告。
Pattern 4: Validators with Dependencies
模式4:带依赖的验证器
IValidateOptions validators are resolved from DI, so they can have dependencies:
csharp
public class DatabaseSettingsValidator : IValidateOptions<DatabaseSettings>
{
private readonly ILogger<DatabaseSettingsValidator> _logger;
private readonly IHostEnvironment _environment;
public DatabaseSettingsValidator(
ILogger<DatabaseSettingsValidator> logger,
IHostEnvironment environment)
{
_logger = logger;
_environment = environment;
}
public ValidateOptionsResult Validate(string? name, DatabaseSettings options)
{
var failures = new List<string>();
if (string.IsNullOrWhiteSpace(options.ConnectionString))
{
failures.Add("ConnectionString is required");
}
// Environment-specific validation
if (_environment.IsProduction())
{
if (options.ConnectionString?.Contains("localhost") == true)
{
failures.Add("Production cannot use localhost database");
}
if (!options.ConnectionString?.Contains("Encrypt=True") == true)
{
_logger.LogWarning("Production database connection should use encryption");
}
}
// Validate connection string format
if (!string.IsNullOrEmpty(options.ConnectionString))
{
try
{
var builder = new SqlConnectionStringBuilder(options.ConnectionString);
if (string.IsNullOrEmpty(builder.DataSource))
{
failures.Add("ConnectionString must specify a Data Source");
}
}
catch (Exception ex)
{
failures.Add($"ConnectionString is malformed: {ex.Message}");
}
}
return failures.Count > 0
? ValidateOptionsResult.Fail(failures)
: ValidateOptionsResult.Success;
}
}IValidateOptions 验证器从DI容器中解析,因此它们可以拥有依赖项:
csharp
public class DatabaseSettingsValidator : IValidateOptions<DatabaseSettings>
{
private readonly ILogger<DatabaseSettingsValidator> _logger;
private readonly IHostEnvironment _environment;
public DatabaseSettingsValidator(
ILogger<DatabaseSettingsValidator> logger,
IHostEnvironment environment)
{
_logger = logger;
_environment = environment;
}
public ValidateOptionsResult Validate(string? name, DatabaseSettings options)
{
var failures = new List<string>();
if (string.IsNullOrWhiteSpace(options.ConnectionString))
{
failures.Add("ConnectionString 是必填项");
}
// 环境特定验证
if (_environment.IsProduction())
{
if (options.ConnectionString?.Contains("localhost") == true)
{
failures.Add("生产环境不能使用本地数据库");
}
if (!options.ConnectionString?.Contains("Encrypt=True") == true)
{
_logger.LogWarning("生产环境数据库连接应使用加密");
}
}
// 验证连接字符串格式
if (!string.IsNullOrEmpty(options.ConnectionString))
{
try
{
var builder = new SqlConnectionStringBuilder(options.ConnectionString);
if (string.IsNullOrEmpty(builder.DataSource))
{
failures.Add("ConnectionString 必须指定数据源");
}
}
catch (Exception ex)
{
failures.Add($"ConnectionString 格式错误: {ex.Message}");
}
}
return failures.Count > 0
? ValidateOptionsResult.Fail(failures)
: ValidateOptionsResult.Success;
}
}Pattern 5: Named Options
模式5:命名选项
When you have multiple instances of the same settings type (e.g., multiple database connections):
csharp
// appsettings.json
{
"Databases": {
"Primary": {
"ConnectionString": "Server=primary;..."
},
"Replica": {
"ConnectionString": "Server=replica;..."
}
}
}
// Registration
builder.Services.AddOptions<DatabaseSettings>("Primary")
.BindConfiguration("Databases:Primary")
.ValidateDataAnnotations()
.ValidateOnStart();
builder.Services.AddOptions<DatabaseSettings>("Replica")
.BindConfiguration("Databases:Replica")
.ValidateDataAnnotations()
.ValidateOnStart();
// Consumption
public class DataService
{
private readonly DatabaseSettings _primary;
private readonly DatabaseSettings _replica;
public DataService(IOptionsSnapshot<DatabaseSettings> options)
{
_primary = options.Get("Primary");
_replica = options.Get("Replica");
}
}当你有多个相同设置类型的实例时(例如多个数据库连接):
csharp
// appsettings.json
{
"Databases": {
"Primary": {
"ConnectionString": "Server=primary;..."
},
"Replica": {
"ConnectionString": "Server=replica;..."
}
}
}
// 注册
builder.Services.AddOptions<DatabaseSettings>("Primary")
.BindConfiguration("Databases:Primary")
.ValidateDataAnnotations()
.ValidateOnStart();
builder.Services.AddOptions<DatabaseSettings>("Replica")
.BindConfiguration("Databases:Replica")
.ValidateDataAnnotations()
.ValidateOnStart();
// 使用
public class DataService
{
private readonly DatabaseSettings _primary;
private readonly DatabaseSettings _replica;
public DataService(IOptionsSnapshot<DatabaseSettings> options)
{
_primary = options.Get("Primary");
_replica = options.Get("Replica");
}
}Named Options Validator
命名选项验证器
csharp
public class DatabaseSettingsValidator : IValidateOptions<DatabaseSettings>
{
public ValidateOptionsResult Validate(string? name, DatabaseSettings options)
{
var failures = new List<string>();
var prefix = string.IsNullOrEmpty(name) ? "" : $"[{name}] ";
if (string.IsNullOrWhiteSpace(options.ConnectionString))
{
failures.Add($"{prefix}ConnectionString is required");
}
// Name-specific validation
if (name == "Primary" && options.ReadOnly)
{
failures.Add("Primary database cannot be read-only");
}
return failures.Count > 0
? ValidateOptionsResult.Fail(failures)
: ValidateOptionsResult.Success;
}
}csharp
public class DatabaseSettingsValidator : IValidateOptions<DatabaseSettings>
{
public ValidateOptionsResult Validate(string? name, DatabaseSettings options)
{
var failures = new List<string>();
var prefix = string.IsNullOrEmpty(name) ? "" : $"[{name}] ";
if (string.IsNullOrWhiteSpace(options.ConnectionString))
{
failures.Add($"{prefix}ConnectionString 是必填项");
}
// 针对名称的特定验证
if (name == "Primary" && options.ReadOnly)
{
failures.Add("主数据库不能是只读的");
}
return failures.Count > 0
? ValidateOptionsResult.Fail(failures)
: ValidateOptionsResult.Success;
}
}Pattern 6: Options Lifetime
模式6:选项生命周期
Understanding the three options interfaces:
| Interface | Lifetime | Reloads on Change | Use Case |
|---|---|---|---|
| Singleton | No | Static config, read once |
| Scoped | Yes (per request) | Web apps needing fresh config |
| Singleton | Yes (with callback) | Background services, real-time updates |
理解三个选项接口:
| 接口 | 生命周期 | 配置变更时重新加载 | 使用场景 |
|---|---|---|---|
| 单例 | 否 | 静态配置,仅读取一次 |
| 作用域 | 是(每个请求) | 需要新鲜配置的Web应用 |
| 单例 | 是(带回调) | 后台服务、实时更新 |
IOptionsMonitor for Background Services
后台服务使用 IOptionsMonitor
csharp
public class BackgroundWorker : BackgroundService
{
private readonly IOptionsMonitor<WorkerSettings> _optionsMonitor;
private WorkerSettings _currentSettings;
public BackgroundWorker(IOptionsMonitor<WorkerSettings> optionsMonitor)
{
_optionsMonitor = optionsMonitor;
_currentSettings = optionsMonitor.CurrentValue;
// Subscribe to configuration changes
_optionsMonitor.OnChange(settings =>
{
_currentSettings = settings;
_logger.LogInformation("Worker settings updated: Interval={Interval}",
settings.PollingInterval);
});
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await DoWorkAsync();
await Task.Delay(_currentSettings.PollingInterval, stoppingToken);
}
}
}csharp
public class BackgroundWorker : BackgroundService
{
private readonly IOptionsMonitor<WorkerSettings> _optionsMonitor;
private WorkerSettings _currentSettings;
public BackgroundWorker(IOptionsMonitor<WorkerSettings> optionsMonitor)
{
_optionsMonitor = optionsMonitor;
_currentSettings = optionsMonitor.CurrentValue;
// 订阅配置变更
_optionsMonitor.OnChange(settings =>
{
_currentSettings = settings;
_logger.LogInformation("工作器设置已更新: 间隔={Interval}",
settings.PollingInterval);
});
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await DoWorkAsync();
await Task.Delay(_currentSettings.PollingInterval, stoppingToken);
}
}
}Pattern 7: Post-Configuration
模式7:后期配置
Modify options after binding but before validation:
csharp
builder.Services.AddOptions<ApiSettings>()
.BindConfiguration("Api")
.PostConfigure(options =>
{
// Ensure BaseUrl ends with /
if (!string.IsNullOrEmpty(options.BaseUrl) && !options.BaseUrl.EndsWith('/'))
{
options.BaseUrl += '/';
}
// Set defaults based on environment
options.Timeout ??= TimeSpan.FromSeconds(30);
})
.ValidateDataAnnotations()
.ValidateOnStart();在绑定后、验证前修改选项:
csharp
builder.Services.AddOptions<ApiSettings>()
.BindConfiguration("Api")
.PostConfigure(options =>
{
// 确保 BaseUrl 以 / 结尾
if (!string.IsNullOrEmpty(options.BaseUrl) && !options.BaseUrl.EndsWith('/'))
{
options.BaseUrl += '/';
}
// 根据环境设置默认值
options.Timeout ??= TimeSpan.FromSeconds(30);
})
.ValidateDataAnnotations()
.ValidateOnStart();PostConfigure with Dependencies
带依赖的后期配置
csharp
builder.Services.AddOptions<ApiSettings>()
.BindConfiguration("Api")
.PostConfigure<IHostEnvironment>((options, env) =>
{
if (env.IsDevelopment())
{
options.Timeout = TimeSpan.FromMinutes(5); // Longer timeout for debugging
}
});csharp
builder.Services.AddOptions<ApiSettings>()
.BindConfiguration("Api")
.PostConfigure<IHostEnvironment>((options, env) =>
{
if (env.IsDevelopment())
{
options.Timeout = TimeSpan.FromMinutes(5); // 调试时使用更长超时
}
});Pattern 8: Complete Example - Production Settings Class
模式8:完整示例 - 生产环境设置类
csharp
using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.Options;
public class AkkaSettings
{
public const string SectionName = "AkkaSettings";
[Required]
public string ActorSystemName { get; set; } = "MySystem";
public AkkaExecutionMode ExecutionMode { get; set; } = AkkaExecutionMode.LocalTest;
public bool LogConfigOnStart { get; set; } = false;
public RemoteOptions RemoteOptions { get; set; } = new();
public ClusterOptions ClusterOptions { get; set; } = new();
public ClusterBootstrapOptions ClusterBootstrapOptions { get; set; } = new();
}
public enum AkkaExecutionMode
{
LocalTest, // No remoting, no clustering
Clustered // Full cluster with sharding, distributed pub/sub
}
public class AkkaSettingsValidator : IValidateOptions<AkkaSettings>
{
private readonly IHostEnvironment _environment;
public AkkaSettingsValidator(IHostEnvironment environment)
{
_environment = environment;
}
public ValidateOptionsResult Validate(string? name, AkkaSettings options)
{
var failures = new List<string>();
// Basic validation
if (string.IsNullOrWhiteSpace(options.ActorSystemName))
{
failures.Add("ActorSystemName is required");
}
// Mode-specific validation
if (options.ExecutionMode == AkkaExecutionMode.Clustered)
{
ValidateClusteredMode(options, failures);
}
// Environment-specific validation
if (_environment.IsProduction() && options.ExecutionMode == AkkaExecutionMode.LocalTest)
{
failures.Add("LocalTest execution mode is not allowed in production");
}
return failures.Count > 0
? ValidateOptionsResult.Fail(failures)
: ValidateOptionsResult.Success;
}
private void ValidateClusteredMode(AkkaSettings options, List<string> failures)
{
if (string.IsNullOrEmpty(options.RemoteOptions.PublicHostName))
{
failures.Add("RemoteOptions.PublicHostName is required in Clustered mode");
}
if (options.RemoteOptions.Port is null or < 0)
{
failures.Add("RemoteOptions.Port must be >= 0 in Clustered mode");
}
if (options.ClusterBootstrapOptions.Enabled)
{
ValidateClusterBootstrap(options.ClusterBootstrapOptions, failures);
}
else if (options.ClusterOptions.SeedNodes?.Length == 0)
{
failures.Add("Either ClusterBootstrap must be enabled or SeedNodes must be specified");
}
}
private void ValidateClusterBootstrap(ClusterBootstrapOptions options, List<string> failures)
{
if (string.IsNullOrEmpty(options.ServiceName))
{
failures.Add("ClusterBootstrapOptions.ServiceName is required");
}
if (options.RequiredContactPointsNr <= 0)
{
failures.Add("ClusterBootstrapOptions.RequiredContactPointsNr must be > 0");
}
switch (options.DiscoveryMethod)
{
case DiscoveryMethod.Config:
if (options.ConfigServiceEndpoints?.Length == 0)
{
failures.Add("ConfigServiceEndpoints required for Config discovery");
}
break;
case DiscoveryMethod.AzureTableStorage:
if (options.AzureDiscoveryOptions == null)
{
failures.Add("AzureDiscoveryOptions required for Azure discovery");
}
break;
}
}
}
// Registration
builder.Services.AddOptions<AkkaSettings>()
.BindConfiguration(AkkaSettings.SectionName)
.ValidateDataAnnotations()
.ValidateOnStart();
builder.Services.AddSingleton<IValidateOptions<AkkaSettings>, AkkaSettingsValidator>();csharp
using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.Options;
public class AkkaSettings
{
public const string SectionName = "AkkaSettings";
[Required]
public string ActorSystemName { get; set; } = "MySystem";
public AkkaExecutionMode ExecutionMode { get; set; } = AkkaExecutionMode.LocalTest;
public bool LogConfigOnStart { get; set; } = false;
public RemoteOptions RemoteOptions { get; set; } = new();
public ClusterOptions ClusterOptions { get; set; } = new();
public ClusterBootstrapOptions ClusterBootstrapOptions { get; set; } = new();
}
public enum AkkaExecutionMode
{
LocalTest, // 无远程调用,无集群
Clustered // 完整集群,分片、分布式发布/订阅
}
public class AkkaSettingsValidator : IValidateOptions<AkkaSettings>
{
private readonly IHostEnvironment _environment;
public AkkaSettingsValidator(IHostEnvironment environment)
{
_environment = environment;
}
public ValidateOptionsResult Validate(string? name, AkkaSettings options)
{
var failures = new List<string>();
// 基础验证
if (string.IsNullOrWhiteSpace(options.ActorSystemName))
{
failures.Add("ActorSystemName 是必填项");
}
// 针对模式的验证
if (options.ExecutionMode == AkkaExecutionMode.Clustered)
{
ValidateClusteredMode(options, failures);
}
// 针对环境的验证
if (_environment.IsProduction() && options.ExecutionMode == AkkaExecutionMode.LocalTest)
{
failures.Add("生产环境不允许使用 LocalTest 执行模式");
}
return failures.Count > 0
? ValidateOptionsResult.Fail(failures)
: ValidateOptionsResult.Success;
}
private void ValidateClusteredMode(AkkaSettings options, List<string> failures)
{
if (string.IsNullOrEmpty(options.RemoteOptions.PublicHostName))
{
failures.Add("集群模式下 RemoteOptions.PublicHostName 是必填项");
}
if (options.RemoteOptions.Port is null or < 0)
{
failures.Add("集群模式下 RemoteOptions.Port 必须 >= 0");
}
if (options.ClusterBootstrapOptions.Enabled)
{
ValidateClusterBootstrap(options.ClusterBootstrapOptions, failures);
}
else if (options.ClusterOptions.SeedNodes?.Length == 0)
{
failures.Add("必须启用 ClusterBootstrap 或指定 SeedNodes");
}
}
private void ValidateClusterBootstrap(ClusterBootstrapOptions options, List<string> failures)
{
if (string.IsNullOrEmpty(options.ServiceName))
{
failures.Add("ClusterBootstrapOptions.ServiceName 是必填项");
}
if (options.RequiredContactPointsNr <= 0)
{
failures.Add("ClusterBootstrapOptions.RequiredContactPointsNr 必须 > 0");
}
switch (options.DiscoveryMethod)
{
case DiscoveryMethod.Config:
if (options.ConfigServiceEndpoints?.Length == 0)
{
failures.Add("使用配置发现时需要 ConfigServiceEndpoints");
}
break;
case DiscoveryMethod.AzureTableStorage:
if (options.AzureDiscoveryOptions == null)
{
failures.Add("使用Azure发现时需要 AzureDiscoveryOptions");
}
break;
}
}
}
// 注册
builder.Services.AddOptions<AkkaSettings>()
.BindConfiguration(AkkaSettings.SectionName)
.ValidateDataAnnotations()
.ValidateOnStart();
builder.Services.AddSingleton<IValidateOptions<AkkaSettings>, AkkaSettingsValidator>();Anti-Patterns to Avoid
需要避免的反模式
1. Manual Configuration Access
1. 手动访问配置
csharp
// BAD: Bypasses validation, hard to test
public class MyService
{
public MyService(IConfiguration configuration)
{
var host = configuration["Smtp:Host"]; // No validation!
}
}
// GOOD: Strongly-typed, validated
public class MyService
{
public MyService(IOptions<SmtpSettings> options)
{
var host = options.Value.Host; // Validated at startup
}
}csharp
// 错误:绕过验证,难以测试
public class MyService
{
public MyService(IConfiguration configuration)
{
var host = configuration["Smtp:Host"]; // 无验证!
}
}
// 正确:强类型,已验证
public class MyService
{
public MyService(IOptions<SmtpSettings> options)
{
var host = options.Value.Host; // 启动时已验证
}
}2. Validation in Constructor
2. 在构造函数中验证
csharp
// BAD: Validation happens at runtime, not startup
public class MyService
{
public MyService(IOptions<Settings> options)
{
if (string.IsNullOrEmpty(options.Value.Required))
throw new ArgumentException("Required is missing"); // Too late!
}
}
// GOOD: Validation at startup
builder.Services.AddOptions<Settings>()
.ValidateDataAnnotations()
.ValidateOnStart();csharp
// 错误:验证在运行时发生,而非启动时
public class MyService
{
public MyService(IOptions<Settings> options)
{
if (string.IsNullOrEmpty(options.Value.Required))
throw new ArgumentException("缺少必填项"); // 太晚了!
}
}
// 正确:启动时验证
builder.Services.AddOptions<Settings>()
.ValidateDataAnnotations()
.ValidateOnStart();3. Forgetting ValidateOnStart
3. 忘记 ValidateOnStart
csharp
// BAD: Validation only runs when first accessed
builder.Services.AddOptions<Settings>()
.ValidateDataAnnotations(); // Missing ValidateOnStart!
// GOOD: Fails immediately if invalid
builder.Services.AddOptions<Settings>()
.ValidateDataAnnotations()
.ValidateOnStart();csharp
// 错误:仅在首次访问时验证
builder.Services.AddOptions<Settings>()
.ValidateDataAnnotations(); // 缺少 ValidateOnStart!
// 正确:如果无效则立即失败
builder.Services.AddOptions<Settings>()
.ValidateDataAnnotations()
.ValidateOnStart();4. Throwing in IValidateOptions
4. 在 IValidateOptions 中抛出异常
csharp
// BAD: Throws exception, breaks validation chain
public ValidateOptionsResult Validate(string? name, Settings options)
{
if (options.Value < 0)
throw new ArgumentException("Value cannot be negative"); // Wrong!
return ValidateOptionsResult.Success;
}
// GOOD: Return failure result
public ValidateOptionsResult Validate(string? name, Settings options)
{
if (options.Value < 0)
return ValidateOptionsResult.Fail("Value cannot be negative");
return ValidateOptionsResult.Success;
}csharp
// 错误:抛出异常,中断验证链
public ValidateOptionsResult Validate(string? name, Settings options)
{
if (options.Value < 0)
throw new ArgumentException("值不能为负"); // 错误!
return ValidateOptionsResult.Success;
}
// 正确:返回失败结果
public ValidateOptionsResult Validate(string? name, Settings options)
{
if (options.Value < 0)
return ValidateOptionsResult.Fail("值不能为负");
return ValidateOptionsResult.Success;
}Testing Configuration Validators
测试配置验证器
csharp
public class SmtpSettingsValidatorTests
{
private readonly SmtpSettingsValidator _validator = new();
[Fact]
public void Validate_WithValidSettings_ReturnsSuccess()
{
var settings = new SmtpSettings
{
Host = "smtp.example.com",
Port = 587,
Username = "user@example.com",
Password = "secret"
};
var result = _validator.Validate(null, settings);
result.Succeeded.Should().BeTrue();
}
[Fact]
public void Validate_WithMissingHost_ReturnsFail()
{
var settings = new SmtpSettings { Host = "" };
var result = _validator.Validate(null, settings);
result.Succeeded.Should().BeFalse();
result.FailureMessage.Should().Contain("Host is required");
}
[Fact]
public void Validate_WithUsernameButNoPassword_ReturnsFail()
{
var settings = new SmtpSettings
{
Host = "smtp.example.com",
Username = "user@example.com",
Password = null // Missing!
};
var result = _validator.Validate(null, settings);
result.Succeeded.Should().BeFalse();
result.FailureMessage.Should().Contain("Password is required");
}
}csharp
public class SmtpSettingsValidatorTests
{
private readonly SmtpSettingsValidator _validator = new();
[Fact]
public void Validate_WithValidSettings_ReturnsSuccess()
{
var settings = new SmtpSettings
{
Host = "smtp.example.com",
Port = 587,
Username = "user@example.com",
Password = "secret"
};
var result = _validator.Validate(null, settings);
result.Succeeded.Should().BeTrue();
}
[Fact]
public void Validate_WithMissingHost_ReturnsFail()
{
var settings = new SmtpSettings { Host = "" };
var result = _validator.Validate(null, settings);
result.Succeeded.Should().BeFalse();
result.FailureMessage.Should().Contain("Host 是必填项");
}
[Fact]
public void Validate_WithUsernameButNoPassword_ReturnsFail()
{
var settings = new SmtpSettings
{
Host = "smtp.example.com",
Username = "user@example.com",
Password = null // 缺失!
};
var result = _validator.Validate(null, settings);
result.Succeeded.Should().BeFalse();
result.FailureMessage.Should().Contain("密码是必填项");
}
}Summary
总结
| Principle | Implementation |
|---|---|
| Fail fast | |
| Strongly-typed | Bind to POCO classes |
| Simple validation | Data Annotations |
| Complex validation | |
| Cross-property rules | |
| Environment-aware | Inject |
| Testable | Validators are plain classes |
| 原则 | 实现方式 |
|---|---|
| 快速失败 | |
| 强类型 | 绑定到POCO类 |
| 简单验证 | 数据注解 |
| 复杂验证 | |
| 跨属性规则 | |
| 环境感知 | 注入 |
| 可测试 | 验证器为普通类 |