說到配置,絕大部分系統都會有配置,不需要配置的系統是非常少的,想想以前做.net 開發時,我們常常將配置放到web.config中,然后使用ConfigurationManager去讀取。
初次接觸到.net core 的同學,在項目中看到有一個appsettings.json文件,確實這個appsettings.json文件是做配置用的,所以想當然的把它看做.net 開發中的web.config一樣,但是我們要清除,.net core並不依賴appsettings.json文件中的配置。
.net core 提供了一種非常靈活的配置方式,大部分時候,我們只需要關注DI容器中的IConfiguration接口實例對象就可以了,下面具體介紹。
這里介紹的.net core版本是3.1,源碼地址:https://github.com/dotnet/extensions/tree/v3.1.12/src/Configuration
一、原理
要介紹原理,先看與配置相關的幾個接口及它們的實現類:
IConfigurationBuilder
配置建造者接口,我們使用它去創建配置對象,有一個實現類:ConfigurationBuilder
IConfiguration
表示配置集合的接口,一般的,程序通過從DI獲取IConfiguration接口的實例來獲取配置
IConfigurationRoot
IConfiguration的子接口,表示配置的根節點,換句話說,IConfigurationBuilder創建的第一個配置對象就是IConfigurationRoot接口對象,它的實現類是:ConfigurationRoot
IConfigurationSection
IConfiguration的子接口,表示配置的一個節點,包含節點名、節點路徑、值等等,配置節點分隔默認是冒號(:),它的實現類是:ConfigurationSection
IConfigurationSource
配置來源接口,IConfigurationSource接口的實現類都很簡單,主要用於結合Options創建配置提供者IConfigurationProvider,一般的,它的作用可以認為就是接收參數,然后在創建IConfigurationProvider時將參數傳進去。
但是在讀取來自文件的配置時,推薦繼承抽象類:FileConfigurationSource ,其它的就直接實現 IConfigurationSource 就可以了,然后添加到 IConfigurationBuilder 的配置源中去。
IConfigurationProvider
配置信息的具體提供者,這個就是提供配置的獲取、更新等等操作的接口,有兩個重要的抽象類:ConfigurationProvider 和 FileConfigurationProvider
一般的,如果我們需要集成自己的配置,需要實現這個 IConfigurationSource 接口和 IConfigurationProvider 接口,如果我們的配置和文件有關,建議通過繼承 FileConfigurationSource 兩個 FileConfigurationProvider 兩個抽象類來實現 IConfigurationSource 和 IConfigurationProvider接口,因為這兩個抽象類已經提供了一些我們可能需要的功能,比如,它們可以監聽文件狀態,如果文件內容被修改,則可以重新加載配置。如果配置不來自文件,配置來源可以直接實現 IConfigurationSource 接口,而通過繼承 ConfigurationProvider 來實現 IConfigurationProvider 接口。
於是乎,將它們串接起來,流程就是這樣的:
1、提供一個實現了 IConfigurationProvider 接口的配置提供類,它需要提供配置的讀取以及更新等操作
2、提供一個 IConfigurationSource 接口實現類,它負責創建 IConfigurationProvider 。
3、創建一個 IConfigurationBuilder 配置建造者對象,然后將 IConfigurationSource 添加進配置構造者中,這里我們一般都采用 IConfigurationBuilder 的拓展方法來實現。
4、使用 IConfigurationBuilder 構造一個 IConfigurationRoot ,然后使用這個 IConfigurationRoot 去操作配置。
這是一般流程,而.net core的配置是一個拓展模塊,也就是說我們可以在控制台等其他項目中引用,只需要安裝包:Microsoft.Extensions.Configuration
為了更好的說明,我們可以先看IConfiguration在WebHost中是怎么集成的,已.net core 3.1為例,它的Program是這樣的:
public class Program { public static void Main(string[] args) { CreateHostBuilder(args).Build().Run(); } public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); }); }
看看Host.CreateDefaultBuilder()方法(源碼),源碼是這樣的:
public static IHostBuilder CreateDefaultBuilder(string[] args) { ... builder.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() && !string.IsNullOrEmpty(env.ApplicationName)) { var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName)); if (appAssembly != null) { config.AddUserSecrets(appAssembly, optional: true); } } config.AddEnvironmentVariables(); if (args != null) { config.AddCommandLine(args); } }) ... }
現在可以看出為什么appsettings.json是默認的配置文件了,ConfigureAppConfiguration方法就是對配置的構造過程,這里默認最多會加載5個配置源(也就是上面config.AddXXXXX()部分,后面具體介紹)。
而ConfigureAppConfiguration的實現就是將傳進去的委托保存(源碼):
public IHostBuilder ConfigureAppConfiguration(Action<HostBuilderContext, IConfigurationBuilder> configureDelegate) { _configureAppConfigActions.Add(configureDelegate ?? throw new ArgumentNullException(nameof(configureDelegate))); return this; }
這里保存是將委托放到一個List中,也就是說ConfigureAppConfiguration方法可以多次調用,我們可以添加我們自己的配置了,在Build時就會按順序來調用(源碼):
public IHost Build() { ... BuildAppConfiguration(); ... }
而BuildAppConfiguration方法則是最終構造配置的過程(源碼):
private void BuildAppConfiguration() { var configBuilder = new ConfigurationBuilder() .SetBasePath(_hostingEnvironment.ContentRootPath) .AddConfiguration(_hostConfiguration, shouldDisposeConfiguration: true); foreach (var buildAction in _configureAppConfigActions) { buildAction(_hostBuilderContext, configBuilder); } _appConfiguration = configBuilder.Build(); _hostBuilderContext.Configuration = _appConfiguration; }
可以看到.net core內部也是直接實例化一個ConfigurationBuilder來構造配置的,而它的Build方法則返回的是一個 IConfigurationRoot 接口對象(源碼),剩下的就是使用 IConfigurationRoot 接口對象來讀取更新配置了。
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); }
二、內置的配置方式
官方在配置方法,提供了一些默認的配置源,它們都是通過IConfigurationBuilder的拓展方法來集成配置源,這也很好的給我們展示了如何添加自己的配置源。
官方默認提供的配置源有:
Json文件
NuGet安裝包:Microsoft.Extensions.Configuration.Json
通過 IConfigurationBuilder的AddJsonFile和AddJsonStream兩個拓展方法來集成(源碼),有多個重載,各參數的含義如下:
provider:提供json文件的一些信息及功能操作,比如所在結構目錄,監聽文件狀態等等,默認默認值:new PhysicalFileProvider(AppContext.BaseDirectory ?? string.Empty)
path:json文件路徑
optional:表示json文件是否是可選的,如果未false,那么當json文件不存在時則會拋出異常
reloadOnChange:表示是否在文件內容修改后重新加載配置,如果未false,表示不重新加載
stream:json文件流
比如有一個json文件(注意文件位置):
{ "Hello": { "Microsoft": { "Extensions": "Configuration" } } }
我們讀取是這樣的:
static void Main(string[] args) { ConfigurationBuilder builder = new ConfigurationBuilder(); builder.AddJsonFile("configuration.json"); var configuration = builder.Build(); var collections = configuration.AsEnumerable(); foreach (var item in collections) { Console.WriteLine("{0}={1}", item.Key, item.Value); } }
結果:

Ini文件
NuGet安裝包:Microsoft.Extensions.Configuration.Ini
通過 IConfigurationBuilder的AddIniFile和AddIniStream兩個拓展方法來集成(源碼),有多個重載,各參數的含義同上Json文件。
比如我們有一個ini文件(注意文件位置):
[SessionName1] KeyName11=value11 KeyName12=value12 [Section2Name] KeyName21=value21 KeyName22=value22
我們讀取是這樣的:
static void Main(string[] args) { ConfigurationBuilder builder = new ConfigurationBuilder(); builder.AddIniFile("iniFile.ini"); var configuration = builder.Build(); var collections = configuration.AsEnumerable(); foreach (var item in collections) { Console.WriteLine("{0}={1}", item.Key, item.Value); } }
結果:

Xml文件
NuGet安裝包:Microsoft.Extensions.Configuration.Xml
通過 IConfigurationBuilder的AddXmlFile和AddXmlStream兩個拓展方法來集成(源碼),有多個重載,各參數的含義同上Json文件(這也是在告訴我們,如果我們要從其他文件添加,只需要類似這些參數就可以了)。
比如我們有一個xml文件(注意文件位置):
<?xml version="1.0" encoding="utf-8" ?> <node1> <node2>value2</node2> <node3> <node4>value4</node4> <node5>value5</node5> </node3> </node1>
我們讀取是這樣的:
static void Main(string[] args) { ConfigurationBuilder builder = new ConfigurationBuilder(); builder.AddXmlFile("xmlFile.xml"); var configuration = builder.Build(); var collections = configuration.AsEnumerable(); foreach (var item in collections) { Console.WriteLine("{0}={1}", item.Key, item.Value); } }
結果:

注:配置的鍵不包含xml中的根路徑
命令行參數
NuGet安裝包:Microsoft.Extensions.Configuration.CommandLine
熟悉命令行的朋友知道,經常的,在執行一個命令時,可以攜帶一些參數,有些使用 - 或者 -- 符號,有些則沒有,同樣的,dotnet命令在執行可以也可以攜帶一些運行時參數,.net core可以將這些參數集成到IConfiguration中。
通過 IConfigurationBuilder的AddCommandLine拓展方法來集成(源碼),同樣有幾個重載,說明如下:
args:命令函參數
switchMappings:映射轉化,主要是將-開頭和--開頭的配置轉換成執行的鍵名
說明一下,參數必須滿足一下規則:
1、必須以 -、--、/ 作為前綴,其中 -- 與 / 等價
2、如果是以 - 為前綴的參數,則必須使用 switchMappings 做一層映射,否則將拋出 FormatException ,所以自定義的命令行參數建議采用 -- 作為前綴
3、參數名與參數值之間使用 = 或者空格分隔,建議使用 =
比如:
static void Main(string[] args) { //dotnet XXXX.dll -a=a --b=b /c=c --e 1 /d 2 var mapper = new Dictionary<string, string>() { { "-a", "mapper-a" }, { "--b", "mapper-b" }, { "--c", "mapper-c" } }; ConfigurationBuilder builder = new ConfigurationBuilder(); builder.AddCommandLine(args, mapper); var configuration = builder.Build(); var collections = configuration.AsEnumerable(); foreach (var item in collections) { Console.WriteLine("{0}={1}", item.Key, item.Value); } }
如果執行命令行:
dotnet XXXX.dll -a=a --b=b /c=c --e 1 /d 2
結果是:

環境變量
NuGet安裝包:Microsoft.Extensions.Configuration.EnvironmentVariables
.net core允許我們將本地的環境變量集成到IConfiguration配置中,通過IConfigurationBuilder的AddEnvironmentVariables拓展方法來集成(源碼),同時可以指定一個前綴(拓展方法中的prefix參數),表示要加載在配置中的哪些環境變量的前綴,而不是全部環境變量。
例如:
static void Main(string[] args) { ConfigurationBuilder builder = new ConfigurationBuilder(); builder.AddEnvironmentVariables("Common"); var configuration = builder.Build(); var collections = configuration.AsEnumerable(); foreach (var item in collections) { Console.WriteLine("{0}={1}", item.Key, item.Value); } }
結果:

注:IConfiguration中的鍵值會去掉環境變量名中的前綴,不如這里我的環境變量名是CommonProgramFiles,但是IConfiguration中的配置名是ProgramFiles
IConfiguration配置
有時候,我們已經存在一個IConfiguration配置了,然后我們可以將它集成到另一個IConfiguration中去使用,通過IConfigurationBuilder的AddConfiguration拓展方法來集成(源碼)。
這個很簡單,只看個例子就可以了:
static void Main(string[] args) { //Json ConfigurationBuilder builder1 = new ConfigurationBuilder(); builder1.AddJsonFile("configuration.json"); var configuration1 = builder1.Build(); //Ini ConfigurationBuilder builder2 = new ConfigurationBuilder(); builder2.AddIniFile("iniFile.ini"); var configuration2 = builder2.Build(); //Xml ConfigurationBuilder builder3 = new ConfigurationBuilder(); builder3.AddXmlFile("xmlFile.xml"); var configuration3 = builder3.Build(); //EnvironmentVariables ConfigurationBuilder builder4 = new ConfigurationBuilder(); builder4.AddEnvironmentVariables("Common"); var configuration4 = builder4.Build(); ConfigurationBuilder builder = new ConfigurationBuilder(); builder.AddConfiguration(configuration1); builder.AddConfiguration(configuration2); builder.AddConfiguration(configuration3); builder.AddConfiguration(configuration4); var configuration = builder.Build(); var collections = configuration.AsEnumerable(); foreach (var item in collections) { Console.WriteLine("{0}={1}", item.Key, item.Value); } }
結果:

內存集合
有時候,我們配置源源自某個集合變量,這是我們同樣可以將它集成到IConfiguration中去,通過IConfigurationBuilder的AddInMemoryCollection拓展方法來集成(源碼)。
這個也很簡單,看例子就明白了了:
static void Main(string[] args) { Dictionary<string, string> dict = new Dictionary<string, string>(); dict["Key1"] = "Value1"; dict["Key2"] = "Value2"; dict["Key3"] = "Value3"; dict["Key4"] = "Value4"; ConfigurationBuilder builder = new ConfigurationBuilder(); builder.AddInMemoryCollection(dict); var configuration = builder.Build(); var collections = configuration.AsEnumerable(); foreach (var item in collections) { Console.WriteLine("{0}={1}", item.Key, item.Value); } }
結果:

文件目錄
NuGet安裝包:Microsoft.Extensions.Configuration.KeyPerFile
可能考慮到在某些情況下,有些軟件將產生的數據保存着獨立的文件中,采用文件名作為區分,因此作者提供了一個將這些數據文件內容作為配置的方法,采用文件名作為key,文件內容作為value。它采用AddKeyPerFile拓展方法集成(源碼)
需要注意的是,文件名中的雙下划線(__)作為配置節點分隔符。
比如,我們有一些文件(在屬性中輸出類型設置成始終復制):

代碼:
static void Main(string[] args) { string directory = Path.Combine(Directory.GetCurrentDirectory(), "files"); ConfigurationBuilder builder = new ConfigurationBuilder(); builder.AddKeyPerFile(directory, true); var configuration = builder.Build(); var collections = configuration.AsEnumerable(); foreach (var item in collections) { Console.WriteLine("{0}={1}", item.Key, item.Value); } }
結果:

用戶私密文件
NuGet安裝包:Microsoft.Extensions.Configuration.UserSecrets
通過IConfigurationBuilder的AddUserSecrets拓展方法來集成(源碼),它主要用來配置一些私密信息文件,這個很少使用,了解一下就行了。
在集成時,會涉及到一個userSecretsId,其實它的本意是一個獨一無二的目錄名,一般設置成GUID,我們有兩種方式使用它:
方式一:直接使用
builder.AddUserSecrets("userSecretsId");
方式二:通過 UserSecretsIdAttribute 特性
首先給某個程序集添加 UserSecretsIdAttribute 特性,比如我這里就是啟動項目設置:
[assembly: Microsoft.Extensions.Configuration.UserSecrets.UserSecretsId("userSecretsId")]
然后使用啟動項目的程序集去獲取:
builder.AddUserSecrets(typeof(Program).Assembly);
上面說到userSecretsId是目錄名,那是哪個目錄名呢?根據源碼,私密文件路徑使用 PathHelper.GetSecretsPathFromSecretsId()方法來獲取(源碼):
public static string GetSecretsPathFromSecretsId(string userSecretsId) { ... const string userSecretsFallbackDir = "DOTNET_USER_SECRETS_FALLBACK_DIR"; // For backwards compat, this checks env vars first before using Env.GetFolderPath var appData = Environment.GetEnvironmentVariable("APPDATA"); var root = appData // On Windows it goes to %APPDATA%\Microsoft\UserSecrets\ ?? Environment.GetEnvironmentVariable("HOME") // On Mac/Linux it goes to ~/.microsoft/usersecrets/ ?? Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? Environment.GetEnvironmentVariable(userSecretsFallbackDir); // this fallback is an escape hatch if everything else fails if (string.IsNullOrEmpty(root)) { throw new InvalidOperationException("Could not determine an appropriate location for storing user secrets. Set the " + userSecretsFallbackDir + " environment variable to a folder where user secrets should be stored."); } return !string.IsNullOrEmpty(appData) ? Path.Combine(root, "Microsoft", "UserSecrets", userSecretsId, SecretsFileName) : Path.Combine(root, ".microsoft", "usersecrets", userSecretsId, SecretsFileName); }
在開發過程中,這個私密文件一般是:C:\Users\[USER]\AppData\Roaming\Microsoft\UserSecrets\[userSecretsId]\secrets.json
其實AddUserSecrets是基於Json文件配置的一個實現,換句話說,我們只需要在上面的隱私文件secrets.json中配置數據,就可以集成到IConfiguration中。
比如我在上面的目錄下的secrets.json(C:\Users\Administrator\AppData\Roaming\Microsoft\UserSecrets\userSecretsId\secrets.json)內容如下:
{ "Secret": { "Key1": { "Key2": "Value2" }, "Key3": "Value3" } }
我們這樣訪問:
[assembly: Microsoft.Extensions.Configuration.UserSecrets.UserSecretsId("userSecretsId")] namespace ConsoleApp { class Program { static void Main(string[] args) { var secretPath = Microsoft.Extensions.Configuration.UserSecrets.PathHelper.GetSecretsPathFromSecretsId("userSecretsId"); Console.WriteLine("secretPath目錄在:" + secretPath); ConfigurationBuilder builder = new ConfigurationBuilder(); //builder.AddUserSecrets("userSecretsId"); builder.AddUserSecrets(typeof(Program).Assembly); var configuration = builder.Build(); var collections = configuration.AsEnumerable(); foreach (var item in collections) { Console.WriteLine("{0}={1}", item.Key, item.Value); } } } }
結果:

Azure雲
NuGet安裝包:Microsoft.Extensions.Configuration.AzureKeyVault
這個和Azure有關,因為我們基本上不用Azure雲,所以就不介紹了,感興趣的可以看看源碼,都挺簡單,源碼地址:https://github.com/dotnet/extensions/tree/v3.1.12/src/Configuration/Config.AzureKeyVault/src
三、IConfiguration使用
一般的,我們要獲取IConfiguration或者IConfigurationRoot,都是結合DI容器來使用的,比如我們的Startup:
public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } ... }
1、在結構上,我們可以將IConfiguration看做一個Key-Value的集合,另一方面,因為Key的特殊性,我們又可以將它看做一種樹形結構的數據集合。
這里說的Key的特殊性,指的就是Key是有層級關系的,層級采用 ConfigurationPath.KeyDelimiter 字段標識來分隔,默認是冒號(:),不建議修改(采用反射可修改)。
2、當我們配置由改動,需要重新加載時,可以調用 IConfigurationRoot 的 Reload 方法重新加載配置
3、IConfiguration的GetSection方法可以獲取某個節點信息,參數key是相對於當前節點的,其中IConfigurationSection中有三個屬性:
Key:當前節點名,不包含路徑
Path:從根節點到當前節點的路徑
Value:當前節點數據
4、IConfiguration的GetConnectionString方法獲取的是當前節點下的ConnectionStrings節點下指定名稱子節點的數據(源碼):
public static string GetConnectionString(this IConfiguration configuration, string name) { return configuration?.GetSection("ConnectionStrings")?[name]; }
5、IConfiguration有兩個重要的方法:Get和Bind。
Get方法將當前節點及其子節點的數據轉換並存放到指定類型的實體對象的屬性中,並返回改實體對象。
Bind方法接收一個實體對象,並將當前節點的及其字節點的數據保存到改實體對象的屬性中。
舉個簡單的例子,比如我們在appsettings.json 中有配置 :
{ ... "Data": { "Value1": 1, "Value2": 3.14, "Value3": true, "Value4": [ 1, 2, 3 ], "Value5": { "Value1": 2, "Value2": 5.20, "Value3": false, "Value4": [ 4,5,6,7 ] } } }
然后相對應的創建一個實體類:
public class TestData { public int Value1 { get; set; } public decimal Value2 { get; set; } public bool Value3 { get; set; } public int[] Value4 { get; set; } public TestData Value5 { get; set; } }
使用時的區別就是:
var section = configuration.GetSection("Data"); var data1= section.Get<TestData>(); var data2 = new TestData(); section.Bind(data2);
這樣一來,經過Get方法或者Bind方法,IConfiguration中的數據就放到我們熟悉的實體對象中去了,再也不用去做那些煩躁的字符串類型轉換了!
四、總結
這一篇就先到這里吧,.net core的配置還是很簡單的,隨便看看源碼就能掌握。
