揭开 ConfigurationManager 的面纱

开发 后端
在这个系列中,我将探索一下 .NET 6 中的一些新特性。已经有很多关于 .NET 6 的内容,包括很多来自 .NET 和 ASP.NET 团队本身的文章。在这个系列中,我将探索一下这些特性背后的一些代码。

在这个系列中,我将探索一下 .NET 6 中的一些新特性。已经有很多关于 .NET 6 的内容,包括很多来自 .NET 和 ASP.NET 团队本身的文章。在这个系列中,我将探索一下这些特性背后的一些代码。

在这第一篇文章中,来研究一下 ConfigurationManager 类,讲一下为什么要新增这个类,并看一下它的的一些实现代码。

1什么是 ConfigurationManager

如果你的第一反应是“什么是 ConfigurationManager”,那么不用担心,你没有错过一个重要的公告:

加入 ConfigurationManager 是为了支持 ASP.NET Core 的新 WebApplication 模型,用于简化 ASP.NET Core 的启动代码。然而 ConfigurationManager 在很大程度上是一个实现细节。它的引入是为了优化一个特定的场景(我很快会讲),但在大多数情况下,你不需要(也不会)知道你在使用它。

在我们讨论 ConfigurationManager 本身之前,我们先来看看它所取代的东西和原因。

2.NET 5 中的/

.NET 5 围绕配置暴露了多种类型,但在你的应用程序中直接使用的两个主要类型是:

  • IConfigurationBuilder - 用来添加配置源。在构建器上调用 Build() 读取每个配置源,并构建最终的配置。
  • IConfigurationRoot - 代表最终“构建”好的配置。

IConfigurationBuilder 接口主要是一个围绕配置源列表的封装器。配置提供者通常包括扩展方法(如 AddJsonFile() 和 AddAzureKeyVault()),将配置源添加到 Sources 列表中。

  1. public interface IConfigurationBuilder 
  2.     IDictionary<string, object> Properties { get; } 
  3.     IList<IConfigurationSource> Sources { get; } 
  4.     IConfigurationBuilder Add(IConfigurationSource source); 
  5.     IConfigurationRoot Build(); 

同时,IConfigurationRoot 代表最终“层”的配置值,结合了每个配置源的所有值,以提供所有配置值的最终“平面”视图。

后者配置提供者(环境变量)覆盖了前者配置提供者(appsettings.json、sharedsettings.json)添加的值。

在 .NET 5 及以前的版本中,IConfigurationBuilder 和 IConfigurationRoot 接口分别由 ConfigurationBuilder 和 ConfigurationRoot 实现。如果你直接使用这些类型,你可能会这样做:

  1. var builder = new ConfigurationBuilder(); 
  2.  
  3. // add static values 
  4. builder.AddInMemoryCollection(new Dictionary<string, string> 
  5.     { "MyKey""MyValue" }, 
  6. }); 
  7.  
  8. // add values from a json file 
  9. builder.AddJsonFile("appsettings.json"); 
  10.  
  11. // create the IConfigurationRoot instance 
  12. IConfigurationRoot config = builder.Build(); 
  13.  
  14. string value = config["MyKey"]; // get a value 
  15. IConfigurationSection section = config.GetSection("SubSection"); //get a section 

在一个典型的 ASP.NET Core 应用程序中,你不会自己创建 ConfigurationBuilder,或调用 Build(),但除此之外,这就是幕后发生的事情。这两种类型之间有明确的分离,而且在大多数情况下,配置系统运行良好,那么为什么我们在.NET 6 中需要一个新类型呢?

3.NET 5 中“部分构建”配置的问题

这种设计的主要问题是在你需要“部分”构建配置的时候。当你将配置存储在 Azure Key Vault 等服务中,甚至是数据库中时,这是一个常见的问题。

例如,以下是在 ASP.NET Core 中的 ConfigureAppConfiguration() 里面从 Azure Key Vault 读取 secrects 的建议方式:

  1. .ConfigureAppConfiguration((context, config) => 
  2.     // "normal" configuration etc 
  3.     config.AddJsonFile("appsettings.json"); 
  4.     config.AddEnvironmentVariables(); 
  5.  
  6.     if (context.HostingEnvironment.IsProduction()) 
  7.     { 
  8.         IConfigurationRoot partialConfig = config.Build(); // build partial config 
  9.         string keyVaultName = partialConfig["KeyVaultName"]; // read value from configuration 
  10.         var secretClient = new SecretClient( 
  11.             new Uri($"https://{keyVaultName}.vault.azure.net/"), 
  12.             new DefaultAzureCredential()); 
  13.         config.AddAzureKeyVault(secretClient, new KeyVaultSecretManager()); // add an extra configuration source 
  14.         // The framework calls config.Build() AGAIN to build the final IConfigurationRoot 
  15.     } 
  16. }) 

配置 Azure Key Vault 提供者需要一个配置值,所以你陷入了一个鸡和蛋的问题--在你建立配置之前,你无法添加配置源。

解决办法是:

  • 添加“初始”配置值;
  • 通过调用 IConfigurationBuilder.Build() 构建“部分”配置结果;
  • 从生成的 IConfigurationRoot 中检索所需的配置值;
  • 使用这些值来添加剩余的配置源;
  • 框架隐含地调用 IConfigurationBuilder.Build(),生成最终的 IConfigurationRoot 并将其用于最终的应用配置。

这整个过程有点乱,但它本身并没有什么问题,那么缺点是什么呢?

缺点是我们必须调用 Build() 两次:一次是只使用第一个源来构建 IConfigurationRoot,另一次是使用所有源来构建 IConfiguartionRoot,包括 Azure Key Vault 源。

在默认的 ConfigurationBuilder 实现中,调用 Build() 会遍历所有的源,加载提供者,并将这些传递给 ConfigurationRoot 的一个新实例。

  1. public IConfigurationRoot Build() 
  2.     var providers = new List<IConfigurationProvider>(); 
  3.     foreach (IConfigurationSource source in Sources) 
  4.     { 
  5.         IConfigurationProvider provider = source.Build(this); 
  6.         providers.Add(provider); 
  7.     } 
  8.     return new ConfigurationRoot(providers); 

然后,ConfigurationRoot 依次循环遍历这些提供者,并加载配置值。

  1. public class ConfigurationRoot : IConfigurationRoot, IDisposable 
  2.     private readonly IList<IConfigurationProvider> _providers; 
  3.     private readonly IList<IDisposable> _changeTokenRegistrations; 
  4.  
  5.     public ConfigurationRoot(IList<IConfigurationProvider> providers) 
  6.     { 
  7.         _providers = providers; 
  8.         _changeTokenRegistrations = new List<IDisposable>(providers.Count); 
  9.  
  10.         foreach (IConfigurationProvider p in providers) 
  11.         { 
  12.             p.Load(); 
  13.             _changeTokenRegistrations.Add(ChangeToken.OnChange(() => p.GetReloadToken(), () => RaiseChanged())); 
  14.         } 
  15.     } 
  16.     // ... remainder of implementation 

如果你在应用启动时调用 Build() 两次,那么所有这些都会发生两次。

一般来说,从配置源获取数据一次以上并无大碍,但这是不必要的工作,而且经常涉及到(相对缓慢的)文件读取等。

这是一种常见的模式,所以在 .NET 6 中引入了一个新的类型来避免这种“重新构建”,即 ConfigurationManager。

4..NET 6 中的配置管理器

作为 .NET 6 中“简化”应用模型的一部分,.NET 团队增加了一个新的配置类型--ConfigurationManager。这种类型同时实现了 IConfigurationBuilder 和 IConfigurationRoot。通过将这两种实现结合在一个单一的类型中,.NET 6 可以优化上一节中展示的常见模式。

有了 ConfigurationManager,当 IConfigurationSource 被添加时(例如当你调用 AddJsonFile() 时),提供者被立即加载,配置被更新。这可以避免在部分构建的情况下不得不多次加载配置源。

由于 IConfigurationBuilder 接口将源作为 IList 公开,因此实现这一点比听起来要难一些:

  1. public interface IConfigurationBuilder 
  2.     IList<IConfigurationSource> Sources { get; } 
  3.     // .. other members 

从 ConfigurationManager 的角度来看,这个问题是 IList<> 暴露了 Add() 和 Remove() 函数。如果使用一个简单的 List<>,消费者可以在 ConfigurationManager 不知道的情况下添加和删除配置提供者。

为了解决这个问题,ConfigurationManager 使用一个自定义的 IList<> 实现。这包含对 ConfigurationManager 实例的引用,这样任何变化都可以反映在配置中:

  1. private class ConfigurationSources : IList<IConfigurationSource> 
  2.     private readonly List<IConfigurationSource> _sources = new(); 
  3.     private readonly ConfigurationManager _config; 
  4.  
  5.     public ConfigurationSources(ConfigurationManager config) 
  6.     { 
  7.         _config = config; 
  8.     } 
  9.  
  10.     public void Add(IConfigurationSource source) 
  11.     { 
  12.         _sources.Add(source); 
  13.         _config.AddSource(source); // add the source to the ConfigurationManager 
  14.     } 
  15.  
  16.     public bool Remove(IConfigurationSource source) 
  17.     { 
  18.         var removed = _sources.Remove(source); 
  19.         _config.ReloadSources(); // reset sources in the ConfigurationManager 
  20.         return removed; 
  21.     } 
  22.  
  23.     // ... additional implementation 

通过使用一个自定义的 IList<> 实现,ConfigurationManager 确保每当有新的源被添加时就调用 AddSource()。这就是 ConfigurationManager 的优势所在:调用 AddSource() 可以立即加载源:

  1. ublic class ConfigurationManager 
  2.  
  3.     private void AddSource(IConfigurationSource source) 
  4.     { 
  5.         lock (_providerLock) 
  6.         { 
  7.             IConfigurationProvider provider = source.Build(this); 
  8.             _providers.Add(provider); 
  9.  
  10.             provider.Load(); 
  11.             _changeTokenRegistrations.Add(ChangeToken.OnChange(() => provider.GetReloadToken(), () => RaiseChanged())); 
  12.         } 
  13.  
  14.         RaiseChanged(); 
  15.     } 

这个方法立即在 IConfigurationSource 上调用 Build 来创建 IConfigurationProvider,并将其添加到提供者列表中。

接下来,该方法调用 IConfigurationProvider.Load()。这将把数据加载到提供者中,(例如从环境变量、JSON 文件或 Azure Key Vault),这是“昂贵”的步骤,而这一切就是为了加载数据 在“正常”情况下,你只需向 IConfigurationBuilder 添加源,并可能需要多次构建它,这就给出了“最佳”方法——源被加载一次,且只有一次。

ConfigurationManager 中 Build() 的实现现在什么都没做,只是返回它自己:

  1. IConfigurationRoot IConfigurationBuilder.Build() => this; 

当然,软件开发是所有关于权衡的问题。如果你只添加源,那么在添加源的时候递增构建源就很有效。然而,如果你调用任何其他 IList<> 函数,如 Clear()、Remove() 或索引器,ConfigurationManager 就必须调用 ReloadSources():

  1. private void ReloadSources() 
  2.     lock (_providerLock) 
  3.     { 
  4.         DisposeRegistrationsAndProvidersUnsynchronized(); 
  5.  
  6.         _changeTokenRegistrations.Clear(); 
  7.         _providers.Clear(); 
  8.  
  9.         foreach (var source in _sources) 
  10.         { 
  11.             _providers.Add(source.Build(this)); 
  12.         } 
  13.  
  14.         foreach (var p in _providers) 
  15.         { 
  16.             p.Load(); 
  17.             _changeTokenRegistrations.Add(ChangeToken.OnChange(() => p.GetReloadToken(), () => RaiseChanged())); 
  18.         } 
  19.     } 
  20.  
  21.     RaiseChanged(); 

正如你所看到的,如果任何一个源改变了,ConfigurationManager 必须删除所有的东西并重新开始,迭代每个源,重新加载它们。如果你要对配置源进行大量的操作,这很快就会变得很昂贵,而且会完全否定 ConfigurationManager 的原始优势。

当然,删除源是非常罕见的,所以 ConfigurationManager 是为最常见的情况而优化的。谁能猜到呢?

下表给出了使用 ConfigurationBuilder 和 ConfigurationManager 的各种操作的相对成本的最终总结:

5.是否需关心 ConfigurationManager

那么读了这么多,你是否应该关心你是使用 ConfigurationManager 还是 ConfigurationBuilder?

也许不应该。

在 .NET 6 中引入的新的 WebApplicationBuilder 使用 ConfigurationManager,它优化了我上面描述的使用情况,即你需要“部分构建”你的配置。

然而,ASP.NET Core 早期版本中引入的 WebHostBuilder 或 HostBuilder 在 .NET 6 中仍然非常受支持,它们继续在幕后使用 ConfigurationBuilder 和 ConfigurationRoot 类型。

我认为唯一需要注意的情况是,如果你在某个地方依赖 IConfigurationBuilder 或 IConfigurationRoot 作为具体类型的 ConfigurationBuilder 或 ConfigurationRoot。这在我看来是非常不太可能发生的,如果你依赖这一点,我很想知道原因。

但除了这个小众的例外,“老”类型不会消失,所以没有必要担心。如果你需要进行“部分构建”,并且你使用了新的 WebApplicationBuilder,那么你的应用程序将会有更高的性能,这一点你应该感到高兴。

6.总结

在这篇文章中,我描述了在 .NET 6 中引入的新的 ConfigurationManager 类型,并在最小(Minimal) API 示例中被新的 WebApplicationBuilder 所使用。引入 ConfigurationManager 是为了优化一种常见的情况,即你需要“部分构建”配置。这通常是因为配置提供者本身需要一些配置,例如,从 Azure Key Vault 加载 secrects,需要配置表明要使用哪个 Vault 库。

ConfigurationManager 优化了这种情况:它在添加源时立即加载,而不是等到你调用 Build()。这就避免了在“部分构建”情况下“重建”配置的需要,其代价是其他不常见操作(如删除一个源)可能变得更昂贵的。

 

责任编辑:武晓燕 来源: 精致码农
相关推荐

2013-09-22 11:03:20

SocketSocket编程

2019-10-12 10:50:00

JavaScript编程语言代码

2015-08-20 13:43:17

NFV网络功能虚拟化

2021-06-07 08:18:12

云计算云端阿里云

2009-09-08 16:30:18

网银木马

2010-05-17 09:13:35

2014-03-12 11:11:39

Storage vMo虚拟机

2023-06-07 13:43:49

云计算

2010-05-26 19:12:41

SVN冲突

2009-12-03 09:19:41

Linux系统奥秘

2018-03-01 09:33:05

软件定义存储

2009-06-01 09:04:44

Google WaveWeb

2009-09-15 15:34:33

Google Fast

2016-04-06 09:27:10

runtime解密学习

2023-11-02 09:55:40

2016-11-16 09:06:59

2016-11-10 12:49:00

2024-02-14 09:00:00

机器学习索引ChatGPT

2009-04-04 09:44:09

微软IE8浏览器

2021-07-28 21:49:01

JVM对象内存
点赞
收藏

51CTO技术栈公众号