爲何提建造者模式?在閱讀.NET Core源碼時,時常碰到IHostBuilder,IConfigurationBuilder,ILoggerBuilder等諸如此類帶Builder名稱的類/接口,起初專研時那是一頭愣。知識不夠,勤奮來湊,在瞭解到Builder模式後終於理解,明白這些Builder類是用來構建相對應類的對象,用完即毀別無他用。理解建造者模式,有助於閱讀源碼時發現核心接口/類,將文件分類,直指堡壘。詳細建造者模式可參閱此篇文章:磁懸浮快線web
在.NET Core中讀取配置是經過IConfiguration接口,它存在於Microsoft.Extensions.Configuration.Abstractions項目中,以下圖:
json
IConfiguration:配置訪問接口
IConfigurationProvider:配置提供者接口
IConfigurationSource:配置源接口
IConfigurationRoot:配置根接口,繼承IConfiguration,維護着IConfigurationProvider集合及從新加載配置
IConfigurationBuilder:IConfigurationRoot接口實例的構造者接口app
1.服務容器中IConfiguration實例註冊(ConfigurationRoot)async
/// <summary> /// Represents the root of an <see cref="IConfiguration"/> hierarchy. => 配置根路徑 /// </summary> public interface IConfigurationRoot : IConfiguration { /// <summary> /// Force the configuration values to be reloaded from the underlying <see cref="IConfigurationProvider"/>s. => 從配置源從新加載配置 /// </summary> void Reload(); /// <summary> /// The <see cref="IConfigurationProvider"/>s for this configuration. => 依賴的配置源集合 /// </summary> IEnumerable<IConfigurationProvider> Providers { get; } }
IConfigurationRoot(繼承IConfiguration)維護着一個IConfigurationProvider集合列表,也就是咱們的配置源。IConfiguration實例的建立並不是經過new()方式,而是由IConfigurationBuilder來構建,實現了按需加載配置源,是建造者模式的充分體現。IConfigurationBuilder上的全部操做如:ide
HostBuilder.ConfigureAppConfiguration((context, builder) => { builder.AddCommandLine(args); // 命令行配置源 builder.AddEnvironmentVariables(); // 環境配置源 builder.AddJsonFile("demo.json"); // json文件配置源 builder.AddInMemoryCollection(); // 內存配置源 // ... })
皆是爲IConfigurationRoot.Providers作準備,最後經過Build()方法生成ConfigurationRoot實例註冊到服務容器ui
public class HostBuilder : IHostBuilder { private HostBuilderContext _hostBuilderContext; // 配置構建 委託 private List<Action<HostBuilderContext, IConfigurationBuilder>> _configureAppConfigActions = new List<Action<HostBuilderContext, IConfigurationBuilder>>(); private IConfiguration _appConfiguration; private void BuildAppConfiguration() { IConfigurationBuilder configBuilder = new ConfigurationBuilder(); foreach (Action<HostBuilderContext, IConfigurationBuilder> 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)this
IConfiguration["key"]
IConfiguration.GetSection("key").Value
...spa
而IConfiguration註冊的實例是ConfigurationRoot,代碼以下,其索引器實現竟是倒序遍歷配置源,獲取配置值。原來當咱們經過IConfiguration獲取配置時,其實就是倒序遍歷IConfigurationBuilder加載進來的配置源。.net
public class ConfigurationRoot : IConfigurationRoot, IDisposable { private readonly IList<IConfigurationProvider> _providers; public IEnumerable<IConfigurationProvider> 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項目中,提供了一個IConfigurationProvider默認實現存儲抽象類ConfigurationProvider,部分代碼以下命令行
/// <summary> /// Base helper class for implementing an <see cref="IConfigurationProvider"/> /// </summary> public abstract class ConfigurationProvider : IConfigurationProvider { protected ConfigurationProvider() { Data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); } /// <summary> /// The configuration key value pairs for this provider. /// </summary> protected IDictionary<string, string> Data { get; set; } public virtual bool TryGet(string key, out string value) => Data.TryGetValue(key, out value); /// <summary> /// 虛方法,供具體配置源重寫,加載配置到 Data中 /// </summary> public virtual void Load() { } }
從上可知,全部加載到程序中的配置源,其本質仍是存儲在Provider裏面一個類型爲IDictionary<string, string> Data屬性中。由此推論: 當經過IConfiguration獲取配置時,就是經過各個Provider的Data讀取!
要實現自定義的配置源,只需實現IConfigurationProvider,IConfigurationSource兩個接口便可,這裏經過一個QueryString格式的簡易配置來演示。蟲洞隧道
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; } /// <summary> /// QueryString文件相對路徑 /// </summary> 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; } /// <summary> /// 重寫Load方法,將自定義的配置解析到 Data 中 /// </summary> 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 { /// <summary> /// 默認文件名稱 queryString.config /// </summary> /// <param name="builder"></param> /// <returns></returns> 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<Startup>(); });
至此,自定義QueryString配置源實現完成,即可經過IConfiguration接口獲取值,結果以下
IConfiguration["server"] => localhost
IConfiguration["datasource"] => demo
IConfiguration["charset"] => utf8mb4
...
.NET Core官方已默認提供了:環境變量、命令行參數,Json、Ini等配置源,不過適用場景卻應有不一樣。不妨可分爲兩類:一類是宿主配置源,一類是應用配置源
1.宿主配置源
宿主配置源:供IHost宿主啓動時使用的配置源。環境變量、命令行參數就可歸爲這類,以IHostEnvironment爲例
/// <summary> /// 提供運行環境相關信息 /// </summary> 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
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 和 FileConfigurationProvider
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); }); } } /// <summary> /// The source settings for this provider. /// </summary> 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<string, string>(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<string, string>(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 在文件變動時被從新賦值也何嘗不可。