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

yi念之間發表於2020-08-30

前言

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

?歡迎掃碼關注我的公眾號? 深入探究.Net Core Configuration讀取配置的優先順序

相關文章