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 | 默认 | 低 |
| 环境专属 JSON | AddJsonFile | 中 |
| 用户机密 | 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 文件 | 典型配置 |
|---|---|---|
| Development | appsettings.Development.json | 本地数据库、详细日志 |
| Staging | appsettings.Staging.json | 生产镜像数据、预发布 |
| Production | appsettings.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 服务都应该有的基础设施。
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。