深入探究.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": "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读取数据的顺序采用的是后来者居上的形式,后被注册的会优先被读取到。
    说点题外话,我觉得阅读源码是一件非常有趣的事情,不是说我要把所有源码看一遍,或者都能看懂。而是当我心理产生了疑惑,但是这个疑惑我通过阅读源码的途径变得豁然开朗,这才是读源码真正的乐趣所在。漫无目的或者为了读而读,会失去兴趣所在,容易导致效率低下,看明白了源码的设计,提升了自己的思维方式,也许才是真正的自我提升。