原始碼解析.Net中IConfiguration配置的實現

SnailZz發表於2021-08-16

前言

關於IConfituration的使用,我覺得大部分人都已經比較熟悉了,如果不熟悉的可以看這裡。因為本篇不準備講IConfiguration都是怎麼使用的,但是在原始碼部分的解讀,網上資源相對少一點,所以本篇準備著重原始碼這一塊的設計,儘量的讓讀者能夠理解它的內部實現。

IConfiguration類之間的關係

這裡我整理了一個UML(可能不是那麼標準,一些依賴關係沒有體現)。可能直接看會有點不懂,下面我會慢慢講這些東西。

原始碼解析

我們知道.net中的配置載入是有優先順序的,如果有相同的key的話,一般後面載入的會覆蓋前面的值,它們的優先順序順序如下圖:

  • 在Host.CreateDefaultBuilder(args)執行的程式碼中,將委託新增到一個IConfigurationBuilder的集合中,builder為HostBuilder,hostingContext為HostBuilderContext,config就是IConfigurationBuilder,我們先來看下載入的程式碼,如下:
builder.ConfigureAppConfiguration((hostingContext, config) =>
{
    IHostEnvironment env = hostingContext.HostingEnvironment;
    bool reloadOnChange = GetReloadConfigOnChangeValue(hostingContext);
    config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: reloadOnChange) //載入appsettings.json
            .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: reloadOnChange); //根據環境變數載入appsettings.json
    if (env.IsDevelopment() && env.ApplicationName is { Length: > 0 })
    {
        var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName));
        if (appAssembly is not null)
        {
            config.AddUserSecrets(appAssembly, optional: true, reloadOnChange: reloadOnChange); //開發環境載入UserSecrets
        }
    }
    config.AddEnvironmentVariables(); //載入環境變數
    if (args is { Length: > 0 })
    {
        config.AddCommandLine(args); //載入命令列引數
    }
})
  • 接下來我們拿其中一個例子,載入命令列引數,看看在AddCommandLine中都做了什麼事情,是如何構建出IConfiguration物件的。
  1. 從UML圖中可以看到,擴充套件物件都實現了IConfigurationSource,並且繼承了抽象類ConfigurationProvider,原始碼如下:
//ConfigurationBuilder擴充套件類
public static class CommandLineConfigurationExtensions
{
    public static IConfigurationBuilder AddCommandLine(this IConfigurationBuilder configurationBuilder, string[] args)
    {
        return configurationBuilder.AddCommandLine(args, switchMappings: null);
    }
    public static IConfigurationBuilder AddCommandLine(
        this IConfigurationBuilder configurationBuilder,
        string[] args,
        IDictionary<string, string> switchMappings)
    {
        //SwitchMappings是Key對映,可以看微軟文件
        configurationBuilder.Add(new CommandLineConfigurationSource { Args = args, SwitchMappings = switchMappings });
        return configurationBuilder;
    }
}
public class CommandLineConfigurationSource : IConfigurationSource
{
    public IDictionary<string, string> SwitchMappings { get; set; }
    public IEnumerable<string> Args { get; set; }
    public IConfigurationProvider Build(IConfigurationBuilder builder)
    {
        return new CommandLineConfigurationProvider(Args, SwitchMappings);
    }
}
//主要實現其Load方法,將kv載入到Data中,以便拿取。
public class CommandLineConfigurationProvider : ConfigurationProvider
{
    private readonly Dictionary<string, string> _switchMappings;
    public CommandLineConfigurationProvider(IEnumerable<string> args, IDictionary<string, string> switchMappings = null)
    {
        Args = args ?? throw new ArgumentNullException(nameof(args));
        //預設情況下mapping是null
        if (switchMappings != null)
        {
            //是個私有方法,原始碼就不貼了,可以自己下載來看看。
            _switchMappings = GetValidatedSwitchMappingsCopy(switchMappings);
        }
    }
    protected IEnumerable<string> Args { get; private set; }
    //load方法,主要是來設定kv,因為data是個字典型別,要把kv給設定和獲取到。
    public override void Load()
    {
        var data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
        string key, value;

        using (IEnumerator<string> enumerator = Args.GetEnumerator())
        {
            while (enumerator.MoveNext())
            {
                string currentArg = enumerator.Current;
                int keyStartIndex = 0;

                if (currentArg.StartsWith("--"))
                {
                    keyStartIndex = 2;
                }
                else if (currentArg.StartsWith("-"))
                {
                    keyStartIndex = 1;
                }
                else if (currentArg.StartsWith("/"))
                {
                    currentArg = $"--{currentArg.Substring(1)}";
                    keyStartIndex = 2;
                }
                int separator = currentArg.IndexOf('=');
                if (separator < 0)
                {
                    if (keyStartIndex == 0)
                    {
                        continue;
                    }
                    if (_switchMappings != null && _switchMappings.TryGetValue(currentArg, out string mappedKey))
                    {
                        key = mappedKey;
                    }
                    else if (keyStartIndex == 1)
                    {
                        continue;
                    }
                    else
                    {
                        key = currentArg.Substring(keyStartIndex);
                    }

                    string previousKey = enumerator.Current;
                    if (!enumerator.MoveNext())
                    {
                        continue;
                    }
                    value = enumerator.Current;
                }
                else
                {
                    string keySegment = currentArg.Substring(0, separator);
                    if (_switchMappings != null && _switchMappings.TryGetValue(keySegment, out string mappedKeySegment))
                    {
                        key = mappedKeySegment;
                    }
                    else if (keyStartIndex == 1)
                    {
                        throw new FormatException(SR.Format(SR.Error_ShortSwitchNotDefined, currentArg));
                    }
                    else
                    {
                        key = currentArg.Substring(keyStartIndex, separator - keyStartIndex);
                    }

                    value = currentArg.Substring(separator + 1);
                }

                data[key] = value;
            }
        }
        Data = data;
    }
}

2.在構建主機資訊HostBuilder,下面只貼出主要程式碼,可以看到最後執行了ConfigurationBuilder的Build方法,構建IConfiguration,原始碼如下:

public class HostBuilder : IHostBuilder
{
    private List<Action<IConfigurationBuilder>> _configureHostConfigActions = new List<Action<IConfigurationBuilder>>();
    public IHostBuilder ConfigureAppConfiguration(Action<HostBuilderContext, IConfigurationBuilder> configureDelegate)
    {
        _configureAppConfigActions.Add(configureDelegate ?? throw new ArgumentNullException(nameof(configureDelegate)));
        return this;
    }
    //即我們在main函式中看到的Build方法
    public IHost Build()
    {
        //只能執行一次這個方法
        if (_hostBuilt)
        {
            throw new InvalidOperationException(SR.BuildCalled);
        }
        _hostBuilt = true;
        using var diagnosticListener = new DiagnosticListener("Microsoft.Extensions.Hosting");
        const string hostBuildingEventName = "HostBuilding";
        const string hostBuiltEventName = "HostBuilt";

        if (diagnosticListener.IsEnabled() && diagnosticListener.IsEnabled(hostBuildingEventName))
        {
            Write(diagnosticListener, hostBuildingEventName, this);
        }
        //執行Host配置(應用程式執行路徑,增加_dotnet環境變數,獲取命令列引數,載入預配置)
        BuildHostConfiguration();
        //設定主機環境變數
        CreateHostingEnvironment();
        //設定上下文
        CreateHostBuilderContext();
        //構建程式配置(載入appsetting.json)
        BuildAppConfiguration();
        //構造容器,載入服務
        CreateServiceProvider();
        var host = _appServices.GetRequiredService<IHost>();
        if (diagnosticListener.IsEnabled() && diagnosticListener.IsEnabled(hostBuiltEventName))
        {
            Write(diagnosticListener, hostBuiltEventName, host);
        }

        return host;
    }
    private void BuildAppConfiguration()
    {
        //對於已經載入過的配置不再重新載入
        IConfigurationBuilder configBuilder = new ConfigurationBuilder()
            .SetBasePath(_hostingEnvironment.ContentRootPath)
            .AddConfiguration(_hostConfiguration, shouldDisposeConfiguration: true);

        //注意這裡是AppConfig
        foreach (Action<HostBuilderContext, IConfigurationBuilder> buildAction in _configureAppConfigActions)
        {
            buildAction(_hostBuilderContext, configBuilder);
        }
        _appConfiguration = configBuilder.Build();
        //將新的配置賦值給config
        _hostBuilderContext.Configuration = _appConfiguration;
    }
}
  1. ConfigurationBuilder的Build方法中載入對用Provider類裡面的Load方法,載入配置資訊。在獲取對應Key值時,由載入的順序,按照從後到前的順序,依次查詢key對應的value值,原始碼如下:
public class ConfigurationBuilder : IConfigurationBuilder
{
    //所有的配置源集合
    public IList<IConfigurationSource> Sources { get; } = new List<IConfigurationSource>();
    public IDictionary<string, object> Properties { get; } = new Dictionary<string, object>();
    public IConfigurationBuilder Add(IConfigurationSource source)
    {
        if (source == null)
        {
            throw new ArgumentNullException(nameof(source));
        }

        Sources.Add(source);
        return this;
    }

    public IConfigurationRoot Build()
    {
        var providers = new List<IConfigurationProvider>();
        foreach (IConfigurationSource source in Sources)
        {
            //通過配置源構建provider物件。
            IConfigurationProvider provider = source.Build(this);
            providers.Add(provider);
        }
        //構建配置根物件
        return new ConfigurationRoot(providers);
    }
}

 public class ConfigurationRoot : IConfigurationRoot, IDisposable
 {
    private readonly IList<IConfigurationProvider> _providers;
    private readonly IList<IDisposable> _changeTokenRegistrations;
    private ConfigurationReloadToken _changeToken = new ConfigurationReloadToken();

    public ConfigurationRoot(IList<IConfigurationProvider> providers)
    {
        if (providers == null)
        {
            throw new ArgumentNullException(nameof(providers));
        }

        _providers = providers;
        _changeTokenRegistrations = new List<IDisposable>(providers.Count);
        foreach (IConfigurationProvider p in providers)
        {
            p.Load();
            _changeTokenRegistrations.Add(ChangeToken.OnChange(() => p.GetReloadToken(), () => RaiseChanged()));
        }
    }
    public IEnumerable<IConfigurationProvider> Providers => _providers;
    //通過索引拿資料,比如其他獲取value的方法,其都是擴充套件類中實現的,讀者可以自己看下。
    public string this[string key]
    {
        get
        {
            //從後往前查Porvider裡面對應的KV,每個Provider裡面都有一個Data(字典)物件,只獲取第一個的。
            for (int i = _providers.Count - 1; i >= 0; i--)
            {
                IConfigurationProvider provider = _providers[i];

                if (provider.TryGet(key, out string value))
                {
                    return value;
                }
            }
            return null;
        }
        set
        {
            if (_providers.Count == 0)
            {
                throw new InvalidOperationException(SR.Error_NoSources);
            }
            //修改時,所有的key對應的value值都修改
            foreach (IConfigurationProvider provider in _providers)
            {
                provider.Set(key, value);
            }
        }
    }
}

總結

  1. 可以看到微軟為了保留擴充套件性,增加了抽象類和抽象介面,讓使用者能夠自定義的擴充套件實現自己的配置。

  2. 配置的載入順序尤為重要,後載入的會覆蓋前面的key。

  3. 檔案監聽,由於篇幅原因,不寫那麼多了,原理是通過ChangeToken和FileProvider實現的。

    這裡只是把核心程式碼給提出來,並不是全部程式碼,所以讀者如果想看其他的,需要自己翻下原始碼。其實在讀原始碼的過程中,一方面是為了探究實現原理,更大的收穫是能夠學習到人家巧妙的設計,能夠用到自己的程式碼中。其實筆者的水平的也不是很高,如果有差錯的地方,望讀者能夠提出來,以便我及時改正。

相關文章