前言
關於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物件的。
- 從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;
}
}
- 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);
}
}
}
}
總結
-
可以看到微軟為了保留擴充套件性,增加了抽象類和抽象介面,讓使用者能夠自定義的擴充套件實現自己的配置。
-
配置的載入順序尤為重要,後載入的會覆蓋前面的key。
-
檔案監聽,由於篇幅原因,不寫那麼多了,原理是通過ChangeToken和FileProvider實現的。
這裡只是把核心程式碼給提出來,並不是全部程式碼,所以讀者如果想看其他的,需要自己翻下原始碼。其實在讀原始碼的過程中,一方面是為了探究實現原理,更大的收穫是能夠學習到人家巧妙的設計,能夠用到自己的程式碼中。其實筆者的水平的也不是很高,如果有差錯的地方,望讀者能夠提出來,以便我及時改正。