淺析Asp.Net Core框架IConfiguration配置
阿新 • • 發佈:2021-01-27
### 目錄
* 一、建造者模式(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 在檔案變更時被重新賦值也未嘗