让我们暂且不谈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)允许您获取此配置提供程序提供的所有键的列表。
包装盒中提供了以下提供程序:
接受以下使用配置提供程序的约定。
- 配置源按指定顺序读取。
- 如果不同的配置源中存在相同的键(比较不区分大小写),则使用最后添加的值。
如果我们使用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即可转换配置文件,因为默认情况下已启用此功能。 当我们要限制配置源的数量时,此方法是必需的。
自定义配置提供程序
为了编写您的配置提供程序,您需要实现IConfigurationProvider和IConfigurationSource接口 。 IConfigurationSource是我们在本文中尚未考虑的新接口。
public interface IConfigurationSource { IConfigurationProvider Build(IConfigurationBuilder builder); }
该接口由单个Build方法组成,该方法以IConfigurationBuilder作为参数并返回IConfigurationProvider的新实例。
为了实现我们的配置提供程序,我们可以使用抽象类ConfigurationProvider和FileConfigurationProvider 。 在这些类中,已经实现了TryGet , Set , GetReloadToken , GetChildKeys方法的逻辑,并且仅用于实现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的实例,该实例包含IFileProvider 。 IFileProvider用于读取文件和订阅文件更改事件。 您还可以注意到, 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(
两个参数传递给ChangeToken静态类的OnChange方法。 第一个参数是一个函数,当配置源(在本例中为文件)更改时,该函数将返回新的IChangeToken ,这就是所谓的生产者 。 第二个参数是回调 (或使用者 )函数,当更改配置源时将调用该函数。
了解有关ChangeToken类的更多信息。
并非所有配置提供程序都实现更改跟踪。 FileConfigurationProvider和AzureKeyVaultConfigurationProvider的后代可以使用此机制。
结论
在.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 )的元素将进行合并,并且将使用最后添加的元素。