4. 配置系统与选项模式

ASP.NET Core 的配置系统并不是“读取一个 JSON 文件”这么简单,它本质上是一条由多个配置源叠加而成的键值管道。理解这条管道,才能正确使用 appsettings.json、多环境覆盖、选项模式以及密钥管理。

一、配置管道:多源叠加与后加载优先

应用启动时,WebApplicationBuilder 会按固定顺序注册默认配置源,后加入的源会覆盖先加入的同名键。典型顺序是:appsettings.json → appsettings.{Environment}.json → 用户机密(仅开发环境)→ 环境变量 → 命令行参数。因此生产环境用环境变量覆盖 JSON 中的默认值,是标准做法。

配置键使用冒号分隔层级,例如 ConnectionStrings:Default 对应 JSON 中的嵌套对象。环境变量中冒号需替换为双下划线,写作 ConnectionStrings__Default。

ASP.NET Core 配置源叠加与优先级 appsettings.json 基础默认值 appsettings.Dev.json 环境覆盖 User Secrets 仅开发环境 环境变量 部署覆盖 命令行 最高优先 IConfiguration 统一键值视图 IOptions<T> 单例,启动时绑定一次 适合不变配置 IOptionsSnapshot<T> Scoped,每请求重新绑定 适合请求级配置 IOptionsMonitor<T> 单例,支持变更通知 适合热更新场景

二、IConfiguration 的直接读取

在最小 API 或控制器中,可以直接注入 IConfiguration 读取值:

var builder = WebApplication.CreateBuilder(args);

// 读取单个值
string? conn = builder.Configuration.GetConnectionString("Default");
int timeout = builder.Configuration.GetValue<int>("Http:TimeoutSeconds", 30);

// 绑定到强类型对象
var jwtSection = builder.Configuration.GetSection("Jwt");
builder.Services.Configure<JwtOptions>(jwtSection);

直接读取适合少量、零散的值;一旦配置项成组出现,就应使用选项模式,避免字符串键散落在代码各处。

三、选项模式:三种生命周期的选择

选项模式把配置节绑定到 POCO 类,并通过 DI 注入。三种接口的差异决定了使用场景:

接口生命周期是否感知变更典型场景
IOptions<T>Singleton否启动后不变的配置
IOptionsSnapshot<T>Scoped是(每请求)请求级读取最新值
IOptionsMonitor<T>Singleton是(实时回调)后台服务、热更新

注意:IOptionsSnapshot 是 Scoped,不能注入到 Singleton 服务中,否则会抛出生命周期不匹配异常。后台服务应使用 IOptionsMonitor,并通过 OnChange 注册回调:

public class CacheWarmupService : BackgroundService
{
    private readonly IOptionsMonitor<CacheOptions> _options;

    public CacheWarmupService(IOptionsMonitor<CacheOptions> options)
    {
        _options = options;
        _options.OnChange(opt =>
            Console.WriteLine($"缓存容量变更为 {opt.Capacity}"));
    }

    protected override async Task ExecuteAsync(CancellationToken token)
    {
        while (!token.IsCancellationRequested)
        {
            var capacity = _options.CurrentValue.Capacity;
            // 使用最新配置执行预热逻辑
            await Task.Delay(TimeSpan.FromMinutes(5), token);
        }
    }
}

四、多环境配置的落地方式

环境由 ASPNETCORE_ENVIRONMENT 决定,常见值为 Development、Staging、Production。启动时按环境名加载对应文件,例如 appsettings.Production.json。生产环境不应把连接字符串写进 JSON,而应通过环境变量或密钥服务注入。

在 launchSettings.json 中为不同 profile 设置 ASPNETCORE_ENVIRONMENT,本地调试即可自动切换配置。

五、自定义配置源

当配置来自数据库、配置中心或自定义文件格式时,可实现 IConfigurationSource 与 ConfigurationProvider:

public class DbConfigSource : IConfigurationSource
{
    public string ConnectionString { get; set; } = "";
    public IConfigurationProvider Build(IConfigurationBuilder builder)
        => new DbConfigProvider(ConnectionString);
}

public class DbConfigProvider : ConfigurationProvider
{
    private readonly string _conn;
    public DbConfigProvider(string conn) => _conn = conn;

    public override void Load()
    {
        // 从数据库读取键值对,填充 Data 字典
        Data["App:FeatureFlag"] = "true";
    }
}

// 注册
builder.Configuration.Add(new DbConfigSource
{
    ConnectionString = builder.Configuration.GetConnectionString("ConfigDb")!
});

若需要热更新,可让 Provider 实现 IDisposable 并调用 OnReload() 触发变更通知。

六、用户机密与 Azure Key Vault

开发阶段用用户机密避免把敏感值提交到仓库。在项目目录执行:

dotnet user-secrets init
dotnet user-secrets set "Jwt:SigningKey" "dev-only-32-char-secret-key-0001"
dotnet user-secrets list

机密存储在用户配置目录,不进入项目文件。生产环境则推荐 Azure Key Vault,通过 DefaultAzureCredential 免密访问:

builder.Configuration.AddAzureKeyVault(
    new Uri("https://my-vault.vault.azure.net/"),
    new DefaultAzureCredential());

Key Vault 中的密钥名不能含冒号,需用双横线替代,例如 Jwt--SigningKey,加载后会自动映射为 Jwt:SigningKey。这样代码无需改动,仅靠配置源顺序即可完成从开发到生产的切换。

Key Vault 访问依赖托管标识或开发者登录凭据,部署到 Azure 时应为应用启用系统分配的托管标识,并在 Key Vault 中授予“机密读取”权限,避免把客户端密钥写入配置文件。

综合来看,配置系统的核心是“源顺序决定覆盖、选项模式决定读取方式、密钥服务决定安全边界”。把这三件事分开处理,多环境部署和敏感信息管理就会变得清晰可控。