Microsoft.ExtensionsConfigurationPatternsSkill microsoft-extensions-configuration

.NET 配置管理技能,包括 IValidateOptions 验证、强类型设置、启动时验证和 Options 模式,用于清晰的配置管理。

后端开发 0 次安装 0 次浏览 更新于 2/26/2026

Microsoft.Extensions 配置模式

何时使用此技能

使用此技能时:

  • 从 appsettings.json 绑定配置到强类型类
  • 在应用程序启动时验证配置(快速失败)
  • 实现设置的复杂验证逻辑
  • 设计可测试和可维护的配置类
  • 理解 IOptions<T>, IOptionsSnapshot<T> 和 IOptionsMonitor<T>

为什么配置验证很重要

**问题:**应用程序经常因为配置错误在运行时失败 - 缺少连接字符串,无效的URL,超出范围的值。这些失败发生在业务逻辑的深处,远离配置加载的位置,使得调试变得困难。

**解决方案:**在启动时验证配置。如果配置无效,应用程序会立即以清晰的错误消息失败。这就是“快速失败”原则。

// 坏:当有人尝试使用服务时在运行时失败
public class EmailService
{
    public EmailService(IOptions<SmtpSettings> options)
    {
        var settings = options.Value;
        // 抛出 NullReferenceException 生产10分钟后
        _client = new SmtpClient(settings.Host, settings.Port);
    }
}

// 好:在启动时以清晰的错误失败
// "SmtpSettings 验证失败:Host是必需的"

模式 1:基本选项绑定

定义设置类

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;
}

从配置绑定

// 在 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
  }
}

在服务中使用

public class EmailService
{
    private readonly SmtpSettings _settings;

    // IOptions<T> - 单例,启动时读取一次
    public EmailService(IOptions<SmtpSettings> options)
    {
        _settings = options.Value;
    }
}

模式 2:数据注释验证

对于简单的验证规则,使用数据注释:

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;
}

启用数据注释验证

builder.Services.AddOptions<SmtpSettings>()
    .BindConfiguration(SmtpSettings.SectionName)
    .ValidateDataAnnotations()  // 启用基于属性的验证
    .ValidateOnStart();         // 在启动时立即验证

关键点: .ValidateOnStart() 是关键。没有它,验证只在选项首次访问时运行,这可能是在应用程序运行几分钟或几小时后。


模式 3:IValidateOptions<T> 用于复杂验证

数据注释适用于简单规则,但复杂验证需要 IValidateOptions<T>

何时使用 IValidateOptions

场景 数据注释 IValidateOptions
必填字段
范围检查
正则表达式模式
跨属性验证
条件验证
外部服务检查
自定义错误消息和上下文 有限
验证器中的依赖注入

实现 IValidateOptions

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($"Port {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;
    }
}

注册验证器

builder.Services.AddOptions<SmtpSettings>()
    .BindConfiguration(SmtpSettings.SectionName)
    .ValidateDataAnnotations()  // 先运行属性验证
    .ValidateOnStart();

// 注册自定义验证器
builder.Services.AddSingleton<IValidateOptions<SmtpSettings>, SmtpSettingsValidator>();

顺序很重要: 首先运行数据注释,然后是 IValidateOptions 验证器。所有失败都一起收集和报告。


模式 4:带有依赖的验证器

IValidateOptions 验证器从 DI 解析,因此它们可以有依赖:

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("生产环境不能使用localhost数据库");
            }

            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必须指定一个DataSource");
                }
            }
            catch (Exception ex)
            {
                failures.Add($"ConnectionString格式错误:{ex.Message}");
            }
        }

        return failures.Count > 0
            ? ValidateOptionsResult.Fail(failures)
            : ValidateOptionsResult.Success;
    }
}

模式 5:命名选项

当你有相同设置类型的多个实例时(例如,多个数据库连接):

// 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");
    }
}

命名选项验证器

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;
    }
}

模式 6:选项生命周期

了解三个选项接口:

接口 生命周期 更改时重新加载 使用案例
IOptions<T> 单例 静态配置,一次性读取
IOptionsSnapshot<T> 作用域 是(每个请求) 需要新鲜配置的Web应用程序
IOptionsMonitor<T> 单例 是(带回调) 后台服务,实时更新

IOptionsMonitor 用于后台服务

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("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);
        }
    }
}

模式 7:配置后

在绑定后但在验证前修改选项:

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

builder.Services.AddOptions<ApiSettings>()
    .BindConfiguration("Api")
    .PostConfigure<IHostEnvironment>((options, env) =>
    {
        if (env.IsDevelopment())
        {
            options.Timeout = TimeSpan.FromMinutes(5); // 调试时更长的超时
        }
    });

模式 8:完整示例 - 生产设置类

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在Clustered模式下是必需的");
        }

        if (options.RemoteOptions.Port is null or < 0)
        {
            failures.Add("RemoteOptions.Port在Clustered模式下必须>=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需要Config发现");
                }
                break;

            case DiscoveryMethod.AzureTableStorage:
                if (options.AzureDiscoveryOptions == null)
                {
                    failures.Add("AzureDiscoveryOptions需要Azure发现");
                }
                break;
        }
    }
}

// 注册
builder.Services.AddOptions<AkkaSettings>()
    .BindConfiguration(AkkaSettings.SectionName)
    .ValidateDataAnnotations()
    .ValidateOnStart();

builder.Services.AddSingleton<IValidateOptions<AkkaSettings>, AkkaSettingsValidator>();

避免的反模式

1. 手动配置访问

// 坏:绕过验证,难以测试
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. 在构造函数中验证

// 坏:验证在运行时发生,而不是启动时
public class MyService
{
    public MyService(IOptions<Settings> options)
    {
        if (string.IsNullOrEmpty(options.Value.Required))
            throw new ArgumentException("Required is missing"); // 太晚了!
    }
}

// 好:在启动时验证
builder.Services.AddOptions<Settings>()
    .ValidateDataAnnotations()
    .ValidateOnStart();

3. 忘记 ValidateOnStart

// 坏:验证只在首次访问时运行
builder.Services.AddOptions<Settings>()
    .ValidateDataAnnotations(); // 缺少 ValidateOnStart!

// 好:如果无效,立即失败
builder.Services.AddOptions<Settings>()
    .ValidateDataAnnotations()
    .ValidateOnStart();

4. 在 IValidateOptions 中抛出

// 坏:抛出异常,打破验证链
public ValidateOptionsResult Validate(string? name, Settings options)
{
    if (options.Value < 0)
        throw new ArgumentException("Value cannot be negative"); // 错误!

    return ValidateOptionsResult.Success;
}

// 好:返回失败结果
public ValidateOptionsResult Validate(string? name, Settings options)
{
    if (options.Value < 0)
        return ValidateOptionsResult.Fail("Value cannot be negative");

    return ValidateOptionsResult.Success;
}

测试配置验证器

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  // 缺失!
        };

        var result = _validator.Validate(null, settings);

        result.Succeeded.Should().BeFalse();
        result.FailureMessage.Should().Contain("Password is required");
    }
}

总结

原则 实施
快速失败 .ValidateOnStart()
强类型 绑定到POCO类
简单验证 数据注释
复杂验证 IValidateOptions<T>
跨属性规则 IValidateOptions<T>
环境感知 注入 IHostEnvironment
可测试 验证器是普通类