1. 程式人生 > >淺析Asp.Net Core框架IConfiguration配置

淺析Asp.Net Core框架IConfiguration配置

### 目錄 * 一、建造者模式(Builder Pattern) * 二、核心介面與配置儲存本質 * 三、簡易QueryString配置源實現 * 四、宿主配置與應用配置 * 五、檔案配置源配置更新原理 ### 一、建造者模式 為什麼提建造者模式?在閱讀.NET Core原始碼時,時常碰到IHostBuilder,IConfigurationBuilder,ILoggerBuilder等諸如此類帶Builder名稱的類/介面,起初專研時那是一頭愣。知識不夠,勤奮來湊,在瞭解到Builder模式後終於理解,明白這些Builder類是用來構建相對應類的物件,用完即毀別無他用。理解建造者模式,有助於閱讀原始碼時發現核心介面/類,將檔案分類,直指堡壘。詳細建造者模式可參閱此篇文章:[磁懸浮快線](https://zhuanlan.zhihu.com/p/58093669) ### 二、核心介面與配置儲存本質 在.NET Core中讀取配置是通過IConfiguration介面,它存在於[Microsoft.Extensions.Configuration.Abstractions](https://source.dot.net/#Microsoft.Extensions.Configuration.Abstractions/IConfiguration.cs)專案中,如下圖: ![Microsoft.Extensions.Configuration.Abstractions](https://img2020.cnblogs.com/blog/574719/202101/574719-20210126125704157-1299144268.png) > IConfiguration:配置訪問介面 > IConfigurationProvider:配置提供者介面 > IConfigurationSource:配置源介面 > IConfigurationRoot:配置根介面,繼承IConfiguration,維護著IConfigurationProvider集合及重新載入配置 > IConfigurationBuilder:IConfigurationRoot介面例項的構造者介面 **1.服務容器中IConfiguration例項註冊(ConfigurationRoot)** ``` /// /// Represents the root of an hierarchy. =>
配置根路徑 ///
public interface IConfigurationRoot : IConfiguration { /// /// Force the configuration values to be reloaded from the underlying s. => 從配置源重新載入配置 /// void Reload(); /// /// The s for this configuration. => 依賴的配置源集合 /// IEnumerable Providers { get; } } ``` IConfigurationRoot(繼承IConfiguration)維護著一個IConfigurationProvider集合列表,也就是我們的配置源。IConfiguration例項的建立並非通過new()方式,而是由IConfigurationBuilder來構建,實現了按需載入配置源,是建造者模式的充分體現。IConfigurationBuilder上的所有操作如: ``` HostBuilder.ConfigureAppConfiguration((context, builder) => { builder.AddCommandLine(args); // 命令列配置源 builder.AddEnvironmentVariables(); // 環境配置源 builder.AddJsonFile("demo.json"); // json檔案配置源 builder.AddInMemoryCollection(); // 記憶體配置源 // ... }) ``` 皆是為IConfigurationRoot.Providers做準備,最後通過Build()方法生成ConfigurationRoot例項註冊到服務容器 ``` public class HostBuilder : IHostBuilder { private HostBuilderContext _hostBuilderContext; // 配置構建 委託 private List> _configureAppConfigActions = new List>(); private IConfiguration _appConfiguration; private void BuildAppConfiguration() { IConfigurationBuilder configBuilder = new ConfigurationBuilder(); foreach (Action buildAction in _configureAppConfigActions) { buildAction(_hostBuilderContext, configBuilder); } _appConfiguration = configBuilder.Build(); // 呼叫Build()建立IConfiguration 例項 ConfigurationRoot _hostBuilderContext.Configuration = _appConfiguration; } private void CreateServiceProvider() { var services = new ServiceCollection(); // register configuration as factory to make it dispose with the service provider services.AddSingleton(_ => _appConfiguration); // 註冊 IConfiguration - 單例 } } ``` **2.IConfiguration/IConfigurationSection讀取配置與配置儲存本質** 程式中我們會通過如下方式獲取配置值(當然還有繫結IOptions) > IConfiguration["key"] > IConfiguration.GetSection("key").Value > ... 而IConfiguration註冊的例項是ConfigurationRoot,程式碼如下,其索引器實現竟是倒序遍歷配置源,獲取配置值。原來當我們通過IConfiguration獲取配置時,其實就是倒序遍歷IConfigurationBuilder載入進來的配置源。 ``` public class ConfigurationRoot : IConfigurationRoot, IDisposable { private readonly IList _providers; public IEnumerable Providers => _providers; public string this[string key] { get { // 倒序遍歷配置源,獲取到配置 就返回,這也是配置覆蓋的根本原因,後新增的相同配置會覆蓋前面的 for (int i = _providers.Count - 1; i >= 0; i--) { IConfigurationProvider provider = _providers[i]; if (provider.TryGet(key, out string value)) { return value; } } return null; } } } ``` 那麼配置資料是以什麼形式儲存的呢?在[Microsoft.Extensions.Configuration](https://source.dot.net/#Microsoft.Extensions.Configuration/ConfigurationProvider.cs,5c6e786dde478171)專案中,提供了一個IConfigurationProvider預設實現儲存抽象類ConfigurationProvider,部分程式碼如下 ``` /// /// Base helper class for implementing an
///
public abstract class ConfigurationProvider : IConfigurationProvider { protected ConfigurationProvider() { Data = new Dictionary(StringComparer.OrdinalIgnoreCase); } /// /// The configuration key value pairs for this provider. /// protected IDictionary Data { get; set; } public virtual bool TryGet(string key, out string value) => Data.TryGetValue(key, out value); /// /// 虛方法,供具體配置源重寫,載入配置到 Data中 ///
public virtual void Load() { } } ``` 從上可知,所有載入到程式中的配置源,其本質還是儲存在Provider裡面一個型別為IDictionary Data屬性中。由此推論: **當通過IConfiguration獲取配置時,就是通過各個Provider的Data讀取!** ### 三、簡易QueryString配置源實現 要實現自定義的配置源,只需實現IConfigurationProvider,IConfigurationSource兩個介面即可,這裡通過一個QueryString格式的簡易配置來演示。[蟲洞隧道](https://files.cnblogs.com/files/GodX/Microsoft.Extensions.Configuration.QueryString.rar) ![](https://img2020.cnblogs.com/blog/574719/202101/574719-20210127101107904-444258795.png) **1.queryString.config資料格式如下** > server=localhost&port=3306&datasource=demo&user=root&password=123456&charset=utf8mb4 **2.實現IConfigurationSource介面(QueryStringConfiguationSource)** ``` public class QueryStringConfiguationSource : IConfigurationSource { public QueryStringConfiguationSource(string path) { Path = path; } /// /// QueryString檔案相對路徑 /// public string Path { get; } public IConfigurationProvider Build(IConfigurationBuilder builder) { return new QueryStringConfigurationProvider(this); } } ``` **3.實現IConfigurationProvider介面(QueryStringConfiguationProvider)** ``` public class QueryStringConfigurationProvider : ConfigurationProvider { public QueryStringConfigurationProvider(QueryStringConfiguationSource source) { Source = source; } public QueryStringConfiguationSource Source { get; } /// /// 重寫Load方法,將自定義的配置解析到 Data 中 /// public override void Load() { // server=localhost&port=3306&datasource=demo&user=root&password=123456&charset=utf8mb4 例子格式 string queryString = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, Source.Path)); string[] arrays = queryString.Split(new[] { "&" }, StringSplitOptions.RemoveEmptyEntries); // & 號分隔 foreach (var item in arrays) { string[] temps = item.Split(new[] { "=" }, StringSplitOptions.RemoveEmptyEntries); // = 號分隔 if (temps.Length != 2) continue; Data.Add(temps[0], temps[1]); } } } ``` **4.IConfigurationBuilder配置源構建** ``` public static class QueryStringConfigurationExtensions { /// /// 預設檔名稱 queryString.config /// /// /// public static IConfigurationBuilder AddQueryStringFile(this IConfigurationBuilder builder) => AddQueryStringFile(builder, "queryString.config"); public static IConfigurationBuilder AddQueryStringFile(this IConfigurationBuilder builder, string path) => builder.Add(new QueryStringConfiguationSource(path)); } ``` **5.Program載入配置源** ``` public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureAppConfiguration(builder => { // 載入QueryString配置源 builder.AddQueryStringFile(); //builder.AddQueryStringFile("queryString.config"); }) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); ``` 至此,自定義QueryString配置源實現完成,便可通過IConfiguration介面獲取值,結果如下 > IConfiguration["server"] => localhost > IConfiguration["datasource"] => demo > IConfiguration["charset"] => utf8mb4 > ... ### 四、宿主配置與應用配置 .NET Core官方已預設提供了:環境變數、命令列引數,Json、Ini等配置源,不過適用場景卻應有不同。不妨可分為兩類:一類是宿主配置源,一類是應用配置源 **1.宿主配置源** 宿主配置源:供IHost宿主啟動時使用的配置源。環境變數、命令列引數就可歸為這類,以[IHostEnvironment](https://source.dot.net/#Microsoft.Extensions.Hosting.Abstractions)為例 ``` /// /// 提供執行環境相關資訊 /// public interface IHostEnvironment { string EnvironmentName { get; set; } string ApplicationName { get; set; } string ContentRootPath { get; set; } } ``` IHostEnvironment介面提供了當前應用執行環境相關資訊,可以通過IsEnvironment()方法判斷當前執行環境是Development還是Production、Staging。 ``` public static bool IsEnvironment(this IHostEnvironment hostEnvironment, string environmentName) { if (hostEnvironment == null) { throw new ArgumentNullException(nameof(hostEnvironment)); } return string.Equals(hostEnvironment.EnvironmentName, environmentName, StringComparison.OrdinalIgnoreCase); } ``` hostEnvironment.EnvironmentName是什麼?這就得益於它註冊到服務容器時所賦的值:[HostBuilder](https://source.dot.net/#Microsoft.Extensions.Hosting/HostBuilder.cs) ``` public class HostBuilder:IHostBuilder { private void CreateHostingEnvironment() { _hostingEnvironment = new HostingEnvironment() { ApplicationName = _hostConfiguration[HostDefaults.ApplicationKey], // _hostConfiguration 型別是 IConfiguration EnvironmentName = _hostConfiguration[HostDefaults.EnvironmentKey] ?? Environments.Production, // 當未配置環境時,預設Production環境,在使用vs開發啟動時,lanuchSetting.json 配置了 環境變數:"ASPNETCORE_ENVIRONMENT": "Development" ContentRootPath = ResolveContentRootPath(_hostConfiguration[HostDefaults.ContentRootKey], AppContext.BaseDirectory), }; if (string.IsNullOrEmpty(_hostingEnvironment.ApplicationName)) { // Note GetEntryAssembly returns null for the net4x console test runner. _hostingEnvironment.ApplicationName = Assembly.GetEntryAssembly()?.GetName().Name; } } } ``` 由此可見,IHostEnvironment所提供的資訊根由仍是從IConfiguration讀取,而這些配置正是來自環境變數、命令列引數配置源。 **2.應用配置源** 應用配置源:供應用業務邏輯使用的配置源。Json、Ini、Xml以及自定義的QueryString等就可歸為類。 ### 五、檔案配置源配置更新原理 對於檔案配置源,.NET Core預設提供了兩個抽象類:[FileConfigurationSource](https://source.dot.net/#Microsoft.Extensions.Configuration.FileExtensions) 和 [FileConfigurationProvider](https://source.dot.net/#Microsoft.Extensions.Configuration.FileExtensions) ``` public abstract class FileConfigurationProvider : ConfigurationProvider, IDisposable { private readonly IDisposable _changeTokenRegistration; public FileConfigurationProvider(FileConfigurationSource source) { if (source == null) { throw new ArgumentNullException(nameof(source)); } Source = source; if (Source.ReloadOnChange && Source.FileProvider != null) { _changeTokenRegistration = ChangeToken.OnChange( // 檔案改變,重新載入配置 () => Source.FileProvider.Watch(Source.Path), () => { Thread.Sleep(Source.ReloadDelay); Load(reload: true); }); } } /// /// The source settings for this provider. /// public FileConfigurationSource Source { get; } private void Load(bool reload) { IFileInfo file = Source.FileProvider?.GetFileInfo(Source.Path); if (file == null || !file.Exists) { if (Source.Optional || reload) // Always optional on reload { Data = new Dictionary(StringComparer.OrdinalIgnoreCase); // Data 被重新建立新的例項賦值了 } else { var error = new StringBuilder($"The configuration file '{Source.Path}' was not found and is not optional."); if (!string.IsNullOrEmpty(file?.PhysicalPath)) { error.Append($" The physical path is '{file.PhysicalPath}'."); } HandleException(ExceptionDispatchInfo.Capture(new FileNotFoundException(error.ToString()))); } } else { // Always create new Data on reload to drop old keys if (reload) { Data = new Dictionary(StringComparer.OrdinalIgnoreCase); // Data 被重新建立新的例項賦值了 } static Stream OpenRead(IFileInfo fileInfo) { if (fileInfo.PhysicalPath != null) { // The default physical file info assumes asynchronous IO which results in unnecessary overhead // especally since the configuration system is synchronous. This uses the same settings // and disables async IO. return new FileStream( fileInfo.PhysicalPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, bufferSize: 1, FileOptions.SequentialScan); } return fileInfo.CreateReadStream(); } using Stream stream = OpenRead(file); try { Load(stream); } catch (Exception e) { HandleException(ExceptionDispatchInfo.Capture(e)); } } } public override void Load() { Load(reload: false); } public abstract void Load(Stream stream); } ``` 所有基於檔案配置源(如果要監控配置檔案更新,如:appsetting.json)都應實現這個兩個抽象類,儘管不懂ChangeToken是個什麼東東,只需明白Provider.Data 在檔案變更時被重新賦值也未嘗