前言

    配置文件中程序运行中,担当着不可或缺的角色;通常情况下,使用 visual studio
进行创建项目过程中,项目配置文件会自动生成在项目根目录下,如 appsettings.json,或者是被大家广泛使用的
appsettings.{env.EnvironmentName}.json;配置文件
作为一个入口,可以让我们在不更新代码的情况,对程序进行干预和调整,那么对其加载过程的全面了解就显得非常必要。

何时加载了默认的配置文件

在 Program.cs 文件中,查看以下代码
public class Program { public static void Main(string[] args) {
CreateWebHostBuilder(args).Build().Run(); } public static IWebHostBuilder
CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>(); }
* WebHost.CreateDefaultBuilder 位于程序集 Microsoft.AspNetCore.dll 内,当程序执行
WebHost.CreateDefaultBuilder(args) 的时候,在 CreateDefaultBuilder 方法内部加载了默认的配置文件
代码如下 public static IWebHostBuilder CreateDefaultBuilder(string[] args) { var
builder = new WebHostBuilder(); if
(string.IsNullOrEmpty(builder.GetSetting(WebHostDefaults.ContentRootKey))) {
builder.UseContentRoot(Directory.GetCurrentDirectory()); } if (args != null) {
builder.UseConfiguration(new
ConfigurationBuilder().AddCommandLine(args).Build()); }
builder.UseKestrel((builderContext, options) => {
options.Configure(builderContext.Configuration.GetSection("Kestrel")); })
.ConfigureAppConfiguration((hostingContext, config) => { var env =
hostingContext.HostingEnvironment; config.AddJsonFile("appsettings.json",
optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true,
reloadOnChange: true); if (env.IsDevelopment()) { var appAssembly =
Assembly.Load(new AssemblyName(env.ApplicationName)); if (appAssembly != null)
{ config.AddUserSecrets(appAssembly, optional: true); } }
config.AddEnvironmentVariables(); if (args != null) {
config.AddCommandLine(args); } }) .ConfigureLogging((hostingContext, logging)
=> {
logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
logging.AddConsole(); logging.AddDebug(); logging.AddEventSourceLogger(); })
.ConfigureServices((hostingContext, services) => { // Fallback
services.PostConfigure<HostFilteringOptions>(options => { if
(options.AllowedHosts == null || options.AllowedHosts.Count == 0) { //
"AllowedHosts": "localhost;127.0.0.1;[::1]" var hosts =
hostingContext.Configuration["AllowedHosts"]?.Split(new[] { ';' },
StringSplitOptions.RemoveEmptyEntries); // Fall back to "*" to disable.
options.AllowedHosts = (hosts?.Length > 0 ? hosts : new[] { "*" }); } }); //
Change notification
services.AddSingleton<IOptionsChangeTokenSource<HostFilteringOptions>>( new
ConfigurationChangeTokenSource<HostFilteringOptions>(hostingContext.Configuration));
services.AddTransient<IStartupFilter, HostFilteringStartupFilter>(); })
.UseIIS() .UseIISIntegration() .UseDefaultServiceProvider((context, options) =>
{ options.ValidateScopes = context.HostingEnvironment.IsDevelopment(); });
return builder; }
* 可以看到,CreateDefaultBuilder 内部还是使用了 IConfigurationBuilder 的实现,且写死了默认配置文件的名字
public static IWebHostBuilder CreateDefaultBuilder(string[] args) { var builder
= new WebHostBuilder(); if
(string.IsNullOrEmpty(builder.GetSetting(WebHostDefaults.ContentRootKey))) {
builder.UseContentRoot(Directory.GetCurrentDirectory()); } if (args != null) {
builder.UseConfiguration(new
ConfigurationBuilder().AddCommandLine(args).Build()); }
builder.UseKestrel((builderContext, options) => {
options.Configure(builderContext.Configuration.GetSection("Kestrel")); })
.ConfigureAppConfiguration((hostingContext, config) => { var env =
hostingContext.HostingEnvironment; config.AddJsonFile("appsettings.json",
optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true,
reloadOnChange: true); if (env.IsDevelopment()) { var appAssembly =
Assembly.Load(new AssemblyName(env.ApplicationName)); if (appAssembly != null)
{ config.AddUserSecrets(appAssembly, optional: true); } }
config.AddEnvironmentVariables(); if (args != null) {
config.AddCommandLine(args); } }) .ConfigureLogging((hostingContext, logging)
=> {
logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
logging.AddConsole(); logging.AddDebug(); logging.AddEventSourceLogger(); })
.ConfigureServices((hostingContext, services) => { // Fallback
services.PostConfigure<HostFilteringOptions>(options => { if
(options.AllowedHosts == null || options.AllowedHosts.Count == 0) { //
"AllowedHosts": "localhost;127.0.0.1;[::1]" var hosts =
hostingContext.Configuration["AllowedHosts"]?.Split(new[] { ';' },
StringSplitOptions.RemoveEmptyEntries); // Fall back to "*" to disable.
options.AllowedHosts = (hosts?.Length > 0 ? hosts : new[] { "*" }); } }); //
Change notification
services.AddSingleton<IOptionsChangeTokenSource<HostFilteringOptions>>( new
ConfigurationChangeTokenSource<HostFilteringOptions>(hostingContext.Configuration));
services.AddTransient<IStartupFilter, HostFilteringStartupFilter>(); })
.UseIIS() .UseIISIntegration() .UseDefaultServiceProvider((context, options) =>
{ options.ValidateScopes = context.HostingEnvironment.IsDevelopment(); });
return builder; }
* 由于以上代码,我们可以在应用程序根目录下使用 appsettings.json 和
appsettings.{env.EnvironmentName}.json 这种形式的默认配置文件名称
并且,由于 Main 方法默认对配置文件进行了 Build 方法的调用操作 public static void Main(string[] args)
{ CreateWebHostBuilder(args).Build().Run(); }
* 我们可以在 Startup.cs 中使用注入的方式获得默认的配置文件对象 IConfigurationRoot/IConfiguration,代码片段
public class Startup { public Startup(IConfiguration configuration) {
Configuration = configuration; }
* 这是为什么呢,因为在 执行 Build 方法的时候,方法内部已经将默认配置文件对象加入了 ServiceCollection 中,代码片段 var
services = new ServiceCollection(); services.AddSingleton(_options);
services.AddSingleton<IHostingEnvironment>(_hostingEnvironment);
services.AddSingleton<Extensions.Hosting.IHostingEnvironment>(_hostingEnvironment);
services.AddSingleton(_context); var builder = new ConfigurationBuilder()
.SetBasePath(_hostingEnvironment.ContentRootPath) .AddConfiguration(_config);
_configureAppConfigurationBuilder?.Invoke(_context, builder); var configuration
= builder.Build(); services.AddSingleton<IConfiguration>(configuration);
_context.Configuration = configuration;
以上这段代码非常熟悉,因为在 Startup.cs 文件中,我们也许会使用过 ServiceCollection
对象将业务系统的自定义对象加入服务上下文中,以方便后续接口注入使用。

AddJsonFile 方法的使用

    通常情况下,我们都会使用默认的配置文件进行开发,或者使用 appsettings.{env.EnvironmentName}.json
的文件名称方式来区分 开发/测试/产品 环境,根据环境变量加载不同的配置文件;可是这样一来带来了另外一个管理上的问题,产品环境的配置参数和开发环境

是不同的,如果使用环境变量的方式控制配置文件的加载,则可能导致密码泄露等风险;诚然,可以手工在产品环境创建此文件,但是这样一来,发布流程将会变得非常繁琐,稍有错漏文件便会被覆盖。

我们推荐使用 AddJsonFile 加载产品环境配置,代码如下
public Startup(IConfiguration configuration, IHostingEnvironment env) {
Configuration = AddCustomizedJsonFile(env).Build(); } public
ConfigurationBuilder AddCustomizedJsonFile(IHostingEnvironment env) { var build
= new ConfigurationBuilder();
build.SetBasePath(env.ContentRootPath).AddJsonFile("appsettings.json", true,
true); if (env.IsProduction()) {
build.AddJsonFile(Path.Combine("/data/sites/config", "appsettings.json"), true,
true); } return build; }
*     通过 AddCustomizedJsonFile 方法去创建一个 ConfigurationBuilder 对象,并覆盖系统默认的
ConfigurationBuilder 对象,在方法内部,默认加载开发环境的配置文件,在产品模式下,额外加载目录
/data/sites/config/appsettings.json 文件,
不同担心配置文件冲突问题,相同键值的内容将由后加入的配置文件所覆盖。
配置文件的变动

* 在调用 AddJsonFile 时,我们看到该方法共有 5 个重载的方法
其中一个方法包含了 4 个参数,代码如下 public static IConfigurationBuilder AddJsonFile(this
IConfigurationBuilder builder, IFileProvider provider, string path, bool
optional, bool reloadOnChange) { if (builder == null) { throw new
ArgumentNullException(nameof(builder)); } if (string.IsNullOrEmpty(path)) {
throw new ArgumentException(Resources.Error_InvalidFilePath, nameof(path)); }
return builder.AddJsonFile(s => { s.FileProvider = provider; s.Path = path;
s.Optional = optional; s.ReloadOnChange = reloadOnChange;
s.ResolveFileProvider(); }); }
*     在此方法中,有一个参数 bool
reloadOnChange,从参数描述可知,该值指示在文件变动的时候是否重新加载,默认值为:false;一般在手动加载配置文件,即调用
AddJsonFile 方法时,建议将该参数值设置为 true。
那么 .netcore 是如果通过该参数 reloadOnChange 是来监控文件变动,以及何时进行重新加载的操作呢,看下面代码 public
IConfigurationRoot Build() { var providers = new
List<IConfigurationProvider>(); foreach (var source in Sources) { var provider
= source.Build(this); providers.Add(provider); } return new
ConfigurationRoot(providers); }
* 在我们执行 .Build 方法的时候,方法内部最后一行代码给我们利用 AddJsonFile 方法的参数创建并返回了一个
ConfigurationRoot 对象
在 ConfigurationRoot 的构造方法中 public
ConfigurationRoot(IList<IConfigurationProvider> providers) { if (providers ==
null) { throw new ArgumentNullException(nameof(providers)); } _providers =
providers; foreach (var p in providers) { p.Load(); ChangeToken.OnChange(() =>
p.GetReloadToken(), () => RaiseChanged()); } }
* 我们看到,方法内部一次读取了通过 AddJsonFile 方法加入的配置文件,并为每个配置文件单独分配了一个监听器
ChangeToken,并绑定当前文件读取对象 IConfigurationProvider.GetReloadToken 方法到监听器中
当文件产生变动的时候,监听器会收到一个通知,同时,对该文件执行原子操作 private void RaiseChanged() { var
previousToken = Interlocked.Exchange(ref _changeToken, new
ConfigurationReloadToken()); previousToken.OnReload(); }
* 由于 AddJsonFile 方法内部使用了 JsonConfigurationSource ,而 Build 的重载方法构造了一个
JsonConfigurationProvider 读取对象,查看代码 public override IConfigurationProvider
Build(IConfigurationBuilder builder) { EnsureDefaults(builder); return new
JsonConfigurationProvider(this); }
* 在 JsonConfigurationProvider 继承自 FileConfigurationProvider 类,该类位于程序集
Microsoft.Extensions.Configuration.Json.dll 内
在 FileConfigurationProvider 的构造方法中实现了监听器重新加载配置文件的过程 public
FileConfigurationProvider(FileConfigurationSource source) { if (source == null)
{ throw new ArgumentNullException(nameof(source)); } Source = source; if
(Source.ReloadOnChange && Source.FileProvider != null) { ChangeToken.OnChange(
() => Source.FileProvider.Watch(Source.Path), () => {
Thread.Sleep(Source.ReloadDelay); Load(reload: true); }); } }
值得注意的是,该监听器不是在得到文件变动通知后第一时间去重新加载配置文件,方法内部可以看到,这里有一个
Thread.Sleep(Source.ReloadDelay),而 ReloadDelay 的默认值为:250ms,该属性的描述为

* 获取或者设置重新加载将等待的毫秒数, 然后调用 "Load" 方法。 这有助于避免在完全写入文件之前触发重新加载。默认值为250
* 让人欣慰的是,我们可以自定义该值,如果业务对文件变动需求不是特别迫切,您可以将该值设置为一个很大的时间,通常情况下,我们不建议那么做
结语

    以上就是 asp.netcore
中配置文件加载的内部执行过程,从中我们认识到,默认配置文件是如何加载,并将默认配置文件如何注入到系统中的,还学习到了如果在不同的环境下,选择加载自定义配置文件的过程;但配置文件变动的时候,系统内部又是如何去把配置文件重新加载到内存中去的。