前言
在之前的文章.Net Core Configuration源碼探究一文中我們曾解讀過Configuration的工作原理,也在.Net Core Configuration Etcd資料源一文中探讨過為Configuration自定義資料源需要哪些操作。由于Configuration配置系統是.Net Core的核心子產品,其中包含了許多細節。通過啟動時指令行CommandLine、環境變量、配置檔案或定義其他資料源的形式,其實都是适配到配置系統中,我們都可以通過Configuration去讀取它們的資料,但是在程式預設的情況下他們讀取的優先級到底是怎麼樣的呢?接下來我們就一起來研究一下。
代碼示範
由于Configuration資料操作是我們實操代碼過程中不可或缺的環節,是以我們先通過代碼的形式來看一下,它的讀取順序到底是什麼樣子的,首先我們建立一個示例,在這個示例中我們分别在常用配置資料的地方,CommandLine、環境變量、appsettings.json、ConfigureWebHostDefaults中的UseSetting和ConfigureAppConfiguration中讀取自定義的檔案mysettings.json中分别設定一個同名的配置節點叫FromSource,然後它的值設定FromSource節點的資料來自于哪個配置方式,比如環境變量中我配置的是Environment
"MyDemo.Config": { "commandName": "Project", "launchBrowser": true, "applicationUrl": "http://localhost:19573", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "FromSource": "Environment" }
配置檔案中我配置的是appsetting.json
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*", "FromSource": "appsetting.json" }
自定義的配置檔案中我配置的是mysettings.json
{ "FromSource": "mysetting.json" }
然後在啟動程式Program.cs中配置如下
public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureAppConfiguration(config => { config.AddJsonFile("mysettings.json", optional: true, reloadOnChange: true); }) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseSetting("FromSource", "UseSetting"); webBuilder.UseStartup<Startup>(); });
為了友善示範我們在程式的預設終結點中添加響應的讀取代碼
app.UseEndpoints(endpoints => { endpoints.MapGet("/", async context => { await context.Response.WriteAsync($"Read Node FromSource={Configration["FromSource"]}"); }); });
以上操作我們都完成了配置後,然後通過CLI的方式啟動程式并傳遞--FromSource=CommandLine
dotnet run --FromSource=CommandLine
程式運作起來之後輸入host+port的形式請求預設路徑得到的結果是
Read Node FromSource=mysetting.json
說明預設情況下優先級最高的是通過ConfigureAppConfiguration方法注冊自定義配置,然後我們注釋掉設定讀取mysetting.json資料源的相關代碼,然後繼續運作程式,得到的結果是
Read Node FromSource=CommandLine
這個是通過CLI啟動程式我們手動傳遞的指令行參數,然後我們退出程式,再次通過CLI的方式運作程式,但是這次我們不傳遞--FromSource=CommandLine,得到的結果是
Read Node FromSource=Environment
這是我們在環境變量中配置的節點資料,然後我們注釋掉在環境變量中配置的節點資料,再次啟動程式得到的結果是
Read Node FromSource=appsetting.json
也就是我們在預設配置檔案中appsetting.json配置的資料,然後我們注釋掉這個資料節點,繼續運作程式,毫無疑問得到的結果是
Read Node FromSource=UseSetting
通過這個示範結果我們可以得到這麼一個結論,在Asp.Net Core中如果你采用的是系統預設的形式建構的程式,那麼讀取配置節點的優先級是ConfigureAppConfiguration(自定義讀取)>CommandLine(指令行參數)>Environment(環境變量)>appsetting.json(預設配置檔案)>UseSetting的順序。
源碼探究
要想知道,為什麼示範示例會出現那種順序,還要從源碼着手。在之前的.Net Core Configuration源碼探究中我們提到過Configuration讀取資料的順序采用的是後來者居上的形式,也就是說,後被注冊的ConfigurationProvider中的資料會優先被讀取到,這個操作處理在ConfigurationRoot類中可以找到相關邏輯[點選檢視源碼👈],它的實作是這樣的
public string this[string key] { get { //通過這個我們可以了解到讀取的順序取決于注冊Source的順序,采用的是後來者居上的方式 //後注冊的會先被讀取到,如果讀取到直接return 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); } } }
通過這段代碼我們就心理就有底了,也就是說,上面示例表現出來的現象,無非就是注冊順序的問題。
預設的CreateDefaultBuilder
預設情況下我們都是通過Host.CreateDefaultBuilder(args)的方式去建構的HostBuilder,那麼我們就從這個方法入手,找到源碼位置👈,我們抽離出關于配置操作的邏輯,大緻如下
public static IHostBuilder CreateDefaultBuilder(string[] args) { var builder = new HostBuilder(); //配置預設内容根目錄為目前程式運作目錄 builder.UseContentRoot(Directory.GetCurrentDirectory()); //配置HostConfiguration,這個地方不要被吓到,最終通過HostConfiguration配置的操作都是要加載到ConfigureAppConfiguration裡的 //至于如何加載,待會我們會通過源碼看到 builder.ConfigureHostConfiguration(config => { //先配置環境變量 config.AddEnvironmentVariables(prefix: "DOTNET_"); //然後配置指令行讀取 if (args != null) { config.AddCommandLine(args); } }); builder.ConfigureAppConfiguration((hostingContext, config) => { var env = hostingContext.HostingEnvironment; //首先添加的就是讀取appsettings.json相關 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(); //啟動時指令行參數不為null則添加CommandLine讀取 if (args != null) { config.AddCommandLine(args); } }) //*其他部分邏輯已省略,有興趣可自行點選上方連接配接檢視源碼 return builder; }
通過CreateDefaultBuilder我們可以非常清晰的得到這個結論由于先注冊的是讀取appsettings.json相關的邏輯,然後是AddEnvironmentVariables去讀取環境變量,最後是AddCommandLine讀取指令行參數加載到Configuration中,是以通過這個我們驗證了優先級CommandLine(指令行參數)>Environment(環境變量)>appsetting.json(預設配置檔案)的順序。
ConfigureAppConfiguration中尋找答案
通過上面CreateDefaultBuilder我們得到了Configuration預設讀取優先級的一部分邏輯認證,但是在示例的示範中,我們清楚的看到ConfigureAppConfiguration中配置的讀取優先級是大于以上任何一個讀取方式的,是以接下來我們還得需要到ConfigureAppConfiguration方法中一探究竟,這是一個擴充方法,預設調用的是HostBuilder中的ConfigureAppConfiguration方法[點選檢視源碼👈]
public IHostBuilder ConfigureAppConfiguration(Action<HostBuilderContext, IConfigurationBuilder> configureDelegate) { _configureAppConfigActions.Add(configureDelegate ?? throw new ArgumentNullException(nameof(configureDelegate))); return this; }
_configureAppConfigActions是HostBuilder的私有屬性
private List<Action<HostBuilderContext, IConfigurationBuilder>> _configureAppConfigActions = new List<Action<HostBuilderContext, IConfigurationBuilder>>();
也就是說我們通過ConfigureAppConfiguration實作的邏輯都會被添加到_configureAppConfigActions這個List中,但是這個還不是我們要查找的核心。看來我們要去HostBuilder.Build()方法找尋找答案了,畢竟真正的建構邏輯還是在Build方法中,最後我們找到了如下方法[點選檢視源碼👈]
private void BuildAppConfiguration() { //用預設的ContentRootPath去建構一個全局的ConfigurationBuilder var configBuilder = new ConfigurationBuilder() .SetBasePath(_hostingEnvironment.ContentRootPath) //首先就是把通過ConfigureHostConfiguration配置的相關添加到ConfigurationBuilder中 .AddConfiguration(_hostConfiguration, shouldDisposeConfiguration: true); //通過循環的方式去執行我們注冊到_configureAppConfigActions集合中的邏輯 foreach (var buildAction in _configureAppConfigActions) { buildAction(_hostBuilderContext, configBuilder); } _appConfiguration = configBuilder.Build(); _hostBuilderContext.Configuration = _appConfiguration; }
由于_configureAppConfigActions是被循環執行的,也就是說先被注冊到ConfigureAppConfiguration中的邏輯也是優先被執行,那麼我們在CreateDefaultBuilder方法中,系統預設給我注冊的AddJsonFile、AddEnvironmentVariables、AddCommandLine的調用順序要優先于我們自行通過ConfigureAppConfiguration注冊配置的邏輯。由于Configuration讀取資料的順序采用的是後來者居上的形式,是以我們自行通過ConfigureAppConfiguration注冊的配置邏輯優先級是大于系統預設給我們注冊讀取配置的優先級。是以通過這些我們可以得到了這個結論ConfigureAppConfiguration(自定義讀取)>CommandLine(指令行參數)>Environment(環境變量)>appsetting.json(預設配置檔案)。除此之外還可以得到一個結論,預設情況下通過ConfigureHostConfiguration添加的配置相關,優先級是最低的。因為在循環執行_configureAppConfigActions循環之前,也就是在建構ConfigurationBuilder的時候就添加了ConfigureHostConfiguration。
UseSetting最後的迷霧
通過上面的相關源碼我們已經得到了,關于預設配置讀取優先級的大部分實作邏輯,僅僅剩下通過ConfigureWebHostDefaults中添加的UseSetting相關邏輯。可能有許多同學不清楚,其實UseSetting也是添加到配置系統當中去的,這個可以檢視具體源碼[點選檢視源碼👈]
private IConfiguration _config = new ConfigurationBuilder() .AddEnvironmentVariables(prefix: "ASPNETCORE_") .Build(); public IWebHostBuilder UseSetting(string key, string value) { _config[key] = value; return this; }
也就是說,接下來我們隻要找到_config是如何注冊到全局的ConfigurationBuilder中,就能撥開最後的迷霧,找到真正的答案。我們通過入口方法ConfigureWebHostDefaults往下找,雖然過程有點曲折,但是我們還是在GenericWebHostBuilder的構造函數中找到了如下邏輯邏輯[點選檢視源碼👈]
public GenericWebHostBuilder(IHostBuilder builder) { _builder = builder; //這個就是上面UseSetting操作的_config _config = new ConfigurationBuilder() .AddEnvironmentVariables(prefix: "ASPNETCORE_") .Build(); //把_config通過ConfigureHostConfiguration方法注冊到了全局的ConfigurationBuilder中去 _builder.ConfigureHostConfiguration(config => { config.AddConfiguration(_config); ExecuteHostingStartups(); }); //*其他部分代碼省略 }
看到這個邏輯突然就恍然大悟了,我們上面曾經說過通過ConfigureHostConfiguration添加的配置相關,優先級是最低的。因為在HostBuilder.Build()調用的BuildAppConfiguration方法中我們可以得知,在循環執行_configureAppConfigActions循環之前,也就是在建構ConfigurationBuilder的時候就添加了ConfigureHostConfiguration。而UseSetting操作的Configuration正是通過ConfigureHostConfiguration注冊到ConfigurationBuilder中去的,是以通過UseSetting添加的配置相關優先級要低于之前我們提到的其他配置邏輯。
總結
通過本次談到我們得到了預設情況下讀取配置Configuration的預設優先級,也就是ConfigureAppConfiguration(自定義讀取)>CommandLine(指令行參數)>Environment(環境變量)>appsetting.json(預設配置檔案)>UseSetting的順序。然後我們通過分析源碼的形式,得到了為什麼會是這個讀取優先級的緣由。總之還是脫離不了那個宗旨,Configuration讀取資料的順序采用的是後來者居上的形式,後被注冊的會優先被讀取到。
說點題外話,我覺得閱讀源碼是一件非常有趣的事情,不是說我要把所有源碼看一遍,或者都能看懂。而是當我心理産生了疑惑,但是這個疑惑我通過閱讀源碼的途徑變得豁然開朗,這才是讀源碼真正的樂趣所在。漫無目的或者為了讀而讀,會失去興趣所在,容易導緻效率低下,看明白了源碼的設計,提升了自己的思維方式,也許才是真正的自我提升。
👇歡迎掃碼關注我的公衆号👇
