直接用IConfigurationSource+IConfigurationProvider实现自定义配置源更灵活、跨平台、支持热重载,而ConfigurationManager已弃用,无法享受现代配置能力如IOptionsMonitor、环境变量覆盖等。

直接用 IConfigurationSource + IConfigurationProvider 实现自定义配置源,比继承 ConfigurationSection 更灵活、更贴近 .NET Core / .NET 5+ 的配置模型,但容易在生命周期、重载机制和线程安全上出错。
为什么不用 ConfigurationManager.GetSection?
.NET Core 及以后版本已弃用 ConfigurationManager 和 App.config 的传统加载方式;IConfigurationSource 是跨平台、可组合、支持热重载的现代配置抽象。如果你还在用 System.Configuration 那套,就无法享受 HostBuilder.ConfigureAppConfiguration、环境变量自动覆盖、IOptionsMonitor<T> 等核心能力。
常见错误现象:
- 调用
ConfigurationManager.GetSection("xxx")返回 null —— 因为没注册自定义ConfigurationSection类型,或configSections位置不对(必须是<configuration>下第一个子节点) - 配置变更后不生效 ——
ConfigurationManager是静态快照,不支持重载 - 类库中读不到自己的
App.config——ConfigurationManager.OpenExeConfiguration默认只读主程序的配置文件,类库的App.config不会被自动加载
实现 IConfigurationSource 和 IConfigurationProvider 的最小可行结构
关键不是写多少代码,而是把职责分清:IConfigurationSource 负责“我从哪来”,IConfigurationProvider 负责“我怎么读”。两者必须成对注册,且 Provider 必须在 Load() 中填充 Data 字典。
实操建议:
-
IConfigurationSource实现只需一个Build方法,返回你的Provider实例 —— 别在里面做耗时操作(如查数据库),留给Provider.Load() -
IConfigurationProvider的Load()方法会被框架自动调用一次;若需定期刷新,自己加Timer或监听外部事件,再调OnReload() -
Data是IDictionary<string, string>,键名必须用冒号分隔层级(如"ConnectionStrings:Default"),不能用点或斜杠 - 如果从数据库读取,避免在
Load()里用同步 IO(如Query())—— 改用QueryAsync()并在Provider构造时传入IServiceProvider获取IDbConnection
示例片段(SQL Server 源):
public class SqlServerConfigurationSource : IConfigurationSource
{
public string ConnectionString { get; }
public bool ReloadPeriodically { get; set; } = false;
public int PeriodInSeconds { get; set; } = 30;
<pre class='brush:php;toolbar:false;'>public SqlServerConfigurationSource(string connectionString) =>
ConnectionString = connectionString;
public IConfigurationProvider Build(IConfigurationBuilder builder) =>
new SqlServerConfigurationProvider(this);}
public class SqlServerConfigurationProvider : ConfigurationProvider { private readonly SqlServerConfigurationSource _source;
public SqlServerConfigurationProvider(SqlServerConfigurationSource source) =>
_source = source;
public override void Load()
{
using var conn = new SqlConnection(_source.ConnectionString);
conn.Open();
using var cmd = conn.CreateCommand();
cmd.CommandText = "SELECT [Key], [Value] FROM Configurations";
using var reader = cmd.ExecuteReader();
Data = new Dictionary<string, string>();
while (reader.Read())
Data[(string)reader["Key"]] = (string)reader["Value"];
}}
注册自定义源时最容易漏掉的三件事
注册本身很简单,但以下三点不处理,配置就进不了 IConfiguration 树:
- 没在
Program.cs或Startup.cs的ConfigureAppConfiguration中显式调用builder.Add(new SqlServerConfigurationSource(connStr))—— 框架不会自动发现你写的源 - 数据库查询返回的键名含空格或特殊字符(如
"My:Api:Timeout Sec"),会导致绑定失败;应统一用下划线或驼峰,例如"My:Api:TimeoutSec" - 没处理并发读写:若
Load()被多线程同时触发(比如多个IOptionsSnapshot<T>初始化),Data字典可能被破坏 —— 必须在Load()开头加锁,或用ConcurrentDictionary替换基类的Data
绑定到强类型对象时,路径大小写必须完全匹配
IOptions<MyConfig> 绑定依赖配置键的完整路径,且严格区分大小写。比如你的数据库返回键 "database:connectionstring",而类中属性叫 public string ConnectionString { get; set; },那它根本不会被赋值 —— 因为默认绑定器找的是 "Database:ConnectionString"。
解决办法只有两个:
- 在数据库里存全大驼峰键(
"Database:ConnectionString"),和 C# 属性命名一致 - 注册时用
ConfigureOptions手动映射,或实现IConfigureOptions<MyConfig>做转换
别指望 ConfigurationBinder.Bind 自动忽略大小写 —— 它不会。


















