微信号:iDotNet

介绍:专注分享 .NET 相关技术文章、教程和工具.有时也会涉及到IT职场相关的一些东西,或者来点幽默趣文.

ASP.NET Core 深入了解配置文件加载过程

2018-11-23 09:05 DotNet

(给DotNet加星标,提升.Net技能


转自:Ron.liang

cnblogs.com/viter/p/9967936.html


前言


配置文件中程序运行中,担当着不可或缺的角色;通常情况下,使用 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 中配置文件加载的内部执行过程,从中我们认识到,默认配置文件是如何加载,并将默认配置文件如何注入到系统中的,还学习到了如果在不同的环境下,选择加载自定义配置文件的过程;但配置文件变动的时候,系统内部又是如何去把配置文件重新加载到内存中去的。


推荐阅读

(点击标题可跳转阅读)

.NET Core未来发展趋势的浅层判断

.NET Core 用RPC方式进行高效的HTTP服务访问

深入浅出 ASP.NET Core 基于JWT的认证和授权


看完本文有收获?请转发分享给更多人

关注「DotNet」加星标,提升.Net技能 

 
DotNet 更多文章 转行程序员?你可能忽略了一件事! .NET Core未来发展趋势的浅层判断 .NET Core 用RPC方式进行高效的HTTP服务访问 深入浅出 ASP.NET Core 基于JWT的认证和授权 C# 爬虫爬取商品信息
猜您喜欢 【第206期】HTTPS 升级指南 真正的骨传导黑科技,动动手指就可以接电话! PHP多种序列化对比 别逃避那些不可逃避的现实 上海 | CODING 技术小馆: 工具、代码、开发者生产力