ASP.NET Core 配置系统
大约 10 分钟约 2934 字
ASP.NET Core 配置系统
简介
ASP.NET Core 配置系统的核心价值,不只是“从 appsettings.json 里读值”,而是把不同来源的配置统一组织成一套可分层、可覆盖、可验证、可按环境切换的配置体系。对真实项目来说,配置系统通常直接决定:部署是否稳定、密钥是否安全、环境差异是否可控,以及线上问题是否容易定位。
特点
实现
配置来源与加载顺序
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// ASP.NET Core 默认已经加载:
// 1. appsettings.json
// 2. appsettings.{Environment}.json
// 3. User Secrets(开发环境)
// 4. 环境变量
// 5. 命令行参数
var app = builder.Build();// appsettings.json
{
"App": {
"Name": "SunnyFan API",
"Version": "1.0.0"
},
"Jwt": {
"SecretKey": "dev-secret-key-please-change",
"Issuer": "sunnyfan.local",
"Audience": "sunnyfan.api",
"ExpireMinutes": 120
},
"ConnectionStrings": {
"Default": "Server=localhost;Database=SunnyFanDb;Trusted_Connection=True;TrustServerCertificate=True",
"Redis": "localhost:6379"
},
"FeatureFlags": {
"EnableSwagger": true,
"EnableCache": true,
"MaxUploadSizeMB": 100
}
}// appsettings.Production.json
{
"Logging": {
"LogLevel": {
"Default": "Warning",
"Microsoft": "Error"
}
},
"FeatureFlags": {
"EnableSwagger": false
}
}# 环境变量覆盖配置
# Linux / Docker / Kubernetes 中很常见
export Jwt__SecretKey="production-super-secret-key"
export ConnectionStrings__Default="Server=prod-db;Database=SunnyFanDb;User Id=app;Password=***"IConfiguration 与强类型 Options
[ApiController]
[Route("api/config-demo")]
public class ConfigDemoController : ControllerBase
{
private readonly IConfiguration _configuration;
public ConfigDemoController(IConfiguration configuration)
{
_configuration = configuration;
}
[HttpGet]
public IActionResult Get()
{
var appName = _configuration["App:Name"];
var version = _configuration["App:Version"];
var db = _configuration.GetConnectionString("Default");
return Ok(new { appName, version, db });
}
}public class JwtSettings
{
public const string SectionName = "Jwt";
public string SecretKey { get; set; } = string.Empty;
public string Issuer { get; set; } = string.Empty;
public string Audience { get; set; } = string.Empty;
public int ExpireMinutes { get; set; } = 60;
}
public class FeatureFlags
{
public const string SectionName = "FeatureFlags";
public bool EnableSwagger { get; set; }
public bool EnableCache { get; set; }
public int MaxUploadSizeMB { get; set; }
}builder.Services.Configure<JwtSettings>(builder.Configuration.GetSection(JwtSettings.SectionName));
builder.Services.Configure<FeatureFlags>(builder.Configuration.GetSection(FeatureFlags.SectionName));public class JwtTokenService
{
private readonly JwtSettings _settings;
public JwtTokenService(IOptions<JwtSettings> options)
{
_settings = options.Value;
}
public int GetExpireMinutes() => _settings.ExpireMinutes;
}IOptions / Snapshot / Monitor 的区别
public class SnapshotFeatureService
{
private readonly FeatureFlags _features;
public SnapshotFeatureService(IOptionsSnapshot<FeatureFlags> options)
{
_features = options.Value;
}
public bool IsSwaggerEnabled() => _features.EnableSwagger;
}public class MonitorFeatureService
{
private readonly IOptionsMonitor<FeatureFlags> _monitor;
private readonly ILogger<MonitorFeatureService> _logger;
public MonitorFeatureService(IOptionsMonitor<FeatureFlags> monitor, ILogger<MonitorFeatureService> logger)
{
_monitor = monitor;
_logger = logger;
_monitor.OnChange(newValue =>
{
_logger.LogInformation("FeatureFlags changed. EnableCache={EnableCache}", newValue.EnableCache);
});
}
public FeatureFlags Current => _monitor.CurrentValue;
}三者区别:
- IOptions<T>:单例读取,适合启动后基本不变的配置
- IOptionsSnapshot<T>:每次请求读取最新值,适合 Web 请求上下文
- IOptionsMonitor<T>:支持长期运行服务和配置变更通知配置验证与启动失败保护
using System.ComponentModel.DataAnnotations;
public class JwtSettings
{
public const string SectionName = "Jwt";
[Required]
[MinLength(16)]
public string SecretKey { get; set; } = string.Empty;
[Required]
public string Issuer { get; set; } = string.Empty;
[Required]
public string Audience { get; set; } = string.Empty;
[Range(1, 1440)]
public int ExpireMinutes { get; set; } = 60;
}builder.Services
.AddOptions<JwtSettings>()
.Bind(builder.Configuration.GetSection(JwtSettings.SectionName))
.ValidateDataAnnotations()
.Validate(s => !s.SecretKey.Contains("please-change"), "JWT SecretKey 不能使用默认示例值")
.ValidateOnStart();配置验证的价值:
- 启动时直接失败,而不是上线运行后才暴露错误
- 对数据库连接、JWT Key、第三方回调地址等关键配置尤其重要自定义配置源与远程配置思路
public class DatabaseConfigurationSource : IConfigurationSource
{
private readonly string _connectionString;
public DatabaseConfigurationSource(string connectionString)
{
_connectionString = connectionString;
}
public IConfigurationProvider Build(IConfigurationBuilder builder)
=> new DatabaseConfigurationProvider(_connectionString);
}
public class DatabaseConfigurationProvider : ConfigurationProvider
{
private readonly string _connectionString;
public DatabaseConfigurationProvider(string connectionString)
{
_connectionString = connectionString;
}
public override void Load()
{
using var connection = new SqlConnection(_connectionString);
connection.Open();
using var command = connection.CreateCommand();
command.CommandText = "SELECT [Key], [Value] FROM AppSettings";
using var reader = command.ExecuteReader();
while (reader.Read())
{
Data[reader.GetString(0)] = reader.GetString(1);
}
}
}// 注册自定义配置源
builder.Configuration.Add(new DatabaseConfigurationSource(
builder.Configuration.GetConnectionString("Default")!));生产环境里更常见的远程配置来源:
- Consul / Nacos
- Azure App Configuration
- Kubernetes ConfigMap / Secret
- 数据库存储(适合少量动态配置)配置热更新与动态刷新
Options 热更新
// IOptionsMonitor<T> 支持配置热更新
// IOptions<T> — 单例,不更新
// IOptionsSnapshot<T> — Scoped,每次请求重新读取
// IOptionsMonitor<T> — 单例,支持变更通知
public class CacheConfig
{
public const string SectionName = "Cache";
public TimeSpan DefaultTtl { get; set; } = TimeSpan.FromMinutes(10);
public int MaxEntries { get; set; } = 1000;
public bool EnableCompression { get; set; } = true;
}
// 方式 1:使用 IOptionsMonitor(推荐,支持变更通知)
public class CacheService : IDisposable
{
private readonly CacheConfig _config;
private readonly IDisposable _changeListener;
public CacheService(IOptionsMonitor<CacheConfig> monitor)
{
_config = monitor.CurrentValue;
_changeListener = monitor.OnChange(newConfig =>
{
Console.WriteLine($"缓存配置已更新: TTL={newConfig.DefaultTtl}");
// 执行配置变更后的逻辑
});
}
public void Dispose() => _changeListener.Dispose();
}
// 方式 2:使用 IOptionsSnapshot(每次请求获取最新值)
public class PerRequestService
{
private readonly CacheConfig _config;
public PerRequestService(IOptionsSnapshot<CacheConfig> snapshot)
{
_config = snapshot.Value; // 每次请求重新读取配置
}
}
// 注册支持热更新
builder.Services.Configure<CacheConfig>(
builder.Configuration.GetSection(CacheConfig.SectionName));
// appsettings.json 必须设置 reloadOnChange: true
// builder.Configuration.AddJsonFile("appsettings.json", reloadOnChange: true);自定义配置变更监听
// 监听配置文件变更
var reloadToken = builder.Configuration.GetReloadToken();
_ = Task.Run(async () =>
{
while (true)
{
reloadToken.RegisterChangeCallback(_ =>
{
Console.WriteLine("配置文件已变更");
return null;
}, null);
await Task.Delay(Timeout.Infinite);
}
});
// 监听特定配置节的变化
ChangeToken.OnChange(
() => builder.Configuration.GetSection("Cache").GetReloadToken(),
() =>
{
var newTtl = builder.Configuration.GetValue<TimeSpan>("Cache:DefaultTtl");
Console.WriteLine($"缓存 TTL 变更为: {newTtl}");
});命名选项(Named Options)
// 多组同名配置(适合多租户、多缓存等场景)
builder.Services.Configure<CacheConfig>("Redis", config =>
{
config.DefaultTtl = TimeSpan.FromMinutes(30);
config.MaxEntries = 5000;
});
builder.Services.Configure<CacheConfig>("Memory", config =>
{
config.DefaultTtl = TimeSpan.FromMinutes(5);
config.MaxEntries = 1000;
});
// 使用 IOptionsSnapshot 获取命名选项
public class MultiCacheService
{
private readonly IOptionsSnapshot<CacheConfig> _snapshot;
public MultiCacheService(IOptionsSnapshot<CacheConfig> snapshot)
{
_snapshot = snapshot;
}
public CacheConfig GetRedisConfig() => _snapshot.Get("Redis");
public CacheConfig GetMemoryConfig() => _snapshot.Get("Memory");
}
// 使用 IOptionsMonitor 获取命名选项(支持变更通知)
public class MultiCacheMonitor
{
private readonly IOptionsMonitor<CacheConfig> _monitor;
public MultiCacheMonitor(IOptionsMonitor<CacheConfig> monitor)
{
_monitor = monitor;
}
public CacheConfig GetRedisConfig() => _monitor.Get("Redis");
// 监听命名选项的变更
public IDisposable WatchRedis(Action<CacheConfig> onChange)
{
return _monitor.OnChange("Redis", onChange);
}
}配置验证
启动时验证
// 方式 1:DataAnnotations 验证
public class DatabaseConfig
{
[Required]
public string ConnectionString { get; set; } = string.Empty;
[Range(1, 100)]
public int MaxPoolSize { get; set; } = 50;
[Range(1, 300)]
public int CommandTimeout { get; set; } = 30;
}
// 启用验证
builder.Services.AddOptions<DatabaseConfig>()
.Bind(builder.Configuration.GetSection("Database"))
.ValidateDataAnnotations()
.ValidateOnStart(); // 启动时立即验证(.NET 6+)
// 方式 2:自定义验证逻辑
builder.Services.AddOptions<CacheConfig>()
.Bind(builder.Configuration.GetSection("Cache"))
.Validate(config =>
{
if (config.DefaultTtl <= TimeSpan.Zero)
return false;
if (config.MaxEntries <= 0)
return false;
return true;
}, "缓存 TTL 必须大于 0,最大条目数必须大于 0")
.ValidateOnStart();
// 方式 3:自定义 IValidateOptions
public class CacheConfigValidator : IValidateOptions<CacheConfig>
{
public ValidateOptionsResult Validate(string? name, CacheConfig options)
{
var errors = new List<string>();
if (options.DefaultTtl < TimeSpan.FromSeconds(1))
errors.Add("缓存 TTL 不能小于 1 秒");
if (options.DefaultTtl > TimeSpan.FromHours(24))
errors.Add("缓存 TTL 不能大于 24 小时");
if (options.MaxEntries > 100000)
errors.Add("最大缓存条目数不能超过 100000");
return errors.Any()
? ValidateOptionsResult.Fail(errors)
: ValidateOptionsResult.Success;
}
}
builder.Services.AddSingleton<IValidateOptions<CacheConfig>, CacheConfigValidator>();配置调试辅助
// 打印所有配置来源(排查配置优先级)
public static class ConfigurationDebugger
{
public static void PrintSources(IConfiguration configuration)
{
Console.WriteLine("=== 配置来源(优先级从高到低)===");
foreach (var source in configuration.AsEnumerable())
{
Console.WriteLine($" {source.Key} = {source.Value}");
}
}
// 检查特定配置值的来源
public static void TraceValue(IConfiguration configuration, string key)
{
Console.WriteLine($"追踪配置: {key}");
foreach (var provider in ((IConfigurationRoot)configuration).Providers)
{
if (provider.TryGet(key, out var value))
{
Console.WriteLine($" 来源: {provider.GetType().Name} = {value}");
}
}
}
}敏感信息管理
User Secrets 与 Key Vault
// 开发环境使用 User Secrets
// dotnet user-secrets set "ConnectionStrings:Default" "Server=localhost;..."
// 生产环境使用环境变量
// ASPNETCORE_ConnectionStrings__Default
// 或双下划线:ConnectionStrings__Default
// 生产环境使用 Azure Key Vault
// 安装:Azure.Extensions.AspNetCore.Configuration.Secrets
builder.Configuration.AddAzureKeyVault(
new Uri("https://my-vault.vault.azure.net/"),
new DefaultAzureCredential());
// Kubernetes Secret
// 通过环境变量挂载 Secret
// env:
// - name: ConnectionStrings__Default
// valueFrom:
// secretKeyRef:
// name: app-secrets
// key: connection-string
// 配置加密值
public class EncryptedConfigService
{
private readonly IConfiguration _config;
private readonly IDataProtectionProvider _protector;
public EncryptedConfigService(IConfiguration config, IDataProtectionProvider protector)
{
_config = config;
_protector = protector;
}
public string GetDecryptedValue(string key)
{
var encrypted = _config[key];
if (string.IsNullOrEmpty(encrypted)) return string.Empty;
var protector = _protector.CreateProtector("ConfigEncryption");
return protector.Unprotect(encrypted);
}
}优点
缺点
缺点
总结
ASP.NET Core 配置系统的关键不是“会读取配置”,而是能把配置来源、优先级、验证、环境隔离和安全边界组织成一套稳定机制。对于生产项目,建议把强类型绑定、启动验证、环境变量覆盖和敏感信息隔离作为默认实践,而不是可选增强。
关键知识点
IConfiguration解决“怎么读”,Options 模式解决“怎么管”。- 配置优先级和来源路径,往往决定线上问题排查速度。
- 敏感配置不应该留在仓库 JSON 文件中。
- 热更新能力必须和业务代码配合,不能想当然。
项目落地视角
- 小型项目用 JSON + 环境变量就足够稳定。
- 中大型项目适合引入强类型 Options + 启动验证。
- 容器部署项目优先用环境变量、ConfigMap、Secret。
- 多环境系统要明确:哪些配置静态、哪些可动态调整、哪些必须走审批。
常见误区
- 把所有配置都写死在
appsettings.json并提交到 Git。 - 不做配置验证,导致错误直到生产运行时才暴露。
- 不清楚当前值来自 appsettings、环境变量还是配置中心。
- 认为支持热更新,就等于所有业务都能安全动态切换。
进阶路线
- 深入学习 Options Pattern、命名选项(Named Options)和验证扩展。
- 结合配置中心实现远程动态配置。
- 将配置治理与 CI/CD、密钥管理、审计体系结合。
- 研究多租户和多区域场景下的配置隔离策略。
适用场景
- 所有 ASP.NET Core Web API / MVC / Worker Service 项目。
- 多环境部署、容器化部署项目。
- 需要 Feature Flags、动态开关、外部服务配置的系统。
- 有安全和审计要求的生产服务。
落地建议
- 关键配置都建立强类型模型,不要长期靠字符串键访问。
- 敏感信息统一走环境变量或密钥管理系统。
- 对 JWT、数据库、第三方回调地址等关键配置启用启动验证。
- 对配置来源、覆盖规则和 owner 形成团队约定文档。
排错清单
- 值不对时,先确认来自哪一层配置源。
- 配置变更不生效时,先检查是否支持 reloadOnChange 或 Monitor。
- 容器部署异常时,先检查环境变量命名是否用了双下划线。
- 线上与本地不一致时,先核对环境名、Secrets 和外部配置源。
复盘问题
- 当前项目最关键的配置有哪些?它们是否已经被强类型绑定和验证?
- 配置问题出现时,团队能否快速定位值的来源?
- 哪些配置应该支持动态调整,哪些必须通过发布变更?
- 现在的配置管理方式,是否足以支撑你们的生产复杂度?
