深入探究.Net Core Configuration讀取配置的優先順序

前言

    在之前的文章.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": "//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讀取數據的順序採用的是後來者居上的形式,後被註冊的會優先被讀取到。
    說點題外話,我覺得閱讀源碼是一件非常有趣的事情,不是說我要把所有源碼看一遍,或者都能看懂。而是當我心理產生了疑惑,但是這個疑惑我通過閱讀源碼的途徑變得豁然開朗,這才是讀源碼真正的樂趣所在。漫無目的或者為了讀而讀,會失去興趣所在,容易導致效率低下,看明白了源碼的設計,提升了自己的思維方式,也許才是真正的自我提升。

👇歡迎掃碼關注我的公眾號👇