上一章介紹了配置的多種數據源被註冊、加載和獲取的過程,本節看一下這個過程系統是如何實現的。(ASP.NET Core 系列目錄)html
在上一節介紹的數據源設置中,appsettings.json、命令行、環境變量三種方式是被系統自動加載的,這是由於系統在webHost.CreateDefaultBuilder(args)中已經爲這三種數據源進了註冊,那麼就從這個方法提及。這個方法中一樣調用了ConfigureAppConfiguration方法,代碼以下:web
public static IWebHostBuilder CreateDefaultBuilder(string[] args) { var builder = newWebHostBuilder(); //省略部分代碼 builder.UseKestrel((builderContext, options) => { options.Configure(builderContext.Configuration.GetSection("Kestrel")); }) .ConfigureAppConfiguration((hostingContext, config) => { var env = hostingContext.HostingEnvironment; config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional:true, reloadOnChange: true); if(env.IsDevelopment()) { var appAssembly = Assembly.Load(newAssemblyName(env.ApplicationName)); if(appAssembly != null) { config.AddUserSecrets(appAssembly, optional: true); } } config.AddEnvironmentVariables(); if(args != null) { config.AddCommandLine(args); } }) //省略部分代碼 return builder; }
看一下其中的ConfigureAppConfiguration方法,加載的內容主要有四種,首先加載的是appsettings.json和appsettings.{env.EnvironmentName}.json兩個JSON文件,關於env.EnvironmentName在前面的章節已經說過,常見的有Development、Staging 和 Production三種值,在咱們開發調試時通常是Development,也就是會加載appsettings.json和appsettings. Development.json兩個JSON文件。第二種加載的是用戶機密文件,這僅限於Development狀態下,會經過config.AddUserSecrets方法加載。第三種是經過config.AddEnvironmentVariables方法加載的環境變量,第四種是經過config.AddCommandLine方法加載的命令行參數。json
注意:這裏的ConfigureAppConfiguration方法這時候是不會被執行的,只是將這個方法做爲一個Action<WebHostBuilderContext, IConfigurationBuilder> configureDelegate添加到了WebHostBuilder的_configureServicesDelegates屬性中。configureServicesDelegates是一個List<Action<WebHostBuilderContext, IConfigurationBuilder>>類型的集合。對應代碼以下:app
public IWebHostBuilder ConfigureAppConfiguration(Action<WebHostBuilderContext, IConfigurationBuilder> configureDelegate) { if(configureDelegate == null) { throw new ArgumentNullException(nameof(configureDelegate)); } _configureAppConfigurationBuilderDelegates.Add(configureDelegate); returnthis; }
上一節的例子中,咱們在webHost.CreateDefaultBuilder(args)方法以後再次調用ConfigureAppConfiguration方法添加了一些自定義的數據源,這個方法也是沒有執行,一樣被添加到了這個集合中。直到WebHostBuilder經過它的Build()方法建立WebHost的時候,纔會遍歷這個集合逐一執行。這段代碼寫在被Build()方法調用的BuildCommonServices()中:ide
private IServiceCollection BuildCommonServices(out AggregateException hostingStartupErrors) { //省略部分代碼 var builder = new ConfigurationBuilder() .SetBasePath(_hostingEnvironment.ContentRootPath) .AddConfiguration(_config); foreach (var configureAppConfiguration in _configureAppConfigurationBuilderDelegates) { configureAppConfiguration(_context, builder); } var configuration = builder.Build(); services.AddSingleton<IConfiguration>(configuration); _context.Configuration = configuration; //省略部分代碼 return services; }
首先建立了一個ConfigurationBuilder對象,而後經過foreach循環逐一執行被添加到集合_configureAppConfigurationBuilderDelegates中的configureAppConfiguration方法,那麼在執行的時候,這些不一樣的數據源是如何被加載的呢?這部分功能在namespace Microsoft.Extensions.Configuration命名空間中。ui
以appsettings.json對應的config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)方法爲例,進一步看一下它的實現方式。首先介紹的是IConfigurationBuilder接口,對應的實現類是ConfigurationBuilder,代碼以下:this
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; } //省略了IConfigurationRoot Build()方法,下文介紹 }
ConfigureAppConfiguration方法中調用的AddJsonFile方法來自JsonConfigurationExtensions類,代碼以下:spa
public static class JsonConfigurationExtensions { //省略部分代碼 public static IConfigurationBuilder AddJsonFile(this IConfigurationBuilder builder, IFileProvider provider, string path, bool optional, bool reloadOnChange) { if (builder == null) { throw new ArgumentNullException(nameof(builder)); } if (string.IsNullOrEmpty(path)) { throw new ArgumentException(Resources.Error_InvalidFilePath, nameof(path)); } return builder.AddJsonFile(s => { s.FileProvider = provider; s.Path = path; s.Optional = optional; s.ReloadOnChange = reloadOnChange; s.ResolveFileProvider(); }); } public static IConfigurationBuilder AddJsonFile(this IConfigurationBuilder builder, Action<JsonConfigurationSource> configureSource) => builder.Add(configureSource); }
AddJsonFile方法會建立一個JsonConfigurationSource並經過ConfigurationBuilder的Add(IConfigurationSource source)方法將這個JsonConfigurationSource添加到ConfigurationBuilder的IList<IConfigurationSource> Sources集和中去。命令行
同理,針對環境變量,存在對應的EnvironmentVariablesExtensions,會建立一個對應的EnvironmentVariablesConfigurationSource添加到ConfigurationBuilder的IList<IConfigurationSource> Sources集和中去。這樣的還有CommandLineConfigurationExtensions和CommandLineConfigurationSource等,最終結果就是會根據數據源的加載順序,生成多個XXXConfigurationSource對象(它們都直接或間接實現了IConfigurationSource接口)添加到ConfigurationBuilder的IList<IConfigurationSource> Sources集和中。調試
在Program文件的WebHost.CreateDefaultBuilder(args)方法中的ConfigureAppConfiguration方法被調用後,若是在CreateDefaultBuilder方法以後再次調用了ConfigureAppConfiguration方法並添加了數據源(如同上一節的例子),一樣會生成相應的XXXConfigurationSource對象添加到ConfigurationBuilder的IList<IConfigurationSource> Sources集和中。
注意:這裏不是每一種數據源生成一個XXXConfigurationSource,而是按照每次添加生成一個XXXConfigurationSource,而且遵循添加的前後順序。例如添加多個JSON文件,會生成多個JsonConfigurationSource。
這些ConfigurationSource之間的關係以下圖1:
圖1
到這裏各類數據源的收集工做完成,都添加到了ConfigurationBuilder的IList<IConfigurationSource> Sources屬性中。
回到BuildCommonServices方法中,經過foreach循環逐一執行了configureAppConfiguration方法獲取到IList<IConfigurationSource>以後,下一句是varconfiguration = builder.Build(),這是調用ConfigurationBuilder的Build()方法建立了一個IConfigurationRoot對象。對應代碼以下:
public class ConfigurationBuilder : IConfigurationBuilder { public IList<IConfigurationSource> Sources { get; } = new List<IConfigurationSource>(); //省略部分代碼 public IConfigurationRoot Build() { var providers = new List<IConfigurationProvider>(); foreach (var source in Sources) { var provider = source.Build(this); providers.Add(provider); } return new ConfigurationRoot(providers); } }
這個方法主要體現了兩個過程:首先,遍歷IList<IConfigurationSource> Sources集合,主要調用其中的各個IConfigurationSource的Build方法建立對應的IConfigurationProvider,最終生成一個List<IConfigurationProvider>;第二,經過集合List<IConfigurationProvider>建立了ConfigurationRoot。ConfigurationRoot實現了IConfigurationRoot接口。
先看第一個過程,依然以JsonConfigurationSource爲例,代碼以下:
public class JsonConfigurationSource : FileConfigurationSource { public override IConfigurationProvider Build(IConfigurationBuilder builder) { EnsureDefaults(builder); return new JsonConfigurationProvider(this); } }
JsonConfigurationSource會經過Build方法建立一個名爲JsonConfigurationProvider的對象。經過JsonConfigurationProvider的名字可知,它是針對JSON類型的,也就是意味着不一樣類型的IConfigurationSource建立的IConfigurationProvider類型也是不同的,對應圖18‑4中的IConfigurationSource,生成的IConfigurationProvider關係以下圖2。
圖2
系統中添加的多個數據源被轉換成了一個個對應的ConfigurationProvider,這些ConfigurationProvider組成了一個ConfigurationProvider的集合。
再看一下第二個過程,ConfigurationBuilder的Build方法的最後一句是return new ConfigurationRoot(providers),就是經過第一個過程建立的ConfigurationProvider的集合建立ConfigurationRoot。ConfigurationRoot代碼以下:
public class ConfigurationRoot : IConfigurationRoot { private IList<IConfigurationProvider> _providers; private ConfigurationReloadToken _changeToken = new ConfigurationReloadToken(); public ConfigurationRoot(IList<IConfigurationProvider> providers) { if (providers == null) { throw new ArgumentNullException(nameof(providers)); } _providers = providers; foreach (var p in providers) { p.Load(); ChangeToken.OnChange(() => p.GetReloadToken(), () => RaiseChanged()); } } //省略部分代碼 }
能夠看出,ConfigurationRoot的構造方法主要的做用就是將ConfigurationProvider的集合做爲本身的一個屬性的值,並遍歷這個集合,逐一調用這些ConfigurationProvider的Load方法,併爲ChangeToken的OnChange方法綁定數據源的改變通知和處理方法。
從圖18‑5可知,全部類型數據源最終建立的XXXConfigurationProvider都繼承自ConfigurationProvider,因此它們都有一個Load方法和一個IDictionary<string, string> 類型的Data 屬性,它們是整個配置系統的重要核心。Load方法用於數據源的數據的讀取與處理,而Data用於保存最終結果。經過逐一調用Provider的Load方法完成了整個配置系統的數據加載。
以JsonConfigurationProvider爲例,它繼承自FileConfigurationProvider,因此先看一下FileConfigurationProvider的代碼:
public abstract class FileConfigurationProvider : ConfigurationProvider { //省略部分代碼 private void Load(bool reload) { var file = Source.FileProvider?.GetFileInfo(Source.Path); if (file == null || !file.Exists) { //省略部分代碼 } else { if (reload) { Data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); } using (var stream = file.CreateReadStream()) { try { Load(stream); } catch (Exception e) { //省略部分代碼 } } } OnReload(); } public override void Load() { Load(reload: false); } public abstract void Load(Stream stream); }
本段代碼的主要功能就是讀取文件生成stream,而後調用Load(stream)方法解析文件內容。從圖18‑5可知,JsonConfigurationProvider、IniConfigurationProvider、XmlConfigurationProvider都是繼承自FileConfigurationProvider,而對應JSON、INI、XML三種數據源來講,只是文件內容的格式不一樣,因此將通用的讀取文件內容的功能交給了FileConfigurationProvider來完成,而這三個子類的ConfigurationProvider只須要將FileConfigurationProvider讀取到的文件內容的解析便可。因此這個參數爲stream 的Load方法寫在JsonConfigurationProvider、IniConfigurationProvider、XmlConfigurationProvider這樣的子類中,用於專門處理自身對應的格式的文件。
JsonConfigurationProvider代碼以下:
public class JsonConfigurationProvider : FileConfigurationProvider { public JsonConfigurationProvider(JsonConfigurationSource source) : base(source) { } public override void Load(Stream stream) { try { Data = JsonConfigurationFileParser.Parse(stream); } catch (JsonReaderException e) { string errorLine = string.Empty; if (stream.CanSeek) { stream.Seek(0, SeekOrigin.Begin); IEnumerable<string> fileContent; using (var streamReader = new StreamReader(stream)) { fileContent = ReadLines(streamReader); errorLine = RetrieveErrorContext(e, fileContent); } } throw new FormatException(Resources.FormatError_JSONParseError(e.LineNumber, errorLine), e); } } //省略部分代碼 }
JsonConfigurationProvider中關於JSON文件的解析由JsonConfigurationFileParser.Parse(stream)完成的。最終的解析結果被賦值給了父類ConfigurationProvider的名爲Data的屬性中。
因此最終每一個數據源的內容都分別被解析成了IDictionary<string, string>集合,這個集合做爲對應的ConfigurationProvider的一個屬性。而衆多ConfigurationProvider組成的集合又做爲ConfigurationRoot的屬性。最終它們的關係圖以下圖3:
圖3
到此,配置的加載與數據的轉換工做完成。下圖4展現了這個過程。
圖4
第一節的例子中,經過_configuration["Theme:Color"]的方式獲取到了對應的配置值,這是如何實現的呢?如今咱們已經瞭解了數據源的加載過程,而這個_configuration就是數據源被加載後的最終產出物,即ConfigurationRoot,見圖18‑7。它的代碼以下:
public class ConfigurationRoot : IConfigurationRoot { private IList<IConfigurationProvider> _providers; private ConfigurationReloadToken _changeToken = new ConfigurationReloadToken(); //省略了上文已講過的構造方法 public IEnumerable<IConfigurationProvider> Providers => _providers; public string this[string key] { get { foreach (var provider in _providers.Reverse()) { string value; if (provider.TryGet(key, out value)) { return value; } } return null; } set { if (!_providers.Any()) { throw new InvalidOperationException(Resources.Error_NoSources); } foreach (var provider in _providers) { provider.Set(key, value); } } } public IEnumerable<IConfigurationSection> GetChildren() => GetChildrenImplementation(null); internal IEnumerable<IConfigurationSection> GetChildrenImplementation(string path) { return _providers .Aggregate(Enumerable.Empty<string>(), (seed, source) => source.GetChildKeys(seed, path)) .Distinct() .Select(key => GetSection(path == null ? key : ConfigurationPath.Combine(path, key))); } public IChangeToken GetReloadToken() => _changeToken; public IConfigurationSection GetSection(string key) => new ConfigurationSection(this, key); public void Reload() { foreach (var provider in _providers) { provider.Load(); } RaiseChanged(); } private void RaiseChanged() { var previousToken = Interlocked.Exchange(ref _changeToken, new ConfigurationReloadToken()); previousToken.OnReload(); } }
對應_configuration["Theme:Color"]的讀取方式的是索引器「string this[string key]」,經過查看其get方法可知,它是經過倒序遍歷全部ConfigurationProvider,在ConfigurationProvider的Data中嘗試查找是否存在Key爲"Theme:Color"的值。這也說明了第一節的例子中,在Theme.json中設置了Theme對象的值後,本來在appsettings.json設置的Theme的值被覆蓋的緣由。從圖18‑6中能夠看到,該值其實也是被讀取並加載的,只是因爲ConfigurationRoot的「倒序」遍歷ConfigurationProvider的方式致使後註冊的Theme.json中的Theme值先被查找到了。同時驗證了全部配置值均認爲是string類型的約定。
ConfigurationRoot還有一個GetSection方法,會返回一個IConfigurationSection對象,對應的是ConfigurationSection類。它的代碼以下:
public class ConfigurationSection : IConfigurationSection { private readonly ConfigurationRoot _root; private readonly string _path; private string _key; public ConfigurationSection(ConfigurationRoot root, string path) { if (root == null) { throw new ArgumentNullException(nameof(root)); } if (path == null) { throw new ArgumentNullException(nameof(path)); } _root = root; _path = path; } public string Path => _path; public string Key { get { if (_key == null) { // Key is calculated lazily as last portion of Path _key = ConfigurationPath.GetSectionKey(_path); } return _key; } } public string Value { get { return _root[Path]; } set { _root[Path] = value; } } public string this[string key] { get { return _root[ConfigurationPath.Combine(Path, key)]; } set { _root[ConfigurationPath.Combine(Path, key)] = value; } } public IConfigurationSection GetSection(string key) => _root.GetSection(ConfigurationPath.Combine(Path, key)); public IEnumerable<IConfigurationSection> GetChildren() => _root.GetChildrenImplementation(Path); public IChangeToken GetReloadToken() => _root.GetReloadToken(); }
它的代碼很簡單,能夠說沒有什麼實質的代碼,它只是保存了當前路徑和對ConfigurationRoot的引用。它的方法大可能是經過調用ConfigurationRoot的對應方法完成的,經過它自身的路徑計算在ConfigurationRoot中對應的Key,從而獲取對應的值。而ConfigurationRoot對配置值的讀取功能以及數據源的從新加載功能(Reload方法)也是經過ConfigurationProvider實現的,實際數據也是保存在ConfigurationProvider的Data值中。因此ConfigurationRoot和ConfigurationSection就像一個外殼,自身並不負責數據源的加載(或重載)與存儲,只負責構建了一個配置值的讀取功能。
而因爲配置值的讀取是按照數據源加載順序的倒序進行的,因此對於Key值相同的多個配置,只會讀取後加載的數據源中的配置,那麼ConfigurationRoot和ConfigurationSection就模擬出了一個樹狀結構,以下圖5:
圖5
本圖是以以下配置爲例:
{ "Theme": { "Name": "Blue", "Color": "#0921DC" } }
ConfigurationRoot利用它制定的讀取規則,將這樣的配置模擬成了如圖18‑8這樣的樹,它有這樣的特性:
A.全部節點都認爲是一個ConfigurationSection,不一樣的是對於「Theme」這樣的節點的值爲空(圖中用空心橢圓表示),而「Name」和「Color」這樣的節點有對應的值(圖中用實心橢圓表示)。
B.因爲對Key值相同的多個配置只會讀取後加載的數據源中的配置,因此不會出現相同路徑的同名節點。例如第一節例子中多種數據源配置了「Theme」值,在這裏只會體現最後加載的配置項。
因爲ConfigurationRoot未實際保存數據源中加載的配置值,因此配置的更新實際仍是由對應的ConfigurationProvider來完成。以JsonConfigurationProvider、IniConfigurationProvider、XmlConfigurationProvider爲例,它們的數據源都是具體文件,因此對文件內容的改變的監控也是放在FileConfigurationProvider中。FileConfigurationProvider的構造方法中添加了對設置了對應文件的監控,固然這裏會首先判斷數據源的ReloadOnChange選項是否被設置爲True了。
public abstract class FileConfigurationProvider : ConfigurationProvider { public FileConfigurationProvider(FileConfigurationSource source) { if (source == null) { throw new ArgumentNullException(nameof(source)); } Source = source; if (Source.ReloadOnChange && Source.FileProvider != null) { changeToken.OnChange( () => Source.FileProvider.Watch(Source.Path), () => { Thread.Sleep(Source.ReloadDelay); Load(reload: true); }); } } //省略其餘代碼 }
因此當數據源發生改變而且ReloadOnChange被設置爲True的時候,對應的ConfigurationProvider就會從新加載數據。但ConfigurationProvider更新數據源也不會改變它在ConfigurationRoot的IEnumerable<IConfigurationProvider>列表中的順序。若是在列表中存在A和B兩個ConfigurationProvider而且含有相同的配置項,B排在A後面,那麼對於這些相同的配置項來講,A中的是被B中的「覆蓋」的。即便A的數據更新了,它依然處於「被覆蓋」的位置,應用中讀取相應配置項的依然是讀取B中的配置項。
在第一節的例子中講過了兩種獲取配置值的方式,相似這樣_configuration["Theme:Name"]和_configuration.GetValue<string>("Theme:Color","#000000")能夠獲取到Theme的Name和Color的值,那麼就會有下面這樣的疑問:
appsettings.json中存在以下這樣的配置
{ "Theme": { "Name": "Blue", "Color": "#0921DC" } }
新建一個Theme類以下:
public class Theme { public string Name { get; set; } public string Color { get; set; } }
是否能夠將配置值獲取並賦值到這樣的一個Theme的實例中呢?
固然能夠,系統提供了這樣的功能,能夠採用以下代碼實現:
Theme theme = new Theme(); _configuration.GetSection("Theme").Bind(theme);
綁定功能由ConfigurationBinder實現,邏輯不復雜,讀者若是感興趣的可自行查看其代碼。
原文出處:https://www.cnblogs.com/FlyLolo/p/ASPNETCore_23.html