ASP.NET Core中的配置Configuration的使用及其源码解析
本章将和大家分享ASP.NET Core中的配置Configuration的使用及其源码解析。
1、使用 IConfiguration 读取配置文件内容
Demo的目录结构如下所示:
本Demo的Web项目为ASP.NET Core Web 应用程序(目标框架为.NET Core 3.1) MVC项目。
首先添加配置文件,内容如下所示:(注意:配置文件的编码必须调整为UTF-8)
appsettings.Development.json 内容如下:
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } } }
appsettings.json 内容如下:
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*" }
hostsettings.json 内容如下:
{ "urls": "http://*:8001" }
otherconfig.json 内容如下:
{ "OtherConfig": { "accountkey": "accountkey_测试", "accountsecret": "accountsecret_测试" } }
otherconfig.Production.json 内容如下:
{ "OtherConfig": { "accountkey": "accountkey_正式", "accountsecret": "accountsecret_正式" } }
siteconfig.json 内容如下:
{ "ConnectionStrings": "server=127.0.0.1;port=3306;database=dotnetcore;uid=root;pwd=123456;CharSet=utf8;", "SiteConfig": { "Name": "测试站点名称", "Admin": "http://admin.demo.test", "Api": "http://api.demo.test", "Image": "http://image.demo.test", "My": "http://my.demo.test", "Upload": "http://upload.demo.test", "Www": "http://www.demo.test", "Domain": { "mc_1633_com": "http://192.168.18.209:8000/mc" } } }
siteconfig.Production.json 内容如下:
{ "ConnectionStrings": "server=127.0.0.1;port=3306;database=dotnetcore;uid=root;pwd=123456;CharSet=utf8;", "SiteConfig": { "Name": "正式站点名称", "Admin": "http://admin.demo.com", "Api": "http://api.demo.com", "Image": "http://image.demo.com", "My": "http://my.demo.com", "Upload": "http://upload.demo.com", "Www": "http://www.demo.com" } }
接着修改 Program.cs 类,如下所示:
using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace IConfiguration_IOptions_Demo { public class Program { public static void Main(string[] args) { //CreateHostBuilder(args).Build().Run(); var hostBuilder = CreateHostBuilder(args); hostBuilder.Build().Run(); } public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureAppConfiguration((hostingContext, config) => { var env = hostingContext.HostingEnvironment; var basePath = System.IO.Path.Combine(env.ContentRootPath, "ConfigFiles"); config.SetBasePath(basePath: basePath) .AddJsonFile(path: "hostsettings.json", optional: true, reloadOnChange: true) .AddJsonFile(path: $"hostsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true) .AddJsonFile(path: "otherconfig.json", optional: true, reloadOnChange: true) .AddJsonFile(path: $"otherconfig.{env.EnvironmentName}.json", optional: true, reloadOnChange: true) .AddJsonFile(path: "siteconfig.json", optional: true, reloadOnChange: true) .AddJsonFile(path: $"siteconfig.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); config.AddEnvironmentVariables(); }) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); }); } }
最后使用 IConfiguration 读取配置文件内容,如下所示:
using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace IConfiguration_IOptions_Demo.Controllers { public class ConfigurationDemoController : Controller { private readonly IConfiguration _configuration; /// <summary> /// 构造函数 /// </summary> public ConfigurationDemoController(IConfiguration configuration) { _configuration = configuration; } /// <summary> /// IConfiguration的使用 /// </summary> /// <returns></returns> public IActionResult Index() { var sb = new StringBuilder(); sb.AppendLine($"_configuration[\"Logging:LogLevel:Default\"] => {_configuration["Logging:LogLevel:Default"]}"); sb.AppendLine($"_configuration[\"AllowedHosts\"] => {_configuration["AllowedHosts"]}"); sb.AppendLine($"_configuration[\"SiteConfig:Name\"] => {_configuration["SiteConfig:Name"]}"); sb.AppendLine($"_configuration[\"OtherConfig:accountkey\"] => {_configuration["OtherConfig:accountkey"]}"); IConfigurationSection siteConfigSection = _configuration.GetSection("SiteConfig"); sb.AppendLine($"siteConfigSection[\"Name\"] => {siteConfigSection["Name"]}"); sb.AppendLine($"siteConfigSection[\"Domain:mc_1633_com\"] => {siteConfigSection["Domain:mc_1633_com"]}"); IConfigurationSection domainSection = _configuration.GetSection("SiteConfig").GetSection("Domain"); sb.AppendLine($"domainSection[\"mc_1633_com\"] => {domainSection["mc_1633_com"]}"); return Content(sb.ToString()); } } }
访问 /ConfigurationDemo/Index 运行结果如下:
2、IConfiguration 源码解析
首先我们通过调试来看下注入给IConfiguration的到底是什么?
可以看出它是 Microsoft.Extensions.Configuration.ConfigurationRoot 类的实例。
我们接着往下调试看下 _configuration.GetSection("SiteConfig") 返回的是啥?
可以看出它返回的是 Microsoft.Extensions.Configuration.ConfigurationSection 类的实例。
其实 ConfigurationRoot 类实现了 IConfigurationRoot 接口,而 IConfigurationRoot 接口又实现了 IConfiguration 接口,并且 ConfigurationSection 类实现了 IConfigurationSection 接口,而 IConfigurationSection 接口又实现了 IConfiguration 接口。IConfiguration、IConfigurationRoot 和 IConfigurationSection 它们三者之间的关系如下:
讲到这里,大家对配置的获取,应该有了一个基本的认识。 那么下面我们就通过查看源码的方式来理解它的底层实现逻辑。
我们从 Program.cs 类的 Main(string[] args) 主函数(应用程序的入口)开始查找:
using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace IConfiguration_IOptions_Demo { public class Program { public static void Main(string[] args) { //CreateHostBuilder(args).Build().Run(); var hostBuilder = CreateHostBuilder(args); hostBuilder.Build().Run(); } public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureAppConfiguration((hostingContext, config) => { var env = hostingContext.HostingEnvironment; var basePath = System.IO.Path.Combine(env.ContentRootPath, "ConfigFiles"); config.SetBasePath(basePath: basePath) .AddJsonFile(path: "hostsettings.json", optional: true, reloadOnChange: true) .AddJsonFile(path: $"hostsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true) .AddJsonFile(path: "otherconfig.json", optional: true, reloadOnChange: true) .AddJsonFile(path: $"otherconfig.{env.EnvironmentName}.json", optional: true, reloadOnChange: true) .AddJsonFile(path: "siteconfig.json", optional: true, reloadOnChange: true) .AddJsonFile(path: $"siteconfig.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); config.AddEnvironmentVariables(); }) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); }); } }
首先找到 Host 类,源码如下:
// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.IO; using System.Reflection; using System.Runtime.InteropServices; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.EventLog; namespace Microsoft.Extensions.Hosting { /// <summary> /// Provides convenience methods for creating instances of <see cref="IHostBuilder"/> with pre-configured defaults. /// </summary> public static class Host { /// <summary> /// Initializes a new instance of the <see cref="HostBuilder"/> class with pre-configured defaults. /// </summary> /// <remarks> /// The following defaults are applied to the returned <see cref="HostBuilder"/>: /// <list type="bullet"> /// <item><description>set the <see cref="IHostEnvironment.ContentRootPath"/> to the result of <see cref="Directory.GetCurrentDirectory()"/></description></item> /// <item><description>load host <see cref="IConfiguration"/> from "DOTNET_" prefixed environment variables</description></item> /// <item><description>load app <see cref="IConfiguration"/> from 'appsettings.json' and 'appsettings.[<see cref="IHostEnvironment.EnvironmentName"/>].json'</description></item> /// <item><description>load app <see cref="IConfiguration"/> from User Secrets when <see cref="IHostEnvironment.EnvironmentName"/> is 'Development' using the entry assembly</description></item> /// <item><description>load app <see cref="IConfiguration"/> from environment variables</description></item> /// <item><description>configure the <see cref="ILoggerFactory"/> to log to the console, debug, and event source output</description></item> /// <item><description>enables scope validation on the dependency injection container when <see cref="IHostEnvironment.EnvironmentName"/> is 'Development'</description></item> /// </list> /// </remarks> /// <returns>The initialized <see cref="IHostBuilder"/>.</returns> public static IHostBuilder CreateDefaultBuilder() => CreateDefaultBuilder(args: null); /// <summary> /// Initializes a new instance of the <see cref="HostBuilder"/> class with pre-configured defaults. /// </summary> /// <remarks> /// The following defaults are applied to the returned <see cref="HostBuilder"/>: /// <list type="bullet"> /// <item><description>set the <see cref="IHostEnvironment.ContentRootPath"/> to the result of <see cref="Directory.GetCurrentDirectory()"/></description></item> /// <item><description>load host <see cref="IConfiguration"/> from "DOTNET_" prefixed environment variables</description></item> /// <item><description>load host <see cref="IConfiguration"/> from supplied command line args</description></item> /// <item><description>load app <see cref="IConfiguration"/> from 'appsettings.json' and 'appsettings.[<see cref="IHostEnvironment.EnvironmentName"/>].json'</description></item> /// <item><description>load app <see cref="IConfiguration"/> from User Secrets when <see cref="IHostEnvironment.EnvironmentName"/> is 'Development' using the entry assembly</description></item> /// <item><description>load app <see cref="IConfiguration"/> from environment variables</description></item> /// <item><description>load app <see cref="IConfiguration"/> from supplied command line args</description></item> /// <item><description>configure the <see cref="ILoggerFactory"/> to log to the console, debug, and event source output</description></item> /// <item><description>enables scope validation on the dependency injection container when <see cref="IHostEnvironment.EnvironmentName"/> is 'Development'</description></item> /// </list> /// </remarks> /// <param name="args">The command line args.</param> /// <returns>The initialized <see cref="IHostBuilder"/>.</returns> public static IHostBuilder CreateDefaultBuilder(string[] args) { var builder = new HostBuilder(); builder.UseContentRoot(Directory.GetCurrentDirectory()); builder.ConfigureHostConfiguration(config => { config.AddEnvironmentVariables(prefix: "DOTNET_"); if (args != null) { config.AddCommandLine(args); } }); builder.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() && !string.IsNullOrEmpty(env.ApplicationName)) { 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) => { var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); // IMPORTANT: This needs to be added *before* configuration is loaded, this lets // the defaults be overridden by the configuration. if (isWindows) { // Default the EventLogLoggerProvider to warning or above logging.AddFilter<EventLogLoggerProvider>(level => level >= LogLevel.Warning); } logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); logging.AddConsole(); logging.AddDebug(); logging.AddEventSourceLogger(); if (isWindows) { // Add the EventLogLoggerProvider on windows machines logging.AddEventLog(); } }) .UseDefaultServiceProvider((context, options) => { var isDevelopment = context.HostingEnvironment.IsDevelopment(); options.ValidateScopes = isDevelopment; options.ValidateOnBuild = isDevelopment; }); return builder; } } }
从该类可以看出调用Host.CreateDefaultBuilder(args)这个方法最终返回的是HostBuilder类型的对象,且在Main(string[] args)主函数中最终会调用该对象的Build()方法。
此外还可以看出 Host.CreateDefaultBuilder(args) 这个方法的内部默认就已经添加了appsettings.json的这个配置,这也就解释了为什么在Program.cs类中添加配置文件的时候不需要加appsettings.json的原因。
接下来我们就来看一下 builder.ConfigureAppConfiguration(...) 这个方法内部到底做了什么操作,我们找到 HostBuilder 类的源码,如下:
// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using System.IO; using System.Reflection; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Hosting.Internal; namespace Microsoft.Extensions.Hosting { /// <summary> /// A program initialization utility. /// </summary> public class HostBuilder : IHostBuilder { private List<Action<IConfigurationBuilder>> _configureHostConfigActions = new List<Action<IConfigurationBuilder>>(); private List<Action<HostBuilderContext, IConfigurationBuilder>> _configureAppConfigActions = new List<Action<HostBuilderContext, IConfigurationBuilder>>(); private List<Action<HostBuilderContext, IServiceCollection>> _configureServicesActions = new List<Action<HostBuilderContext, IServiceCollection>>(); private List<IConfigureContainerAdapter> _configureContainerActions = new List<IConfigureContainerAdapter>(); private IServiceFactoryAdapter _serviceProviderFactory = new ServiceFactoryAdapter<IServiceCollection>(new DefaultServiceProviderFactory()); private bool _hostBuilt; private IConfiguration _hostConfiguration; private IConfiguration _appConfiguration; private HostBuilderContext _hostBuilderContext; private HostingEnvironment _hostingEnvironment; private IServiceProvider _appServices; /// <summary> /// A central location for sharing state between components during the host building process. /// </summary> public IDictionary<object, object> Properties { get; } = new Dictionary<object, object>(); /// <summary> /// Set up the configuration for the builder itself. This will be used to initialize the <see cref="IHostEnvironment"/> /// for use later in the build process. This can be called multiple times and the results will be additive. /// </summary> /// <param name="configureDelegate">The delegate for configuring the <see cref="IConfigurationBuilder"/> that will be used /// to construct the <see cref="IConfiguration"/> for the host.</param> /// <returns>The same instance of the <see cref="IHostBuilder"/> for chaining.</returns> public IHostBuilder ConfigureHostConfiguration(Action<IConfigurationBuilder> configureDelegate) { _configureHostConfigActions.Add(configureDelegate ?? throw new ArgumentNullException(nameof(configureDelegate))); return this; } /// <summary> /// Sets up the configuration for the remainder of the build process and application. This can be called multiple times and /// the results will be additive. The results will be available at <see cref="HostBuilderContext.Configuration"/> for /// subsequent operations, as well as in <see cref="IHost.Services"/>. /// </summary> /// <param name="configureDelegate">The delegate for configuring the <see cref="IConfigurationBuilder"/> that will be used /// to construct the <see cref="IConfiguration"/> for the host.</param> /// <returns>The same instance of the <see cref="IHostBuilder"/> for chaining.</returns> public IHostBuilder ConfigureAppConfiguration(Action<HostBuilderContext, IConfigurationBuilder> configureDelegate) { _configureAppConfigActions.Add(configureDelegate ?? throw new ArgumentNullException(nameof(configureDelegate))); return this; } /// <summary> /// Adds services to the container. This can be called multiple times and the results will be additive. /// </summary> /// <param name="configureDelegate">The delegate for configuring the <see cref="IConfigurationBuilder"/> that will be used /// to construct the <see cref="IConfiguration"/> for the host.</param> /// <returns>The same instance of the <see cref="IHostBuilder"/> for chaining.</returns> public IHostBuilder ConfigureServices(Action<HostBuilderContext, IServiceCollection> configureDelegate) { _configureServicesActions.Add(configureDelegate ?? throw new ArgumentNullException(nameof(configureDelegate))); return this; } /// <summary> /// Overrides the factory used to create the service provider. /// </summary> /// <typeparam name="TContainerBuilder">The type of the builder to create.</typeparam> /// <param name="factory">A factory used for creating service providers.</param> /// <returns>The same instance of the <see cref="IHostBuilder"/> for chaining.</returns> public IHostBuilder UseServiceProviderFactory<TContainerBuilder>(IServiceProviderFactory<TContainerBuilder> factory) { _serviceProviderFactory = new ServiceFactoryAdapter<TContainerBuilder>(factory ?? throw new ArgumentNullException(nameof(factory))); return this; } /// <summary> /// Overrides the factory used to create the service provider. /// </summary> /// <param name="factory">A factory used for creating service providers.</param> /// <typeparam name="TContainerBuilder">The type of the builder to create.</typeparam> /// <returns>The same instance of the <see cref="IHostBuilder"/> for chaining.</returns> public IHostBuilder UseServiceProviderFactory<TContainerBuilder>(Func<HostBuilderContext, IServiceProviderFactory<TContainerBuilder>> factory) { _serviceProviderFactory = new ServiceFactoryAdapter<TContainerBuilder>(() => _hostBuilderContext, factory ?? throw new ArgumentNullException(nameof(factory))); return this; } /// <summary> /// Enables configuring the instantiated dependency container. This can be called multiple times and /// the results will be additive. /// </summary> /// <typeparam name="TContainerBuilder">The type of the builder to create.</typeparam> /// <param name="configureDelegate">The delegate for configuring the <see cref="IConfigurationBuilder"/> that will be used /// to construct the <see cref="IConfiguration"/> for the host.</param> /// <returns>The same instance of the <see cref="IHostBuilder"/> for chaining.</returns> public IHostBuilder ConfigureContainer<TContainerBuilder>(Action<HostBuilderContext, TContainerBuilder> configureDelegate) { _configureContainerActions.Add(new ConfigureContainerAdapter<TContainerBuilder>(configureDelegate ?? throw new ArgumentNullException(nameof(configureDelegate)))); return this; } /// <summary> /// Run the given actions to initialize the host. This can only be called once. /// </summary> /// <returns>An initialized <see cref="IHost"/></returns> public IHost Build() { if (_hostBuilt) { throw new InvalidOperationException("Build can only be called once."); } _hostBuilt = true; BuildHostConfiguration(); CreateHostingEnvironment(); CreateHostBuilderContext(); BuildAppConfiguration(); CreateServiceProvider(); return _appServices.GetRequiredService<IHost>(); } private void BuildHostConfiguration() { var configBuilder = new ConfigurationBuilder() .AddInMemoryCollection(); // Make sure there's some default storage since there are no default providers foreach (var buildAction in _configureHostConfigActions) { buildAction(configBuilder); } _hostConfiguration = configBuilder.Build(); } private void CreateHostingEnvironment() { _hostingEnvironment = new HostingEnvironment() { ApplicationName = _hostConfiguration[HostDefaults.ApplicationKey], EnvironmentName = _hostConfiguration[HostDefaults.EnvironmentKey] ?? Environments.Production, ContentRootPath = ResolveContentRootPath(_hostConfiguration[HostDefaults.ContentRootKey], AppContext.BaseDirectory), }; if (string.IsNullOrEmpty(_hostingEnvironment.ApplicationName)) { // Note GetEntryAssembly returns null for the net4x console test runner. _hostingEnvironment.ApplicationName = Assembly.GetEntryAssembly()?.GetName().Name; } _hostingEnvironment.ContentRootFileProvider = new PhysicalFileProvider(_hostingEnvironment.ContentRootPath); } private string ResolveContentRootPath(string contentRootPath, string basePath) { if (string.IsNullOrEmpty(contentRootPath)) { return basePath; } if (Path.IsPathRooted(contentRootPath)) { return contentRootPath; } return Path.Combine(Path.GetFullPath(basePath), contentRootPath); } private void CreateHostBuilderContext() { _hostBuilderContext = new HostBuilderContext(Properties) { HostingEnvironment = _hostingEnvironment, Configuration = _hostConfiguration }; } private void BuildAppConfiguration() { var configBuilder = new ConfigurationBuilder() .SetBasePath(_hostingEnvironment.ContentRootPath) .AddConfiguration(_hostConfiguration, shouldDisposeConfiguration: true); foreach (var buildAction in _configureAppConfigActions) { buildAction(_hostBuilderContext, configBuilder); } _appConfiguration = configBuilder.Build(); _hostBuilderContext.Configuration = _appConfiguration; } private void CreateServiceProvider() { var services = new ServiceCollection(); #pragma warning disable CS0618 // Type or member is obsolete services.AddSingleton<IHostingEnvironment>(_hostingEnvironment); #pragma warning restore CS0618 // Type or member is obsolete services.AddSingleton<IHostEnvironment>(_hostingEnvironment); services.AddSingleton(_hostBuilderContext); // register configuration as factory to make it dispose with the service provider services.AddSingleton(_ => _appConfiguration); #pragma warning disable CS0618 // Type or member is obsolete services.AddSingleton<IApplicationLifetime>(s => (IApplicationLifetime)s.GetService<IHostApplicationLifetime>()); #pragma warning restore CS0618 // Type or member is obsolete services.AddSingleton<IHostApplicationLifetime, ApplicationLifetime>(); services.AddSingleton<IHostLifetime, ConsoleLifetime>(); services.AddSingleton<IHost, Internal.Host>(); services.AddOptions(); services.AddLogging(); foreach (var configureServicesAction in _configureServicesActions) { configureServicesAction(_hostBuilderContext, services); } var containerBuilder = _serviceProviderFactory.CreateBuilder(services); foreach (var containerAction in _configureContainerActions) { containerAction.ConfigureContainer(_hostBuilderContext, containerBuilder); } _appServices = _serviceProviderFactory.CreateServiceProvider(containerBuilder); if (_appServices == null) { throw new InvalidOperationException($"The IServiceProviderFactory returned a null IServiceProvider."); } // resolve configuration explicitly once to mark it as resolved within the // service provider, ensuring it will be properly disposed with the provider _ = _appServices.GetService<IConfiguration>(); } } }
仔细阅读后可以发现其实 ConfigureAppConfiguration(...) 这个方法就只是将 Action<HostBuilderContext, IConfigurationBuilder> 类型的委托参数添加到 _configureAppConfigActions 这个集合中。
接着在构建的时候(即调用 Build() 方法时)调用 BuildAppConfiguration() 方法,该方法首先会去创建一个 ConfigurationBuilder 类型的对象 configBuilder ,然后遍历 _configureAppConfigActions 集合,执行该集合中所有的委托(其中调用委托时传递的参数之一就是这个 configBuilder 对象),最后调用 configBuilder.Build() 方法,该方法返回一个 ConfigurationRoot 类型的对象。
最终在调用 CreateServiceProvider() 方法时再将这个 ConfigurationRoot 类型的对象依赖注入给 IConfiguration 。
我们继续找到这个 ConfigurationBuilder 类的源码,如下所示:
// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; namespace Microsoft.Extensions.Configuration { /// <summary> /// Used to build key/value based configuration settings for use in an application. /// </summary> public class ConfigurationBuilder : IConfigurationBuilder { /// <summary> /// Returns the sources used to obtain configuration values. /// </summary> public IList<IConfigurationSource> Sources { get; } = new List<IConfigurationSource>(); /// <summary> /// Gets a key/value collection that can be used to share data between the <see cref="IConfigurationBuilder"/> /// and the registered <see cref="IConfigurationProvider"/>s. /// </summary> public IDictionary<string, object> Properties { get; } = new Dictionary<string, object>(); /// <summary> /// Adds a new configuration source. /// </summary> /// <param name="source">The configuration source to add.</param> /// <returns>The same <see cref="IConfigurationBuilder"/>.</returns> public IConfigurationBuilder Add(IConfigurationSource source) { if (source == null) { throw new ArgumentNullException(nameof(source)); } Sources.Add(source); return this; } /// <summary> /// Builds an <see cref="IConfiguration"/> with keys and values from the set of providers registered in /// <see cref="Sources"/>. /// </summary> /// <returns>An <see cref="IConfigurationRoot"/> with keys and values from the registered providers.</returns> 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); } } }
从ConfigurationBuilder类中可以看出在调用该类 Build() 方法的时候它会去遍历 IList<IConfigurationSource> Sources 这个集合,那么这个集合里面存放的到底是什么数据,它又是什么时候加进去的呢?
其实我们在调用 hostBuilder.ConfigureAppConfiguration(...) 这个方法时传递的委托参数中的 config.AddJsonFile(...) 这个操作就是往 IList<IConfigurationSource> Sources 这个集合里面添加数据。
我们找到 Program.cs 类,将光标移动到 .AddJsonFile(...) 后按 F12 ,这样就可以定位找到 JsonConfigurationExtensions 类,如下所示:
接着我们去找到 Microsoft.Extensions.Configuration.JsonConfigurationExtensions 类的源码,如下所示:
// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.IO; using Microsoft.Extensions.Configuration.Json; using Microsoft.Extensions.FileProviders; namespace Microsoft.Extensions.Configuration { /// <summary> /// Extension methods for adding <see cref="JsonConfigurationProvider"/>. /// </summary> public static class JsonConfigurationExtensions { /// <summary> /// Adds the JSON configuration provider at <paramref name="path"/> to <paramref name="builder"/>. /// </summary> /// <param name="builder">The <see cref="IConfigurationBuilder"/> to add to.</param> /// <param name="path">Path relative to the base path stored in /// <see cref="IConfigurationBuilder.Properties"/> of <paramref name="builder"/>.</param> /// <returns>The <see cref="IConfigurationBuilder"/>.</returns> public static IConfigurationBuilder AddJsonFile(this IConfigurationBuilder builder, string path) { return AddJsonFile(builder, provider: null, path: path, optional: false, reloadOnChange: false); } /// <summary> /// Adds the JSON configuration provider at <paramref name="path"/> to <paramref name="builder"/>. /// </summary> /// <param name="builder">The <see cref="IConfigurationBuilder"/> to add to.</param> /// <param name="path">Path relative to the base path stored in /// <see cref="IConfigurationBuilder.Properties"/> of <paramref name="builder"/>.</param> /// <param name="optional">Whether the file is optional.</param> /// <returns>The <see cref="IConfigurationBuilder"/>.</returns> public static IConfigurationBuilder AddJsonFile(this IConfigurationBuilder builder, string path, bool optional) { return AddJsonFile(builder, provider: null, path: path, optional: optional, reloadOnChange: false); } /// <summary> /// Adds the JSON configuration provider at <paramref name="path"/> to <paramref name="builder"/>. /// </summary> /// <param name="builder">The <see cref="IConfigurationBuilder"/> to add to.</param> /// <param name="path">Path relative to the base path stored in /// <see cref="IConfigurationBuilder.Properties"/> of <paramref name="builder"/>.</param> /// <param name="optional">Whether the file is optional.</param> /// <param name="reloadOnChange">Whether the configuration should be reloaded if the file changes.</param> /// <returns>The <see cref="IConfigurationBuilder"/>.</returns> public static IConfigurationBuilder AddJsonFile(this IConfigurationBuilder builder, string path, bool optional, bool reloadOnChange) { return AddJsonFile(builder, provider: null, path: path, optional: optional, reloadOnChange: reloadOnChange); } /// <summary> /// Adds a JSON configuration source to <paramref name="builder"/>. /// </summary> /// <param name="builder">The <see cref="IConfigurationBuilder"/> to add to.</param> /// <param name="provider">The <see cref="IFileProvider"/> to use to access the file.</param> /// <param name="path">Path relative to the base path stored in /// <see cref="IConfigurationBuilder.Properties"/> of <paramref name="builder"/>.</param> /// <param name="optional">Whether the file is optional.</param> /// <param name="reloadOnChange">Whether the configuration should be reloaded if the file changes.</param> /// <returns>The <see cref="IConfigurationBuilder"/>.</returns> 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(); }); } /// <summary> /// Adds a JSON configuration source to <paramref name="builder"/>. /// </summary> /// <param name="builder">The <see cref="IConfigurationBuilder"/> to add to.</param> /// <param name="configureSource">Configures the source.</param> /// <returns>The <see cref="IConfigurationBuilder"/>.</returns> public static IConfigurationBuilder AddJsonFile(this IConfigurationBuilder builder, Action<JsonConfigurationSource> configureSource) => builder.Add(configureSource); /// <summary> /// Adds a JSON configuration source to <paramref name="builder"/>. /// </summary> /// <param name="builder">The <see cref="IConfigurationBuilder"/> to add to.</param> /// <param name="stream">The <see cref="Stream"/> to read the json configuration data from.</param> /// <returns>The <see cref="IConfigurationBuilder"/>.</returns> public static IConfigurationBuilder AddJsonStream(this IConfigurationBuilder builder, Stream stream) { if (builder == null) { throw new ArgumentNullException(nameof(builder)); } return builder.Add<JsonStreamConfigurationSource>(s => s.Stream = stream); } } }
然后定位找到对应的 AddJsonFile 方法,如下所示:
/// <summary> /// Adds the JSON configuration provider at <paramref name="path"/> to <paramref name="builder"/>. /// </summary> /// <param name="builder">The <see cref="IConfigurationBuilder"/> to add to.</param> /// <param name="path">Path relative to the base path stored in /// <see cref="IConfigurationBuilder.Properties"/> of <paramref name="builder"/>.</param> /// <param name="optional">Whether the file is optional.</param> /// <param name="reloadOnChange">Whether the configuration should be reloaded if the file changes.</param> /// <returns>The <see cref="IConfigurationBuilder"/>.</returns> public static IConfigurationBuilder AddJsonFile(this IConfigurationBuilder builder, string path, bool optional, bool reloadOnChange) { return AddJsonFile(builder, provider: null, path: path, optional: optional, reloadOnChange: reloadOnChange); }
接着按 F12 会找到如下方法:
/// <summary> /// Adds a JSON configuration source to <paramref name="builder"/>. /// </summary> /// <param name="builder">The <see cref="IConfigurationBuilder"/> to add to.</param> /// <param name="provider">The <see cref="IFileProvider"/> to use to access the file.</param> /// <param name="path">Path relative to the base path stored in /// <see cref="IConfigurationBuilder.Properties"/> of <paramref name="builder"/>.</param> /// <param name="optional">Whether the file is optional.</param> /// <param name="reloadOnChange">Whether the configuration should be reloaded if the file changes.</param> /// <returns>The <see cref="IConfigurationBuilder"/>.</returns> 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(); }); }
继续按 F12 找到如下方法:
/// <summary> /// Adds a JSON configuration source to <paramref name="builder"/>. /// </summary> /// <param name="builder">The <see cref="IConfigurationBuilder"/> to add to.</param> /// <param name="configureSource">Configures the source.</param> /// <returns>The <see cref="IConfigurationBuilder"/>.</returns> public static IConfigurationBuilder AddJsonFile(this IConfigurationBuilder builder, Action<JsonConfigurationSource> configureSource) => builder.Add(configureSource);
可以发现它最终是调用 builder.Add(...) 这个扩展方法的,该扩展方法位于 Microsoft.Extensions.Configuration.ConfigurationExtensions 类中。
我们找到 ConfigurationExtensions 类的源码,如下所示:
// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using System.Linq; namespace Microsoft.Extensions.Configuration { /// <summary> /// Extension methods for configuration classes./>. /// </summary> public static class ConfigurationExtensions { /// <summary> /// Adds a new configuration source. /// </summary> /// <param name="builder">The <see cref="IConfigurationBuilder"/> to add to.</param> /// <param name="configureSource">Configures the source secrets.</param> /// <returns>The <see cref="IConfigurationBuilder"/>.</returns> public static IConfigurationBuilder Add<TSource>(this IConfigurationBuilder builder, Action<TSource> configureSource) where TSource : IConfigurationSource, new() { var source = new TSource(); configureSource?.Invoke(source); return builder.Add(source); } /// <summary> /// Shorthand for GetSection("ConnectionStrings")[name]. /// </summary> /// <param name="configuration">The configuration.</param> /// <param name="name">The connection string key.</param> /// <returns>The connection string.</returns> public static string GetConnectionString(this IConfiguration configuration, string name) { return configuration?.GetSection("ConnectionStrings")?[name]; } /// <summary> /// Get the enumeration of key value pairs within the <see cref="IConfiguration" /> /// </summary> /// <param name="configuration">The <see cref="IConfiguration"/> to enumerate.</param> /// <returns>An enumeration of key value pairs.</returns> public static IEnumerable<KeyValuePair<string, string>> AsEnumerable(this IConfiguration configuration) => configuration.AsEnumerable(makePathsRelative: false); /// <summary> /// Get the enumeration of key value pairs within the <see cref="IConfiguration" /> /// </summary> /// <param name="configuration">The <see cref="IConfiguration"/> to enumerate.</param> /// <param name="makePathsRelative">If true, the child keys returned will have the current configuration's Path trimmed from the front.</param> /// <returns>An enumeration of key value pairs.</returns> public static IEnumerable<KeyValuePair<string, string>> AsEnumerable(this IConfiguration configuration, bool makePathsRelative) { var stack = new Stack<IConfiguration>(); stack.Push(configuration); var rootSection = configuration as IConfigurationSection; var prefixLength = (makePathsRelative && rootSection != null) ? rootSection.Path.Length + 1 : 0; while (stack.Count > 0) { var config = stack.Pop(); // Don't include the sections value if we are removing paths, since it will be an empty key if (config is IConfigurationSection section && (!makePathsRelative || config != configuration)) { yield return new KeyValuePair<string, string>(section.Path.Substring(prefixLength), section.Value); } foreach (var child in config.GetChildren()) { stack.Push(child); } } } /// <summary> /// Determines whether the section has a <see cref="IConfigurationSection.Value"/> or has children /// </summary> public static bool Exists(this IConfigurationSection section) { if (section == null) { return false; } return section.Value != null || section.GetChildren().Any(); } } }
从中找到对应的扩展方法:
/// <summary> /// Adds a new configuration source. /// </summary> /// <param name="builder">The <see cref="IConfigurationBuilder"/> to add to.</param> /// <param name="configureSource">Configures the source secrets.</param> /// <returns>The <see cref="IConfigurationBuilder"/>.</returns> public static IConfigurationBuilder Add<TSource>(this IConfigurationBuilder builder, Action<TSource> configureSource) where TSource : IConfigurationSource, new() { var source = new TSource(); configureSource?.Invoke(source); return builder.Add(source); }
由此可见 config.AddJsonFile(...) 这个方法最终会变成调用 ConfigurationBuilder 类中的 Add(...) 方法,传递 JsonConfigurationSource 类型的对象。
下面我们继续往下分析 ConfigurationBuilder 类的源码:
// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; namespace Microsoft.Extensions.Configuration { /// <summary> /// Used to build key/value based configuration settings for use in an application. /// </summary> public class ConfigurationBuilder : IConfigurationBuilder { /// <summary> /// Returns the sources used to obtain configuration values. /// </summary> public IList<IConfigurationSource> Sources { get; } = new List<IConfigurationSource>(); /// <summary> /// Gets a key/value collection that can be used to share data between the <see cref="IConfigurationBuilder"/> /// and the registered <see cref="IConfigurationProvider"/>s. /// </summary> public IDictionary<string, object> Properties { get; } = new Dictionary<string, object>(); /// <summary> /// Adds a new configuration source. /// </summary> /// <param name="source">The configuration source to add.</param> /// <returns>The same <see cref="IConfigurationBuilder"/>.</returns> public IConfigurationBuilder Add(IConfigurationSource source) { if (source == null) { throw new ArgumentNullException(nameof(source)); } Sources.Add(source); return this; } /// <summary> /// Builds an <see cref="IConfiguration"/> with keys and values from the set of providers registered in /// <see cref="Sources"/>. /// </summary> /// <returns>An <see cref="IConfigurationRoot"/> with keys and values from the registered providers.</returns> 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); } } }
从上文的分析中我们已经知道此时 IList<IConfigurationSource> Sources 这个集合中存放的其实就是 JsonConfigurationSource 类型的对象。
我们找到 JsonConfigurationSource 类的源码:
// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; namespace Microsoft.Extensions.Configuration.Json { /// <summary> /// Represents a JSON file as an <see cref="IConfigurationSource"/>. /// </summary> public class JsonConfigurationSource : FileConfigurationSource { /// <summary> /// Builds the <see cref="JsonConfigurationProvider"/> for this source. /// </summary> /// <param name="builder">The <see cref="IConfigurationBuilder"/>.</param> /// <returns>A <see cref="JsonConfigurationProvider"/></returns> public override IConfigurationProvider Build(IConfigurationBuilder builder) { EnsureDefaults(builder); return new JsonConfigurationProvider(this); } } }
从中我们可以知道 JsonConfigurationSource 中的 Build(...) 方法返回的是 JsonConfigurationProvider 类型的对象。
至此,我们就清楚了 ConfigurationBuilder 类中的 Build(...) 方法返回的是 new ConfigurationRoot(providers) ,而 providers 集合里面存放的就是 JsonConfigurationProvider 类型的对象。
我们继续找到 ConfigurationRoot 类的源码,如下所示:
// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using System.Linq; using System.Threading; using Microsoft.Extensions.Primitives; namespace Microsoft.Extensions.Configuration { /// <summary> /// The root node for a configuration. /// </summary> public class ConfigurationRoot : IConfigurationRoot, IDisposable { private readonly IList<IConfigurationProvider> _providers; private readonly IList<IDisposable> _changeTokenRegistrations; private ConfigurationReloadToken _changeToken = new ConfigurationReloadToken(); /// <summary> /// Initializes a Configuration root with a list of providers. /// </summary> /// <param name="providers">The <see cref="IConfigurationProvider"/>s for this configuration.</param> public ConfigurationRoot(IList<IConfigurationProvider> providers) { if (providers == null) { throw new ArgumentNullException(nameof(providers)); } _providers = providers; _changeTokenRegistrations = new List<IDisposable>(providers.Count); foreach (var p in providers) { p.Load(); _changeTokenRegistrations.Add(ChangeToken.OnChange(() => p.GetReloadToken(), () => RaiseChanged())); } } /// <summary> /// The <see cref="IConfigurationProvider"/>s for this configuration. /// </summary> public IEnumerable<IConfigurationProvider> Providers => _providers; /// <summary> /// Gets or sets the value corresponding to a configuration key. /// </summary> /// <param name="key">The configuration key.</param> /// <returns>The configuration value.</returns> public string this[string key] { get { for (var i = _providers.Count - 1; i >= 0; i--) { var provider = _providers[i]; if (provider.TryGet(key, out var value)) { return value; } } return null; } set { if (!_providers.Any()) { throw new InvalidOperationException(Resources.Error_NoSources); } foreach (var provider in _providers) { provider.Set(key, value); } } } /// <summary> /// Gets the immediate children sub-sections. /// </summary> /// <returns>The children.</returns> public IEnumerable<IConfigurationSection> GetChildren() => this.GetChildrenImplementation(null); /// <summary> /// Returns a <see cref="IChangeToken"/> that can be used to observe when this configuration is reloaded. /// </summary> /// <returns>The <see cref="IChangeToken"/>.</returns> public IChangeToken GetReloadToken() => _changeToken; /// <summary> /// Gets a configuration sub-section with the specified key. /// </summary> /// <param name="key">The key of the configuration section.</param> /// <returns>The <see cref="IConfigurationSection"/>.</returns> /// <remarks> /// This method will never return <c>null</c>. If no matching sub-section is found with the specified key, /// an empty <see cref="IConfigurationSection"/> will be returned. /// </remarks> public IConfigurationSection GetSection(string key) => new ConfigurationSection(this, key); /// <summary> /// Force the configuration values to be reloaded from the underlying sources. /// </summary> public void Reload() { foreach (var provider in _providers) { provider.Load(); } RaiseChanged(); } private void RaiseChanged() { var previousToken = Interlocked.Exchange(ref _changeToken, new ConfigurationReloadToken()); previousToken.OnReload(); } /// <inheritdoc /> public void Dispose() { // dispose change token registrations foreach (var registration in _changeTokenRegistrations) { registration.Dispose(); } // dispose providers foreach (var provider in _providers) { (provider as IDisposable)?.Dispose(); } } } }
其中 ConfigurationSection 类的源码如下:
// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using Microsoft.Extensions.Primitives; namespace Microsoft.Extensions.Configuration { /// <summary> /// Represents a section of application configuration values. /// </summary> public class ConfigurationSection : IConfigurationSection { private readonly IConfigurationRoot _root; private readonly string _path; private string _key; /// <summary> /// Initializes a new instance. /// </summary> /// <param name="root">The configuration root.</param> /// <param name="path">The path to this section.</param> public ConfigurationSection(IConfigurationRoot root, string path) { if (root == null) { throw new ArgumentNullException(nameof(root)); } if (path == null) { throw new ArgumentNullException(nameof(path)); } _root = root; _path = path; } /// <summary> /// Gets the full path to this section from the <see cref="IConfigurationRoot"/>. /// </summary> public string Path => _path; /// <summary> /// Gets the key this section occupies in its parent. /// </summary> public string Key { get { if (_key == null) { // Key is calculated lazily as last portion of Path _key = ConfigurationPath.GetSectionKey(_path); } return _key; } } /// <summary> /// Gets or sets the section value. /// </summary> public string Value { get { return _root[Path]; } set { _root[Path] = value; } } /// <summary> /// Gets or sets the value corresponding to a configuration key. /// </summary> /// <param name="key">The configuration key.</param> /// <returns>The configuration value.</returns> public string this[string key] { get { return _root[ConfigurationPath.Combine(Path, key)]; } set { _root[ConfigurationPath.Combine(Path, key)] = value; } } /// <summary> /// Gets a configuration sub-section with the specified key. /// </summary> /// <param name="key">The key of the configuration section.</param> /// <returns>The <see cref="IConfigurationSection"/>.</returns> /// <remarks> /// This method will never return <c>null</c>. If no matching sub-section is found with the specified key, /// an empty <see cref="IConfigurationSection"/> will be returned. /// </remarks> public IConfigurationSection GetSection(string key) => _root.GetSection(ConfigurationPath.Combine(Path, key)); /// <summary> /// Gets the immediate descendant configuration sub-sections. /// </summary> /// <returns>The configuration sub-sections.</returns> public IEnumerable<IConfigurationSection> GetChildren() => _root.GetChildrenImplementation(Path); /// <summary> /// Returns a <see cref="IChangeToken"/> that can be used to observe when this configuration is reloaded. /// </summary> /// <returns>The <see cref="IChangeToken"/>.</returns> public IChangeToken GetReloadToken() => _root.GetReloadToken(); } }
仔细观察上面的源码后我们可以发现:
1、在ConfigurationRoot类的构造函数中它会依次调用传递过来的Provider的Load方法去加载数据。
2、使用ConfigurationRoot索引器取数据的时候,它会逆序依次调用Provider的TryGet方法获取数据,如果成功就直接返回,这就是为什么后添加的数据源会覆盖之前添加的数据源。
3、在创建ConfigurationSection的时候会把ConfigurationRoot传递进去,而ConfigurationSection取数据的时候也是通过ConfigurationRoot来取的,实际上ConfigurationSection并不包含任何实际的配置数据。
接下来我们就来看一下它是如何加载数据的,找到 JsonConfigurationProvider 类的源码,如下所示:
// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using System.IO; using System.Text.Json; namespace Microsoft.Extensions.Configuration.Json { /// <summary> /// A JSON file based <see cref="FileConfigurationProvider"/>. /// </summary> public class JsonConfigurationProvider : FileConfigurationProvider { /// <summary> /// Initializes a new instance with the specified source. /// </summary> /// <param name="source">The source settings.</param> public JsonConfigurationProvider(JsonConfigurationSource source) : base(source) { } /// <summary> /// Loads the JSON data from a stream. /// </summary> /// <param name="stream">The stream to read.</param> public override void Load(Stream stream) { try { Data = JsonConfigurationFileParser.Parse(stream); } catch (JsonException e) { throw new FormatException(Resources.Error_JSONParseError, e); } } } }
它是继承 FileConfigurationProvider 类,我们找到 FileConfigurationProvider 类的源码:
// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using System.IO; using System.Runtime.ExceptionServices; using System.Text; using System.Threading; using Microsoft.Extensions.Primitives; namespace Microsoft.Extensions.Configuration { /// <summary> /// Base class for file based <see cref="ConfigurationProvider"/>. /// </summary> public abstract class FileConfigurationProvider : ConfigurationProvider, IDisposable { private readonly IDisposable _changeTokenRegistration; /// <summary> /// Initializes a new instance with the specified source. /// </summary> /// <param name="source">The source settings.</param> public FileConfigurationProvider(FileConfigurationSource source) { if (source == null) { throw new ArgumentNullException(nameof(source)); } Source = source; if (Source.ReloadOnChange && Source.FileProvider != null) { _changeTokenRegistration = ChangeToken.OnChange( () => Source.FileProvider.Watch(Source.Path), () => { Thread.Sleep(Source.ReloadDelay); Load(reload: true); }); } } /// <summary> /// The source settings for this provider. /// </summary> public FileConfigurationSource Source { get; } /// <summary> /// Generates a string representing this provider name and relevant details. /// </summary> /// <returns> The configuration name. </returns> public override string ToString() => $"{GetType().Name} for '{Source.Path}' ({(Source.Optional ? "Optional" : "Required")})"; private void Load(bool reload) { var file = Source.FileProvider?.GetFileInfo(Source.Path); if (file == null || !file.Exists) { if (Source.Optional || reload) // Always optional on reload { Data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); } else { var error = new StringBuilder($"The configuration file '{Source.Path}' was not found and is not optional."); if (!string.IsNullOrEmpty(file?.PhysicalPath)) { error.Append($" The physical path is '{file.PhysicalPath}'."); } HandleException(ExceptionDispatchInfo.Capture(new FileNotFoundException(error.ToString()))); } } else { // Always create new Data on reload to drop old keys if (reload) { Data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); } using (var stream = file.CreateReadStream()) { try { Load(stream); } catch (Exception e) { HandleException(ExceptionDispatchInfo.Capture(e)); } } } // REVIEW: Should we raise this in the base as well / instead? OnReload(); } /// <summary> /// Loads the contents of the file at <see cref="Path"/>. /// </summary> /// <exception cref="FileNotFoundException">If Optional is <c>false</c> on the source and a /// file does not exist at specified Path.</exception> public override void Load() { Load(reload: false); } /// <summary> /// Loads this provider's data from a stream. /// </summary> /// <param name="stream">The stream to read.</param> public abstract void Load(Stream stream); private void HandleException(ExceptionDispatchInfo info) { bool ignoreException = false; if (Source.OnLoadException != null) { var exceptionContext = new FileLoadExceptionContext { Provider = this, Exception = info.SourceException }; Source.OnLoadException.Invoke(exceptionContext); ignoreException = exceptionContext.Ignore; } if (!ignoreException) { info.Throw(); } } /// <inheritdoc /> public void Dispose() => Dispose(true); /// <summary> /// Dispose the provider. /// </summary> /// <param name="disposing"><c>true</c> if invoked from <see cref="IDisposable.Dispose"/>.</param> protected virtual void Dispose(bool disposing) { _changeTokenRegistration?.Dispose(); } } }
仔细阅读 JsonConfigurationProvider 和 FileConfigurationProvider 这两个类的源码后,可以知道它最终是通过 JsonConfigurationFileParser.Parse(stream) 这个操作来加载数据的。
并且 FileConfigurationProvider 类继承自 ConfigurationProvider 类,我们找到 ConfigurationProvider 和 JsonConfigurationFileParser 这两个类的源码,如下所示:
ConfigurationProvider类源码如下:
// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using System.Linq; using System.Threading; using Microsoft.Extensions.Primitives; namespace Microsoft.Extensions.Configuration { /// <summary> /// Base helper class for implementing an <see cref="IConfigurationProvider"/> /// </summary> public abstract class ConfigurationProvider : IConfigurationProvider { private ConfigurationReloadToken _reloadToken = new ConfigurationReloadToken(); /// <summary> /// Initializes a new <see cref="IConfigurationProvider"/> /// </summary> protected ConfigurationProvider() { Data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); } /// <summary> /// The configuration key value pairs for this provider. /// </summary> protected IDictionary<string, string> Data { get; set; } /// <summary> /// Attempts to find a value with the given key, returns true if one is found, false otherwise. /// </summary> /// <param name="key">The key to lookup.</param> /// <param name="value">The value found at key if one is found.</param> /// <returns>True if key has a value, false otherwise.</returns> public virtual bool TryGet(string key, out string value) => Data.TryGetValue(key, out value); /// <summary> /// Sets a value for a given key. /// </summary> /// <param name="key">The configuration key to set.</param> /// <param name="value">The value to set.</param> public virtual void Set(string key, string value) => Data[key] = value; /// <summary> /// Loads (or reloads) the data for this provider. /// </summary> public virtual void Load() { } /// <summary> /// Returns the list of keys that this provider has. /// </summary> /// <param name="earlierKeys">The earlier keys that other providers contain.</param> /// <param name="parentPath">The path for the parent IConfiguration.</param> /// <returns>The list of keys for this provider.</returns> public virtual IEnumerable<string> GetChildKeys( IEnumerable<string> earlierKeys, string parentPath) { var prefix = parentPath == null ? string.Empty : parentPath + ConfigurationPath.KeyDelimiter; return Data .Where(kv => kv.Key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) .Select(kv => Segment(kv.Key, prefix.Length)) .Concat(earlierKeys) .OrderBy(k => k, ConfigurationKeyComparer.Instance); } private static string Segment(string key, int prefixLength) { var indexOf = key.IndexOf(ConfigurationPath.KeyDelimiter, prefixLength, StringComparison.OrdinalIgnoreCase); return indexOf < 0 ? key.Substring(prefixLength) : key.Substring(prefixLength, indexOf - prefixLength); } /// <summary> /// Returns a <see cref="IChangeToken"/> that can be used to listen when this provider is reloaded. /// </summary> /// <returns>The <see cref="IChangeToken"/>.</returns> public IChangeToken GetReloadToken() { return _reloadToken; } /// <summary> /// Triggers the reload change token and creates a new one. /// </summary> protected void OnReload() { var previousToken = Interlocked.Exchange(ref _reloadToken, new ConfigurationReloadToken()); previousToken.OnReload(); } /// <summary> /// Generates a string representing this provider name and relevant details. /// </summary> /// <returns> The configuration name. </returns> public override string ToString() => $"{GetType().Name}"; } }
JsonConfigurationFileParser类源码如下:
// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.Json; namespace Microsoft.Extensions.Configuration.Json { internal class JsonConfigurationFileParser { private JsonConfigurationFileParser() { } private readonly IDictionary<string, string> _data = new SortedDictionary<string, string>(StringComparer.OrdinalIgnoreCase); private readonly Stack<string> _context = new Stack<string>(); private string _currentPath; public static IDictionary<string, string> Parse(Stream input) => new JsonConfigurationFileParser().ParseStream(input); private IDictionary<string, string> ParseStream(Stream input) { _data.Clear(); var jsonDocumentOptions = new JsonDocumentOptions { CommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true, }; using (var reader = new StreamReader(input)) using (JsonDocument doc = JsonDocument.Parse(reader.ReadToEnd(), jsonDocumentOptions)) { if (doc.RootElement.ValueKind != JsonValueKind.Object) { throw new FormatException(Resources.FormatError_UnsupportedJSONToken(doc.RootElement.ValueKind)); } VisitElement(doc.RootElement); } return _data; } private void VisitElement(JsonElement element) { foreach (var property in element.EnumerateObject()) { EnterContext(property.Name); VisitValue(property.Value); ExitContext(); } } private void VisitValue(JsonElement value) { switch (value.ValueKind) { case JsonValueKind.Object: VisitElement(value); break; case JsonValueKind.Array: var index = 0; foreach (var arrayElement in value.EnumerateArray()) { EnterContext(index.ToString()); VisitValue(arrayElement); ExitContext(); index++; } break; case JsonValueKind.Number: case JsonValueKind.String: case JsonValueKind.True: case JsonValueKind.False: case JsonValueKind.Null: var key = _currentPath; if (_data.ContainsKey(key)) { throw new FormatException(Resources.FormatError_KeyIsDuplicated(key)); } _data[key] = value.ToString(); break; default: throw new FormatException(Resources.FormatError_UnsupportedJSONToken(value.ValueKind)); } } private void EnterContext(string context) { _context.Push(context); _currentPath = ConfigurationPath.Combine(_context.Reverse()); } private void ExitContext() { _context.Pop(); _currentPath = ConfigurationPath.Combine(_context.Reverse()); } } }
其中ConfigurationPath类源码如下:
// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; namespace Microsoft.Extensions.Configuration { /// <summary> /// Utility methods and constants for manipulating Configuration paths /// </summary> public static class ConfigurationPath { /// <summary> /// The delimiter ":" used to separate individual keys in a path. /// </summary> public static readonly string KeyDelimiter = ":"; /// <summary> /// Combines path segments into one path. /// </summary> /// <param name="pathSegments">The path segments to combine.</param> /// <returns>The combined path.</returns> public static string Combine(params string[] pathSegments) { if (pathSegments == null) { throw new ArgumentNullException(nameof(pathSegments)); } return string.Join(KeyDelimiter, pathSegments); } /// <summary> /// Combines path segments into one path. /// </summary> /// <param name="pathSegments">The path segments to combine.</param> /// <returns>The combined path.</returns> public static string Combine(IEnumerable<string> pathSegments) { if (pathSegments == null) { throw new ArgumentNullException(nameof(pathSegments)); } return string.Join(KeyDelimiter, pathSegments); } /// <summary> /// Extracts the last path segment from the path. /// </summary> /// <param name="path">The path.</param> /// <returns>The last path segment of the path.</returns> public static string GetSectionKey(string path) { if (string.IsNullOrEmpty(path)) { return path; } var lastDelimiterIndex = path.LastIndexOf(KeyDelimiter, StringComparison.OrdinalIgnoreCase); return lastDelimiterIndex == -1 ? path : path.Substring(lastDelimiterIndex + 1); } /// <summary> /// Extracts the path corresponding to the parent node for a given path. /// </summary> /// <param name="path">The path.</param> /// <returns>The original path minus the last individual segment found in it. Null if the original path corresponds to a top level node.</returns> public static string GetParentPath(string path) { if (string.IsNullOrEmpty(path)) { return null; } var lastDelimiterIndex = path.LastIndexOf(KeyDelimiter, StringComparison.OrdinalIgnoreCase); return lastDelimiterIndex == -1 ? null : path.Substring(0, lastDelimiterIndex); } } }
仔细阅读与ConfigurationProvider类相关的源码后可以发现, 其实ConfigurationProvider只是一个抽象类,具体的需要由对应类型的Provider去实现,其实也只是需要实现Load方法,去填充那个字符串字典。而在实现的过程中需要用到ConfigurationPath这个静态类来帮助生成字典里的key,具体来说就是各层级之间用冒号 “:” 隔开,例如:“A:B:C” 。这个冒号是以静态只读的形式定义在ConfigurationPath类中的。
3、小结
四大配置对象的UML图如下所示:
1、IConfigurationBuilder:整个配置系统的核心,包含多个IConfigurationSource,利用它们产生多个IConfigurationProvider,最终是为了得到一个IConfigurationRoot对象,并将它注入到容器中。
2、IConfigurationProvider:实际包含配置信息的类,内部包含一个字符串字典,它由IConfigurationSource产生。
3、IConfigurationSource:包含了一个配置源的信息,以文件为例,包含了文件名和文件路径等,并不包含实际的配置信息。如果是基于数据库的数据源,它会包含数据库连接信息,SQL等。它的目的是为了产生一个IConfigurationProvider。
4、IConfigurationRoot:在获取配置信息时,直接操作的对象,内部包含一个IConfigurationProvider列表。
大致过程:首先是调用各类数据源类库提供的扩展方法往IConfigurationBuilder中添加数据源IConfigurationSource,之后IConfigurationBuilder会依次调用IConfigurationSource的Build方法产生对应的IConfigurationProvider,并将他们传入到IConfigurationRoot中,最终我们会拿到IConfigurationRoot进行使用。
本文部分内容参考博文:https://www.cnblogs.com/zhurongbo/p/10831284.html
至此本文就全部介绍完了,如果觉得对您有所启发请记得点个赞哦!!!
aspnetcore源码:
链接:https://pan.baidu.com/s/1fszyRzDw9di1hVvPIF4VYg 提取码:wz1j
Demo源码:
链接:https://pan.baidu.com/s/1AdV-cwWnIKNx81WQqEXUdg 提取码:ki9c
此文由博主精心撰写转载请保留此原文链接:https://www.cnblogs.com/xyh9039/p/15916859.html
版权声明:如有雷同纯属巧合,如有侵权请及时联系本人修改,谢谢!!!