.NET Core中配置的工作方式

让我们暂且不谈DDD和反射。 我建议谈论简单的应用程序设置的组织。


在我和我的同事决定切换到.NET Core之后,出现了在新环境中如何组织配置文件,如何执行转换等问题。 在许多示例中可以找到以下代码,并且许多示例已成功使用它。


public IConfiguration Configuration { get; set; } public IHostingEnvironment Environment { get; set; } public Startup(IConfiguration configuration, IHostingEnvironment environment) {   Environment = environment;   Configuration = new ConfigurationBuilder() .AddJsonFile("appsettings.json") .AddJsonFile($"appsettings.{Environment.EnvironmentName}.json") .Build(); } 

但是,让我们看看配置是如何工作的,在哪种情况下使用此方法,以及在哪种情况下信任.NET Core的开发人员。 我要猫。


和以前一样


像任何故事一样,本文也有一个开始。 切换到ASP.NET Core后的第一个问题是配置文件的转换。


回想一下使用web.config之前的情况


配置由几个文件组成。 主文件是web.config ,并且已经根据组件的配置对其进行了转换( web.Development.config等)。 同时,积极使用xml属性来搜索和转换xml文档的各个部分。


但是,正如我们在ASP.NET Core中所知道的那样, web.config文件已由 appsettings.json取代,并且不再具有通常的转换机制。


谷歌告诉我们什么?

在Google上“转换为ASP.NET Core”的搜索结果产生了以下代码:


 public IConfiguration Configuration { get; set; } public IHostingEnvironment Environment { get; set; } public Startup(IConfiguration configuration, IHostingEnvironment environment) {  Environment = environment;  Configuration = new ConfigurationBuilder() .AddJsonFile("appsettings.json") .AddJsonFile($"appsettings.{Environment.EnvironmentName}.json") .Build(); } 

Startup类的构造函数中,我们使用ConfigurationBuilder创建一个配置对象。 在这种情况下,我们明确指出我们要使用的配置源。


这样的:


 public IConfiguration Configuration { get; set; } public IHostingEnvironment Environment { get; set; } public Startup(IConfiguration configuration, IHostingEnvironment environment) {  Environment = environment;  Configuration = new ConfigurationBuilder() .AddJsonFile($"appsettings.{Environment.EnvironmentName}.json") .Build(); } 

根据环境变量,选择一个或另一个配置源。


这些答案通常可以在SO和其他较不受欢迎的资源上找到。 但是感觉并没有离开。 我们错了。 如果要在配置中使用环境变量或命令行参数怎么办? 为什么需要在每个项目中编写此代码?


为了寻找真相,我不得不深入研究文档和源代码。 我想分享在本文中获得的知识。


让我们看看配置如何在.NET Core中工作。


构型


.NET Core中的配置由IConfiguration接口对象表示


 public interface IConfiguration {  string this[string key] { get; set; }  IConfigurationSection GetSection(string key);  IEnumerable<IConfigurationSection> GetChildren();  IChangeToken GetReloadToken(); } 

  • [string key]索引器,它允许通过密钥获取配置参数的值
  • GetSection(字符串键)返回与该键对应的配置节
  • GetChildren()返回当前配置节的子节集
  • GetReloadToken()返回IChangeToken的实例,该实例可用于在配置更改时接收通知

配置是键值对的集合。 从配置源(文件,环境变量)读取时,层次结构数据将简化为平面结构。 例如, json是以下形式对象


 { "Settings": { "Key": "I am options" } } 

将缩小为平面视图:


 Settings:Key = I am options 

在这里,键是“设置:键” ,值是“ 我是选项”
使用配置提供程序来填充配置。


配置提供者


接口对象负责从配置源读取数据。
IConfigurationProvider


 public interface IConfigurationProvider {  bool TryGet(string key, out string value);  void Set(string key, string value);  IChangeToken GetReloadToken();  void Load();  IEnumerable<string> GetChildKeys(IEnumerable<string> earlierKeys, string parentPath); } 

  • TryGet(字符串键,输出字符串值)允许通过键获取配置参数的值
  • Set(字符串键,字符串值)用于设置配置参数的值
  • GetReloadToken()返回IChangeToken的实例,当配置源更改时,该实例可用于接收通知
  • Load()方法负责读取配置源
  • GetChildKeys(IEnumerable <string> earlyKeys,字符串parentPath)允许您获取此配置提供程序提供的所有键的列表。

包装盒中提供了以下提供程序:


  • 杰森
  • 伊尼
  • Xml
  • 环境变量
  • 记忆
  • 蔚蓝
  • 自定义配置提供程序

接受以下使用配置提供程序的约定。


  1. 配置源按指定顺序读取。
  2. 如果不同的配置源中存在相同的键(比较不区分大小写),则使用最后添加的值。

如果我们使用CreateDefaultBuilder创建Web服务器的实例,则默认情况下将连接以下配置提供程序:



  • 通过此提供程序的ChainedConfigurationProvider ,您可以获取其他配置提供程序添加的值和配置密钥
  • JsonConfigurationProvider使用json文件作为配置源。 如您所见,此类型的三个提供程序已添加到提供程序列表中。 第一个使用appsettings.json作为源,第二个使用appsettings。{Environment} .json 。 第三个从secrets.json读取数据。 如果您在“ 发布”配置中构建应用程序,则不会连接第三个提供程序,因为不建议在生产环境中使用机密
  • EnvironmentVariablesConfigurationProvider从环境变量中检索配置参数
  • CommandLineConfigurationProvider允许将命令行参数添加到配置中

由于配置存储为字典,因此有必要确保键的唯一性。 默认情况下,这是这样的。


如果CommandLineConfigurationProvider提供程序的元素具有键密钥,而JsonConfigurationProvider提供程序的元素具有键密钥,则JsonConfigurationProvider中的元素将被CommandLineConfigurationProvider中的元素替换,因为它被最后注册并具有更高的优先级。


回顾本文开头的示例
 public IConfiguration Configuration { get; set; } public IHostingEnvironment Environment { get; set; } public Startup(IConfiguration configuration, IHostingEnvironment environment) {  Environment = environment;  Configuration = new ConfigurationBuilder() .AddJsonFile("appsettings.json") .AddJsonFile($"appsettings.{Environment.EnvironmentName}.json") .Build(); } 

我们不需要自己创建IConfiguration即可转换配置文件,因为默认情况下已启用此功能。 当我们要限制配置源的数量时,此方法是必需的。


自定义配置提供程序


为了编写您的配置提供程序,您需要实现IConfigurationProviderIConfigurationSource接口IConfigurationSource是我们在本文中尚未考虑新接口。


 public interface IConfigurationSource { IConfigurationProvider Build(IConfigurationBuilder builder); } 

该接口由单个Build方法组成,该方法以IConfigurationBuilder作为参数并返回IConfigurationProvider的新实例。


为了实现我们的配置提供程序,我们可以使用抽象类ConfigurationProviderFileConfigurationProvider 。 在这些类中,已经实现了TryGetSetGetReloadTokenGetChildKeys方法的逻辑,并且仅用于实现Load方法。


让我们来看一个例子。 有必要实现从yaml文件中读取配置,并且还可以在不重新启动应用程序的情况下更改配置。


创建YamlConfigurationProvider类,并使它成为FileConfigurationProvider的继承者。


 public class YamlConfigurationProvider : FileConfigurationProvider { private readonly string _filePath; public YamlConfigurationProvider(FileConfigurationSource source) : base(source) { } public override void Load(Stream stream) { throw new NotImplementedException(); } } 

在上面的代码片段中,您可以注意到FileConfigurationProvider类的某些功能。 构造函数接受FileConfigurationSource的实例,该实例包含IFileProviderIFileProvider用于读取文件和订阅文件更改事件。 您还可以注意到, Load方法接受一个Stream,在其中打开了配置文件以供读取。 这是FileConfigurationProvider类的方法,并且不在IConfigurationProvider接口中。


添加一个简单的实现,使我们能够读取yaml文件。 要读取文件,我将使用YamlDotNet包。


YamlConfigurationProvider实现
  public class YamlConfigurationProvider : FileConfigurationProvider { private readonly string _filePath; public YamlConfigurationProvider(FileConfigurationSource source) : base(source) { } public override void Load(Stream stream) { if (stream.CanSeek) { stream.Seek(0L, SeekOrigin.Begin); using (StreamReader streamReader = new StreamReader(stream)) { var fileContent = streamReader.ReadToEnd(); var yamlObject = new DeserializerBuilder() .Build() .Deserialize(new StringReader(fileContent)) as IDictionary<object, object>; Data = new Dictionary<string, string>(); foreach (var pair in yamlObject) { FillData(String.Empty, pair); } } } } private void FillData(string prefix, KeyValuePair<object, object> pair) { var key = String.IsNullOrEmpty(prefix) ? pair.Key.ToString() : $"{prefix}:{pair.Key}"; switch (pair.Value) { case string value: Data.Add(key, value); break; case IDictionary<object, object> section: { foreach (var sectionPair in section) FillData(pair.Key.ToString(), sectionPair); break; } } } } 

要创建我们的配置提供程序的实例,您必须实现FileConfigurationSource


YamlConfigurationSource实现
 public class YamlConfigurationSource : FileConfigurationSource { public YamlConfigurationSource(string fileName) { Path = fileName; ReloadOnChange = true; } public override IConfigurationProvider Build(IConfigurationBuilder builder) { this.EnsureDefaults(builder); return new YamlConfigurationProvider(this); } } 

重要的是,在这里要初始化基类的属性,必须调用this.EnsureDefaults(builder)方法。


要在应用程序中注册自定义配置提供程序,需要将提供程序实例添加到IConfigurationBuilder 。 您可以从IConfigurationBuilder调用Add方法,但是我将立即在扩展方法中放入YamlConfigurationProvider初始化逻辑


实现YamlConfigurationExtensions
 public static class YamlConfigurationExtensions { public static IConfigurationBuilder AddYaml( this IConfigurationBuilder builder, string filePath) { if (builder == null) throw new ArgumentNullException(nameof(builder)); if (string.IsNullOrEmpty(filePath)) throw new ArgumentNullException(nameof(filePath)); return builder .Add(new YamlConfigurationSource(filePath)); } } 

调用AddYaml方法
 public class Program { public static void Main(string[] args) { CreateWebHostBuilder(args).Build().Run(); } public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .ConfigureAppConfiguration((context, builder) => { builder.AddYaml("appsettings.yaml"); }) .UseStartup<Startup>(); } 

变更追踪


在新的api配置中,更改配置源后可以重新读取配置源。 同时,该应用程序不会重新启动。
运作方式:


  • 配置提供程序监视配置源更改
  • 如果发生配置更改,则会创建一个新的IChangeToken。
  • 更改IChangeToken后 ,将调用配置重载

让我们看看如何在FileConfigurationProvider中实现更改跟踪。


 ChangeToken.OnChange( //producer () => Source.FileProvider.Watch(Source.Path), //consumer () => { Thread.Sleep(Source.ReloadDelay); Load(reload: true); }); 

两个参数传递给ChangeToken静态类的OnChange方法。 第一个参数是一个函数,当配置源(在本例中为文件)更改时,该函数将返回新的IChangeToken ,这就是所谓的生产者 。 第二个参数是回调 (或使用者 )函数,当更改配置源时将调用该函数。
了解有关ChangeToken类的更多信息。


并非所有配置提供程序都实现更改跟踪。 FileConfigurationProviderAzureKeyVaultConfigurationProvider的后代可以使用此机制。


结论


在.NET Core中,我们有一个简单,方便的机制来管理应用程序设置。 开箱即用,有许多附加组件,默认情况下使用许多组件。
当然,每个人都可以决定使用哪种方式,但是我的确是因为人们知道他们的工具。


本文仅介绍基础知识。 除了基础知识之外,我们还可以使用IOptions,后配置脚本,设置验证等。 但这是另一个故事。


您可以在Github上的存储库中找到带有本文示例的应用程序项目。
在评论中分享谁使用哪种配置管理方法?
谢谢您的关注。


upd 。:正如AdAbsurdum正确建议的那样,在处理数组时,合并来自两个来源的配置时,元素不会总是被替换。
考虑一个例子。 从appsettings.json读取数组时,我们得到以下平面视图:


 array:0=valueA 

appsettings.Development.json读取时:


 array:0=valueB array:1=value 

结果,配置将是:


 array:0=valueB array:1=value 

所有具有唯一索引的元素( 数组:示例中为1 )将添加到结果数组中。 来自不同配置源但具有相同索引(示例中的数组:0 )的元素将进行合并,并且将使用最后添加的元素。

Source: https://habr.com/ru/post/zh-CN453416/


All Articles