配置系统与 Options 模式

系统讲解 .NET 配置系统的提供程序链与环境配置,深入 Options 模式的强类型绑定、校验与命名选项,并给出配置刷新与敏感信息保护的最佳实践。

1. IConfiguration 与提供程序链

一句话总结: IConfiguration 把分层键值对抽象成统一读取接口,多个提供程序按注册顺序叠加,后注册的优先级更高。

ASP.NET Core 的配置系统是「提供程序链」:appsettings.json、环境变量、命令行参数等各自是一个提供程序,它们按顺序注册并后写覆盖先写。IConfiguration 用 Key:Section:Value 的分层键读取配置,不关心底层来源。

var builder = WebApplication.CreateBuilder(args);

// 默认已经注册了 appsettings.json / 环境变量 / 命令行
// 手动追加自定义 JSON 文件,可选、可刷新
builder.Configuration
    .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
    .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json",
                 optional: true, reloadOnChange: true)
    .AddEnvironmentVariables("APP_")          // 只接收 APP_ 前缀的变量
    .AddCommandLine(args);                     // 命令行参数最高优先级

var app = builder.Build();

// 分层键读取
var conn = app.Configuration.GetConnectionString("Default");
var timeout = app.Configuration.GetValue<int>("Cache:SlidingExpiryMinutes");
{
  "ConnectionStrings": {
    "Default": "Server=db;Database=shop;Trusted_Connection=true;"
  },
  "Cache": {
    "SlidingExpiryMinutes": 20
  }
}
提供程序注册方法优先级
appsettings.json默认低
环境专属 JSONAddJsonFile中
用户机密AddUserSecrets中高
环境变量AddEnvironmentVariables高
命令行AddCommandLine最高

避坑: 环境变量名里的 : 在部分平台(老版 Windows 环境变量)不支持,配置系统兼容用 __(双下划线)表示层级:Cache__SlidingExpiryMinutes。另外密钥绝不能进 appsettings.json——那是会进版本控制的明文。

2. 环境配置与按环境切换

一句话总结: ASPNETCORE_ENVIRONMENT 决定加载哪份环境专属配置,开发、测试、生产用不同的 appsettings 文件与变量组合。

环境名(Development / Staging / Production)通过 ASPNETCORE_ENVIRONMENT 环境变量设定,框架按 appsettings.{环境名}.json 加载环境专属配置。这样连接串、日志级别、外部服务地址能随环境切换,代码零改动。

# Linux / macOS
export ASPNETCORE_ENVIRONMENT=Production
dotnet run

# Windows PowerShell
$env:ASPNETCORE_ENVIRONMENT = "Production"
dotnet run

# 命令行注入(最高优先级)
dotnet run --environment Production
// 按环境分支配置
if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/error");
}

// 按环境读取开关
var verboseLogging = builder.Configuration.GetValue<bool>("Logging:EnableVerbose");

// IHostEnvironment 注入
public class StartupProbe(IHostEnvironment env)
{
    public string Mode => env.IsProduction() ? "prod" : env.EnvironmentName;
}
环境appsettings 文件典型配置
Developmentappsettings.Development.json本地数据库、详细日志
Stagingappsettings.Staging.json生产镜像数据、预发布
Productionappsettings.Production.json真实连接串、最小日志

避坑: 环境专属文件里仍可能有敏感值(比如 staging 的真实数据库)。正确姿势是:配置文件只放非敏感的默认值,连接串/密钥一律走用户机密或环境变量/Secret Manager。判断环境用 IsDevelopment() 这类 API,别手写字符串比较。

3. 强类型绑定与 Options 模式

一句话总结: Options 模式把配置节绑定成强类型 POCO,通过 IOptions 注入使用,编译期类型安全替代魔法字符串。

services.Configure<T>(configuration.GetSection("节名")) 把 JSON 节映射为强类型对象,业务代码注入 IOptions<T>、IOptionsSnapshot<T> 或 IOptionsMonitor<T> 读取。相比到处 GetValue<string>("Foo:Bar"),强类型绑定让配置有类型、有文档、可测试。

// 配置节
// "Cache": { "SlidingExpiryMinutes": 20, "Redis": "..." }

public class CacheOptions
{
    public int SlidingExpiryMinutes { get; set; } = 15;   // 默认值兜底
    public string? Redis { get; set; }
}

// 注册:绑定 Cache 节
builder.Services.Configure<CacheOptions>(
    builder.Configuration.GetSection("Cache"));

// 消费:注入强类型
public class OrderService(IOptions<CacheOptions> cacheOptions)
{
    public int GetExpiry() => cacheOptions.Value.SlidingExpiryMinutes;
}

// 用 OptionsBuilder 链式配置
builder.Services.AddOptions<CacheOptions>()
    .Bind(builder.Configuration.GetSection("Cache"))
    .ValidateDataAnnotations();          // 绑定后校验
接口生命周期刷新能力
IOptions<T>Singleton不刷新
IOptionsSnapshot<T>Scoped每请求重新计算
IOptionsMonitor<T>Singleton文件变更即时刷新

避坑: IOptions<T> 是单例且不感知配置变更;需要「改 appsettings 后热生效」必须用 IOptionsMonitor<T> 并订阅 OnChange。把绑定节名写错不会报错,只会在读取时全是默认值——最好加 ValidateDataAnnotations 或自定义校验把错误提前暴露。

4. Options 校验

一句话总结: Options 校验在启动或首次解析时验证配置正确性,Data Annotation 与自定义验证函数双管齐下,把「启动即失败」变成可预期行为。

配置错误越早暴露代价越小。ValidateDataAnnotations() 跑内置规则,Validate(...) 写自定义逻辑,ValidateOnStart() 让应用在启动阶段就校验而不是等到第一次使用。这样 CI 里配置写错会直接构建失败。

public class SmsOptions
{
    [Required, StringLength(32)]
    public string AccessKey { get; set; } = string.Empty;

    [Required]
    public string Secret { get; set; } = string.Empty;

    [Range(1, 60)]
    public int TimeoutSeconds { get; set; } = 10;
}

builder.Services.AddOptions<SmsOptions>()
    .Bind(builder.Configuration.GetSection("Sms"))
    .ValidateDataAnnotations()
    .Validate(o => o.AccessKey != o.Secret,
              "AccessKey 与 Secret 不能相同")
    .ValidateOnStart();     // 应用启动时立即校验
// 备选:完整配置对象 + IValidateOptions
public class ValidateSmsOptions : IValidateOptions<SmsOptions>
{
    public ValidateOptionsResult Validate(string? name, SmsOptions options)
    {
        if (string.IsNullOrWhiteSpace(options.AccessKey))
            return ValidateOptionsResult.Fail("缺少 Sms:AccessKey");
        if (options.TimeoutSeconds is < 1 or > 60)
            return ValidateOptionsResult.Fail("TimeoutSeconds 超出范围");
        return ValidateOptionsResult.Success;
    }
}
校验方式时机适用
ValidateDataAnnotations解析时必填/范围/长度
Validate(...) 委托解析时跨字段规则
ValidateOnStart启动时尽早失败
IValidateOptions<T>自定义复杂组合校验

避坑: ValidateDataAnnotations 默认在首次解析 Options 时才触发,不是注册时。生产上配置错误最好让进程拒绝启动——用 ValidateOnStart(),把「配置错了」变成部署期可见的失败,而不是运行时悄悄降级。

5. 命名 Options 与多租户配置

一句话总结: Configure<T>(name, ...) 注册命名 Options,同一类型可为不同租户或渠道保存多份配置,Get(name) 按名取用。

单实例服务要支持多个下游(多个 Redis 实例、多个第三方渠道)时,命名 Options 是标准解法:同一个 Options 类型注册多个名字,注入 IOptionsSnapshot<T> 后用 Get("name") 读取。

// 两个渠道共用同一结构
// "Payment": {
//   "WeChat": { "AppId": "...", "Secret": "..." },
//   "Alipay": { "AppId": "...", "Secret": "..." }
// }

builder.Services.Configure<PaymentChannelOptions>("WeChat",
    builder.Configuration.GetSection("Payment:WeChat"));
builder.Services.Configure<PaymentChannelOptions>("Alipay",
    builder.Configuration.GetSection("Payment:Alipay"));

public class PaymentService(IOptionsSnapshot<PaymentChannelOptions> channels)
{
    public async Task PayAsync(string channel, decimal amount)
    {
        var opts = channels.Get(channel);      // 按名取配置
        // 使用 opts.AppId / opts.Secret 发起支付
    }
}
API作用
Configure<T>(name, section)注册命名配置
GetOptions<T>(name)取指定名配置
ConfigureAll<T>(section)应用到所有命名
PostConfigure<T>(name, ...)解析后二次加工

一句话: 命名 Options 是「一份类型、多份实例」的配置抽象。租户隔离、多渠道、多集群场景下,它比「每租户一个配置类」优雅得多——配置结构统一,取值按名路由,业务代码不感知差异。

6. 配置刷新与热更新

一句话总结: reloadOnChange 让 JSON 变更实时生效,IOptionsMonitor 提供新的配置值与变更回调,无需重启进程。

AddJsonFile(..., reloadOnChange: true) 启用文件监视,IOptionsMonitor<T> 暴露 CurrentValue 与 OnChange 回调,让特性开关、限流阈值等配置免重启热更新。这是灰度开关、动态调参的基础设施。

// 注册:开启重载
builder.Configuration.AddJsonFile("appsettings.json",
    optional: false, reloadOnChange: true);
builder.Services.AddOptions<FeatureFlags>()
    .Bind(builder.Configuration.GetSection("FeatureFlags"))
    .ValidateOnStart();

public class FeatureService(IOptionsMonitor<FeatureFlags> flags)
{
    public bool IsEnabled(string name)
    {
        // CurrentValue 每次读取最新值
        return flags.CurrentValue.Map.GetValueOrDefault(name);
    }

    public void Watch(Action<FeatureFlags> onChange)
    {
        // 配置变更时触发回调
        flags.OnChange(onChange);
    }
}
// 运行时动态更新示例
var flagService = app.Services.GetRequiredService<FeatureService>();
flagService.Watch(f => Console.WriteLine($"新配置: 折扣={f.DiscountPercent}%"));

app.MapGet("/flag/discount", () =>
    flagService.IsEnabled("discount") ? "on" : "off");
手段刷新粒度备注
reloadOnChange文件级依赖 FileSystemWatcher
IOptionsMonitor即时原子替换 CurrentValue
IOptionsSnapshot每请求请求内一致
手动 IConfiguration 重建粗粒度简单但费事

避坑: 热更新不等于「所有配置都能热」。连接串、依赖外部连接的配置在运行时变更往往无法平滑应用——刷新的是配置对象,不是已建立的连接。区分「运行时可变」(开关、阈值)与「启动时定死」(连接串、绑定端口),后者不要开 reloadOnChange。

7. 敏感信息保护与最佳实践

一句话总结: 密钥与连接串应远离配置文件,User Secrets 用于开发、环境变量用于测试生产、Secret Manager 或云密钥服务用于生产,且日志绝不能打印配置值。

配置最佳实践的核心是分层保密:开发用 User Secrets(本地文件、不进版本控制)、CI/测试用环境变量、生产用密钥管理服务(Azure Key Vault、AWS Secrets Manager 等)。IConfiguration 的提供程序链让密钥来源与代码解耦。

# 初始化 User Secrets
dotnet user-secrets init
# 设置密钥
dotnet user-secrets set "Sms:Secret" "dev-only-secret"
# 查看
dotnet user-secrets list
// 开发环境:User Secrets 覆盖 appsettings
if (builder.Environment.IsDevelopment())
{
    builder.Configuration.AddUserSecrets<Program>();
}

// 生产:从密钥服务加载
builder.Configuration.AddAzureKeyVault(
    new Uri("https://myvault.vault.azure.net/"),
    new DefaultAzureCredential());
场景密钥存放
本地开发User Secrets
CI 测试CI 平台的 Secret 变量
生产Azure Key Vault / AWS Secrets Manager
团队共享非敏感默认值appsettings.json

避坑: 两个最容易翻车的细节——其一,AddUserSecrets 只应在开发环境调用,否则生产也会读本地文件;其二,日志中间件或异常处理里绝对不要输出完整连接串,打日志前把密码字段脱敏。另外 .gitignore 应忽略 appsettings.Production.json 等可能含密钥的文件。

8. 总结

环节要点
提供程序链JSON/环境变量/命令行按序叠加,后写覆盖
环境配置ASPNETCORE_ENVIRONMENT 选择 appsettings 文件
Options 模式强类型绑定 POCO,注入 IOptions
校验Data Annotation + ValidateOnStart 尽早失败
命名 Options一份类型多份实例,按名路由
热更新reloadOnChange + IOptionsMonitor
敏感信息User Secrets / 环境变量 / 密钥服务,日志脱敏

配置系统是 .NET 应用的第一道工程化防线:它决定了环境切换、密钥安全与运行时可调性。把配置做对,应用就能「一份代码跑遍开发到生产」;把配置做错,轻则启动崩溃,重则密钥泄漏。Options 模式 + 启动校验 + 密钥分层,是每个 .NET 服务都应该有的基础设施。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「csharp」更多文章

  1. 消息与后台任务
  2. 缓存与并发控制
  3. 测试体系:xUnit 与 Moq