Microsoft.Extensions.Configuration.dll 用來處理組態,讀檔、重新載入、支援多種格式,包括記憶體、Json、Xml、Ini,也可以像在 .Net Fx 用 Transform 切換組態。
它是在 .NET Core 的基礎建設之一,除了.NET Core,也支援 NET Framework 4.6.1 以上;也可以像在 .Net Fx 用 Transform 切換組態。
ASP.NET Core 預設的組態是 appSetting.json,本篇簡單介紹組態設定的使用方法,關於 ASP.NET Core 的 Host,以後再寫一篇為大家講解
開發環境
- Rider 2021.3.4
- Windows 10
- .Net Fx 4.8 via 新版專案範本 .NET Project SDKs
- .Net Core 5
- Microsoft.Extensions.Configuration 5.0.0
多種 Configuration Provider
.NET 有以下幾種 Configuration Provider
- File configuration provider
- Environment variable configuration provider
- Command-line configuration provider
- Key-per-file configuration provider
- Memory configuration provider
FileConfigurationProvider 是讀取檔案組態的基礎類別,以下是衍生類別
- Microsoft.Extensions.Configuration.Ini.IniConfigurationProvider
- Microsoft.Extensions.Configuration.Json.JsonConfigurationProvider
- Microsoft.Extensions.Configuration.NewtonsoftJson.NewtonsoftJsonConfigurationProvider
- Microsoft.Extensions.Configuration.Xml.XmlConfigurationProvider
實例化 IConfigurationBuilder
通過 ConfigurationBuilder 的封裝,隱藏了 Provider 的實作細節,我們只需要配置需要甚麼樣的檔案格式、檔案內容,ConfigurationBuilder 就會吐回來相關的資料給我們,我們也不需要改變原有的 Production。
IConfigurationBuilder.AddJsonFile 擴充方法,骨子裡面幫我們實例化後放到他自己的容器。
不同的 Provider 都有相對應的擴充方法
- IConfigurationBuilder.AddJsonFile / IConfigurationBuilder.AddJsonStream
- IConfigurationBuilder.AddXmlFile / IConfigurationBuilder.AddXmlStream
- IConfigurationBuilder.AddIniFile / IConfigurationBuilder.AddIniStream
- IConfigurationBuilder.AddEnvironmentVariables
- IConfigurationBuilder.AddKeyPerFile
- IConfigurationBuilder.AddInMemoryCollection
- IConfigurationBuilder.AddCommandLine
手動實例化 IConfigurationBuilder
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json");
由 Host 實例化 IConfigurationBuilder
ASP.NET Core 或是 .NET App 都可以使用 Microsoft.Extensions.Hosting 來取得 ConfigurationBuilder
private static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration(config =>
{
config.Sources.Clear();
config.AddJsonFile("appsettings.json", true, true);
var configRoot = config.Build();
...
})
.ConfigureServices(service =>
{
//DI
});
想要了解 Host 更多的請參考以下:
- .NET Generic Host:Default builder settings
- ASP.NET Core:Default configuration
IHostBuilder 可以設定兩種型態的組態
- 主機組態
- 應用程式組態
主機組態 IHostBuilder.ConfigureHostConfiguration
Host.CreateDefaultBuilder()
.ConfigureHostConfiguration((config)=>
{
config.AddJsonFile("appsettings.json", false, true);
})
應用程式組態 IHostBuilder.ConfigureAppConfiguration
Host.CreateDefaultBuilder()
.ConfigureAppConfiguration((hostContext,config) =>
{
config.AddJsonFile("appsettings.json", true, true);
})
實例化 IConfigurationProvider
IConfigurationBuilder 骨子裡面其實是根據檔案型態建立相關的 Provider 實例,再讀取裡面的節點,以下為 JsonConfigurationProvider 範例:
[TestMethod]
public void 實例化JsonConfigurationProvider()
{
var configProvider = new JsonConfigurationProvider(new JsonConfigurationSource
{
Optional = false,
Path = "appsettings.json",
ReloadOnChange = true
});
configProvider.Load();
configProvider.TryGet("Player:AppId", out var appId);
Console.WriteLine($"AppId = {appId}");
}
讀取檔案組態檔
讀取 Json 組態檔
安裝套件:
Install-Package Microsoft.Extensions.Configuration.Json
appsettings.json 內容如下:
{
"ConnectionStrings": {
"DefaultConnectionString": "Server=(localdb)\\mssqllocaldb;Database=ConsoleApp.NewDb;Trusted_Connection=True;"
},
"Player": {
"AppId": "play1",
"Key": "1234567890"
}
}
組態存放內容
Configuration providers 裡面最終會使用 Key-Value (string-string) 的字典集合來存放狀態,反編譯取得的代碼如下:
public abstract class ConfigurationProvider : IConfigurationProvider
{
private ConfigurationReloadToken _reloadToken = new ConfigurationReloadToken();
/// <summary>
/// Initializes a new <see cref="IConfigurationProvider"/>
/// </summary>
protected ConfigurationProvider()
{
Data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
}
Key 約束
- 同一個 Provider 實例不能有相同的 Key。
- 不同的 Provider 實例 有相同的 Key,Value 以後面為主。
- Key 不分大小寫
- 階層 Keys
- 組態設定以冒號 : 區隔,例如 主節點:子節點:次子節點
- 環境變數用雙底線 __ 區隔
- Azure Key Vault 用雙槓 –
Value 約束
- null 不會被保留
通過 IConfiguration 索引
- 通過 IConfigurationBuilder 物件建立 IConfigurationRoot 物件。
- IConfigurationBuilder.SetBasePath 方法是設定檔案的基本路徑
- IConfigurationBuilder.AddJsonFile 方法是讀取設定檔的路徑,完整的路徑為 "基本路徑 + AddJsonFile"
- IConfigurationRoot[節點名稱] / IConfiguration[節點名稱] 取得設定值,節點名稱是 Key,階層用冒號 : 區隔,例如 主節點:子節點:次子節點
- 當節點(key)不存在的時候,Value 會得到 null
代碼如下:
[TestMethod]
public void 手動實例化ConfigurationBuilder()
{
var configBuilder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json");
var configRoot = configBuilder.Build();
//讀取組態
Console.WriteLine($"AppId = {configRoot["AppId"]}");
Console.WriteLine($"AppId = {configRoot["Player:AppId"]}");
Console.WriteLine($"Key = {configRoot["Player:Key"]}");
Console.WriteLine($"Connection String = {configRoot["ConnectionStrings:DefaultConnectionString"]}");
}
通過 IConfiguration.GetSection
[TestMethod]
public void 讀取設定檔_GetSection()
{
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json");
var configRoot = builder.Build();
Console.WriteLine($"AppId = {configRoot.GetSection("AppId")}");
Console.WriteLine($"AppId = {configRoot.GetSection("Player:AppId")}");
Console.WriteLine($"Key = {configRoot.GetSection("Player:Key")}");
Console.WriteLine($"Connection String = {configRoot.GetSection("ConnectionStrings:DefaultConnectionString")}");
}
通過 IConfigurationProvider.TryGet
IConfigurationRoot.Providers 存放 Provider 的實例,用 IConfigurationProvider.TryGet 讀取,一個一個的取出各個 Provider 所裝載的組態
[TestMethod]
public void 讀取設定檔_TryGet()
{
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json");
var configRoot = builder.Build();
//TryGet
foreach (var provider in configRoot.Providers)
{
provider.TryGet("Player:AppId", out var value);
Console.WriteLine($"AppId = {value}");
}
}
再看另外一個例子
[TestMethod]
public void 實例化JsonConfigurationProvider()
{
var configProvider = new JsonConfigurationProvider(new JsonConfigurationSource
{
Optional = false,
Path = "appsettings.json",
ReloadOnChange = true
});
configProvider.Load();
configProvider.TryGet("Player:AppId", out var appId);
Console.WriteLine($"AppId = {appId}");
}
通過 IConfiguration.GetChildren
上述 IConfiguration.GetSection、IConfigurationProvider.TryGet 的 Configuration Key 必須要帶入完整的結構才能取得資料,IConfiguration.GetChildren 可以將整個結構倒出來處理,範例程式如下:
[TestMethod]
public void 讀取設定檔_GetChild()
{
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json");
var configRoot = builder.Build();
var firstSections = configRoot.GetChildren();
foreach (var firstSection in firstSections)
{
var secondSections = firstSection.GetChildren();
foreach (var secondSection in secondSections)
{
Console.WriteLine($"{secondSection.Key}={secondSection.Value}\tPath={secondSection.Path}");
}
}
}
執行結果如下:
DefaultConnectionString=Server=(localdb)\mssqllocaldb;Database=EFGetStarted.ConsoleApp.NewDb;Trusted_Connection=True; Path=ConnectionStrings:DefaultConnectionString
AppId=player1 Path=Player:AppId
Key=1234567890 Path=Player:Key
通過綁定
綁定可以將某一個父節點,整個轉成強型別物件,簡化處理節點的苦差事
安裝套件:
Install-Package Microsoft.Extensions.Configuration.Binder -Version 5.0.0
上面的例子都是用弱型別 string 處理,IConfiguration.Bind 擴充方法可以將組態設定綁定到強型別物件,代碼如下:
[TestMethod]
public void 讀取設定檔_綁定()
{
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json");
var configRoot = builder.Build();
var appSetting = new AppSetting();
config.Bind(appSetting);
Console.WriteLine($"AppId = {appSetting.Player.AppId}");
Console.WriteLine($"Key = {appSetting.Player.Key}");
Console.WriteLine($"Connection String = {appSetting.ConnectionStrings.DefaultConnectionString}");
}
型別定義如下:
public struct AppSetting
{
public Player Player { get; set; }
public ConnectionStrings ConnectionStrings { get; set; }
}
public struct Player
{
public string AppId { get; set; }
public string Key { get; set; }
}
public struct ConnectionStrings
{
public string DefaultConnectionString { get; set; }
public string AuthenticationConnectionString { get; set; }
}
ConfigurationBinder.Get<T> 擴充方法,可以針對某一個組態的父節點轉成強型別
[TestMethod]
public void 讀取設定檔_綁定_Get()
{
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json");
var config = builder.Build();
var player = config.GetSection("Player").Get<Player>();
var appSetting = config.Get<AppSetting>();
Console.WriteLine($"AppId = {player.AppId}");
Console.WriteLine($"Key = {appSetting.Player.Key}");
Console.WriteLine($"Connection String = {appSetting.ConnectionStrings.DefaultConnectionString}");
}
config.GetSection("Player"),這樣沒有內容
組態是否為必要條件
optional 預設屬性為 false,當組態檔不存在時會噴出 System.IO.FileNotFoundException;反之,則是不存在時,不會噴例外
var configBuilder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
;
var configRoot = configBuilder.Build();
... read config
重新載入組態
只要把 reloadOnChange = true,當組態設定更改時,就會重新載入;
var configBuilder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
;
var configRoot = configBuilder.Build();
... read config
讀取 Xml 組態檔
Install-Package Microsoft.Extensions.Configuration.Xml -Version 5.0.0
appsettings.xml 內容如下:
<?xml version="1.0" encoding="UTF-8" ?>
<root>
<ConnectionStrings>
<DefaultConnectionString>Server=(localdb)\mssqllocaldb;Database=EFGetStarted.ConsoleApp.NewDb;Trusted_Connection=True;</DefaultConnectionString>
</ConnectionStrings>
<Player>
<AppId>testApp</AppId>
<Key>12345678990</Key>
</Player>
</root>
代碼如下:
[TestMethod]
public void 手動實例化ConfigurationBuilder()
{
var configBuilder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddXmlFile("appsettings.xml",optional:false,reloadOnChange:true);
var configRoot = configBuilder.Build();
//讀取組態
Console.WriteLine($"AppId = {configRoot["AppId"]}");
Console.WriteLine($"AppId = {configRoot["Player:AppId"]}");
Console.WriteLine($"Key = {configRoot["Player:Key"]}");
Console.WriteLine($"Connection String = {configRoot["ConnectionStrings:DefaultConnectionString"]}");
}
讀取 Ini 組態檔
安裝套件
Install-Package Microsoft.Extensions.Configuration.Ini -Version 5.0.0
appsettings.ini 內容如下:
[ConnectionStrings]
DefaultConnectionString="Server=(localdb)\\mssqllocaldb;Database=EFGetStarted.ConsoleApp.NewDb;Trusted_Connection=True;"
[Player]
AppId=testApp
Key=12345678990
代碼如下:
[TestMethod]
public void 手動實例化ConfigurationBuilder()
{
var configBuilder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddIniFile("appsettings.ini", optional: false, reloadOnChange: true);
var configRoot = configBuilder.Build();
//讀取組態
Console.WriteLine($"AppId = {configRoot["AppId"]}");
Console.WriteLine($"AppId = {configRoot["Player:AppId"]}");
Console.WriteLine($"Key = {configRoot["Player:Key"]}");
Console.WriteLine($"Connection String = {configRoot["ConnectionStrings:DefaultConnectionString"]}");
}
讀取記憶體組態
在不變動 Production Code 的情況之下,我們可以注入記憶體資料結構,隨時可以根據測試案例注入我們想要的內容,這功能就很像我在前幾篇提到的 VFS
下圖出自:.NET 的 Virtual File System - Lexical.FileSystem | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)
使用方式相當簡單,就是在 IConfigurationBuilder.AddInMemoryCollection 填入 Dictionary<string,string>,讀取的方式就都一樣了,比較特別的是結構設計,這裡是使用 : 分號,來當成階層關係
[TestMethod]
public void 讀取記憶體組態()
{
var configBuilder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddInMemoryCollection(new Dictionary<string, string>
{
{ "Player:AppId", "player1" },
{ "Player:Key", "1234567890" },
{
"ConnectionStrings:DefaultConnectionString",
"Server=(localdb)\\mssqllocaldb;Database=EFGetStarted.ConsoleApp.NewDb;Trusted_Connection=True;"
},
})
;
var configRoot = configBuilder.Build();
//讀取組態
Console.WriteLine($"AppId = {configRoot["AppId"]}");
Console.WriteLine($"AppId = {configRoot["Player:AppId"]}");
Console.WriteLine($"Key = {configRoot["Player:Key"]}");
Console.WriteLine($"Connection String = {configRoot["ConnectionStrings:DefaultConnectionString"]}");
}
轉成複雜型別
[TestMethod]
public void 讀取記憶體組態_綁定()
{
var configBuilder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddInMemoryCollection(new Dictionary<string, string>
{
{ "Player:AppId", "player1" },
{ "Player:Key", "1234567890" },
{
"ConnectionStrings:DefaultConnectionString",
"Server=(localdb)\\mssqllocaldb;Database=EFGetStarted.ConsoleApp.NewDb;Trusted_Connection=True;"
},
})
;
var configRoot = configBuilder.Build();
var appSetting = configRoot.Get<AppSetting>();
//讀取組態
Console.WriteLine($"AppId = {appSetting.Player.AppId}");
Console.WriteLine($"Key = {appSetting.Player.Key}");
Console.WriteLine($"Connection String = {appSetting.ConnectionStrings.DefaultConnectionString}");
}
轉成集合
在 Player 前面再多一個 "a",代表索引,這個值可以隨便命名,就可以轉成複雜型別了
[TestMethod]
public void 讀取記憶體組態_綁定_集合()
{
var configBuilder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddInMemoryCollection(new Dictionary<string, string>
{
{ "a:Player:AppId", "player1" },
{ "a:Player:Key", "1234567890" },
{
"a:ConnectionStrings:DefaultConnectionString",
"a:Server=(localdb)\\mssqllocaldb;Database=EFGetStarted.ConsoleApp.NewDb;Trusted_Connection=True;"
},
{ "b:Player:AppId", "player2" },
{ "b:Player:Key", "1234567890" },
{
"b:ConnectionStrings:DefaultConnectionString",
"b:Server=(localdb)\\mssqllocaldb;Database=EFGetStarted.ConsoleApp.NewDb;Trusted_Connection=True;"
},
})
;
var configRoot = configBuilder.Build();
var appSettings = configRoot.Get<IList<AppSetting>>();
//讀取組態
Console.WriteLine($"AppId = {appSettings[0].Player.AppId}");
Console.WriteLine($"Key = {appSettings[0].Player.Key}");
Console.WriteLine($"Connection String = {appSettings[0].ConnectionStrings.DefaultConnectionString}");
}
轉成字典
[TestMethod]
public void 讀取記憶體組態_綁定_字典()
{
var configBuilder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddInMemoryCollection(new Dictionary<string, string>
{
{ "a:Player:AppId", "player1" },
{ "a:Player:Key", "1234567890" },
{
"a:ConnectionStrings:DefaultConnectionString",
"a:Server=(localdb)\\mssqllocaldb;Database=EFGetStarted.ConsoleApp.NewDb;Trusted_Connection=True;"
},
{ "b:Player:AppId", "player2" },
{ "b:Player:Key", "1234567890" },
{
"b:ConnectionStrings:DefaultConnectionString",
"b:Server=(localdb)\\mssqllocaldb;Database=EFGetStarted.ConsoleApp.NewDb;Trusted_Connection=True;"
},
})
;
var configRoot = configBuilder.Build();
var appSettings = configRoot.Get<Dictionary<string, AppSetting>>();
//讀取組態
Console.WriteLine($"AppId = {appSettings["a"].Player.AppId}");
Console.WriteLine($"Key = {appSettings["a"].Player.Key}");
Console.WriteLine($"Connection String = {appSettings["a"].ConnectionStrings.DefaultConnectionString}");
Console.WriteLine($"AppId = {appSettings["b"].Player.AppId}");
Console.WriteLine($"Key = {appSettings["b"].Player.Key}");
Console.WriteLine($"Connection String = {appSettings["b"].ConnectionStrings.DefaultConnectionString}");
}
讀取命令組態
安裝套件
Install-Package Microsoft.Extensions.Configuration.CommandLine -Version 5.0.0
應用程式可以透過外部命令注入參數,在 VS IDE 有提供 UI 讓我們輸入參數,如下圖:
參數的規則
key 對應 value
key 後面接 = ,例如 key=value,以下是支援的格式
- --key=value
- /key=value
- key=value
代碼如下:
[TestMethod]
[DataRow(new[] {"--AppId=1234567890"})]
[DataRow(new[] {"/AppId=1234567890"})]
[DataRow(new[] {"AppId=1234567890"})]
public void 實例化CommandLineConfigurationProvider(string[] args)
{
var provider = new CommandLineConfigurationProvider(args);
provider.Load();
provider.TryGet("AppId", out var appId);
Console.WriteLine($"{args.First()}\r\n" +
$"AppId:{appId}");
}
執行結果如下:
--AppId=1234567890
AppId:1234567890
/AppId=1234567890
AppId:1234567890
AppId=1234567890
AppId:1234567890
參數對應
將參數對應到另外一個 Key,比如說一個短的參數 i,對應完整的參數 AppId
[TestMethod]
[DataRow(new[] {"-i=1234567890", "-c=app.json"})]
public void 命令對應_Host(string[] args)
{
var map = new Dictionary<string, string>
{
{"-i", "AppId"},
{"-c", "Config"}
};
var builder = Host.CreateDefaultBuilder()
.ConfigureAppConfiguration(config =>
{
// config.Sources.Clear();
config.AddCommandLine(args, map);
var configRoot = config.Build();
var appId = configRoot["AppId"];
var configPath = configRoot["Config"];
Console.WriteLine($"{args.First()}\r\n" +
$"AppId:{appId}\r\n" +
$"ConfigPath:{configPath}");
})
.ConfigureServices(service =>
{
//DI
service.AddScoped(typeof(AppWorkFlow));
})
;
var host = builder.Build();
}
之前,有介紹過一個套件 PowserArgs ,我個人認為它比較強大 ,使用 PowerArgs 解析 Console / WinForm / WPF 的參數 | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)
讀取資料夾
讀取資料夾,把檔案名稱作為 key,檔案內容為 value,這個用於 Docker Hosting 情境
安裝套件
Install-Package Microsoft.Extensions.Configuration.KeyPerFile -Version 5.0.0
然後在專案目錄建立檔案,結構如下圖:
檔案輸出:永遠複製
代碼如下:
[TestMethod]
public void 手動實例化ConfigurationBuilder()
{
var expected = "我是檔案內容";
var folderPath = Path.Combine(Directory.GetCurrentDirectory(), "keys/aws/web");
var configBuilder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddKeyPerFile(folderPath,false);
var configRoot = configBuilder.Build();
//讀取組態
var actual = configRoot["NewFile1.txt"];
Console.WriteLine($"NewFile1.txt = {actual}");
Assert.AreEqual(expected,actual);
}
有幾個規則:
- 資料夾路徑必須是絕對路徑
- 文件名稱用雙底線 __,轉換成組態 Key 的時候會變冒號 : ,例如,文件名 Logging__LogLevel__System => Logging:logLevel:System
讀取環境變數
- Environment Variables Configuration Provider 的 Prefixes 屬性用來分離帶有某個前綴詞的環境變數,例如 Prefixes="CUSTOM_",環境變數有 CUSTOM_ENV,最後得到的 Key 會變成 ENV。
- 不指定 Prefixes 時,會得到所有的環境變數。
- Host 預設的 Prefixes 可能是 DOTNET_ or ASPNETCORE_,看你使用的是哪一種 Host
- 當使用 ConfigureWebHostDefaults,ASPNETCORE_ENVIRONMENT 的值會覆寫 DOTNET_ENVIRONMENT
下圖使用 Web Host
下圖使用 Host
讀取作業系統的環境變數,首先要先確定作業系統有這個變數。
在 Windows 環境底下的設定步驟
讀不到設定好的環境變數,可以試著重新開機
最後我在我的電腦設定了以下
- ASPNETCORE_ENVIRONMENT=Test
- CUSTOM_ENVIRONMENT1=Test
- DOTNET_ENVIRONMENT2=Test
測試代碼如下:
[TestMethod]
public void Host實例化ConfigurationBuilder()
{
var builder = Host.CreateDefaultBuilder()
.ConfigureAppConfiguration((hosting,configBuilder) =>
{
// config.Sources.Clear();
configBuilder.AddEnvironmentVariables("Custom_");
var configRoot = configBuilder.Build();
//讀取組態
Console.WriteLine($"ASPNETCORE_ENVIRONMENT = {configRoot["ASPNETCORE_ENVIRONMENT"]}");
Console.WriteLine($"DOTNET_ENVIRONMENT2 = {configRoot["DOTNET_ENVIRONMENT2"]}");
Console.WriteLine($"CUSTOM_ENVIRONMENT1 = {configRoot["CUSTOM_ENVIRONMENT1"]}");
Console.WriteLine($"ENVIRONMENT1 = {configRoot["ENVIRONMENT1"]}");
})
;
builder.Build();
}
執行結果如下:
ASPNETCORE_ENVIRONMENT = Test
DOTNET_ENVIRONMENT2 = Test
CUSTOM_ENVIRONMENT1 = Test
ENVIRONMENT1 = Test
.NET 提供的 Environment.SetEnvironmentVariable() 方法也可以達到相同的效果,綁定的用法都跟 MemoryConfigurationProvider 一樣,也蠻適用在測試案例的
[TestMethod]
public void 讀取環境變數()
{
Environment.SetEnvironmentVariable("Player:AppId", "player1");
Environment.SetEnvironmentVariable("Player:Key", "1234567890");
Environment.SetEnvironmentVariable("ConnectionStrings:DefaultConnectionString",
"Server=(localdb)\\mssqllocaldb;Database=EFGetStarted.ConsoleApp.NewDb;Trusted_Connection=True;");
var configBuilder = new ConfigurationBuilder().AddEnvironmentVariables();
var configRoot = configBuilder.Build();
//讀取組態
Console.WriteLine($"AppId = {configRoot["AppId"]}");
Console.WriteLine($"AppId = {configRoot["Player:AppId"]}");
Console.WriteLine($"Key = {configRoot["Player:Key"]}");
Console.WriteLine($"Connection String = {configRoot["ConnectionStrings:DefaultConnectionString"]}");
}
[TestMethod]
public void 讀取環境變數_綁定()
{
Environment.SetEnvironmentVariable("Player:AppId", "player1");
Environment.SetEnvironmentVariable("Player:Key", "1234567890");
Environment.SetEnvironmentVariable("ConnectionStrings:DefaultConnectionString",
"Server=(localdb)\\mssqllocaldb;Database=EFGetStarted.ConsoleApp.NewDb;Trusted_Connection=True;");
var configBuilder = new ConfigurationBuilder().AddEnvironmentVariables();
var configRoot = configBuilder.Build();
var appSetting = configRoot.Get<AppSetting>();
//讀取組態
Console.WriteLine($"AppId = {appSetting.Player.AppId}");
Console.WriteLine($"Key = {appSetting.Player.Key}");
Console.WriteLine($"Connection String = {appSetting.ConnectionStrings.DefaultConnectionString}");
}
[TestMethod]
public void 讀取環境變數_綁定_集合()
{
Environment.SetEnvironmentVariable("a:Player:AppId", "player1");
Environment.SetEnvironmentVariable("a:Player:Key", "1234567890");
Environment.SetEnvironmentVariable("a:ConnectionStrings:DefaultConnectionString",
"Server=(localdb)\\mssqllocaldb;Database=EFGetStarted.ConsoleApp.NewDb;Trusted_Connection=True;");
Environment.SetEnvironmentVariable("b:Player:AppId", "player2");
Environment.SetEnvironmentVariable("b:Player:Key", "1234567890");
Environment.SetEnvironmentVariable("b:ConnectionStrings:DefaultConnectionString",
"Server=(localdb)\\mssqllocaldb;Database=EFGetStarted.ConsoleApp.NewDb;Trusted_Connection=True;");
var configBuilder = new ConfigurationBuilder().AddEnvironmentVariables();
var configRoot = configBuilder.Build();
var appSettings = configRoot.Get<IList<AppSetting>>();
//讀取組態
Console.WriteLine($"AppId = {appSettings[0].Player.AppId}");
Console.WriteLine($"Key = {appSettings[0].Player.Key}");
Console.WriteLine($"Connection String = {appSettings[0].ConnectionStrings.DefaultConnectionString}");
Console.WriteLine($"AppId = {appSettings[1].Player.AppId}");
Console.WriteLine($"Key = {appSettings[1].Player.Key}");
Console.WriteLine($"Connection String = {appSettings[1].ConnectionStrings.DefaultConnectionString}");
}
[TestMethod]
public void 讀取環境變數_綁定_字典()
{
Environment.SetEnvironmentVariable("a:Player:AppId", "player1");
Environment.SetEnvironmentVariable("a:Player:Key", "1234567890");
Environment.SetEnvironmentVariable("a:ConnectionStrings:DefaultConnectionString",
"Server=(localdb)\\mssqllocaldb;Database=EFGetStarted.ConsoleApp.NewDb;Trusted_Connection=True;");
Environment.SetEnvironmentVariable("b:Player:AppId", "player2");
Environment.SetEnvironmentVariable("b:Player:Key", "1234567890");
Environment.SetEnvironmentVariable("b:ConnectionStrings:DefaultConnectionString",
"Server=(localdb)\\mssqllocaldb;Database=EFGetStarted.ConsoleApp.NewDb;Trusted_Connection=True;");
var configBuilder = new ConfigurationBuilder().AddEnvironmentVariables();
var configRoot = configBuilder.Build();
var appSettings = configRoot.Get<Dictionary<string,AppSetting>>();
//讀取組態
Console.WriteLine($"AppId = {appSettings["a"].Player.AppId}");
Console.WriteLine($"Key = {appSettings["a"].Player.Key}");
Console.WriteLine($"Connection String = {appSettings["a"].ConnectionStrings.DefaultConnectionString}");
Console.WriteLine($"AppId = {appSettings["b"].Player.AppId}");
Console.WriteLine($"Key = {appSettings["b"].Player.Key}");
Console.WriteLine($"Connection String = {appSettings["b"].ConnectionStrings.DefaultConnectionString}");
}
切換組態
根據不同的 Build 切換組態
根據不同的環境切換不同的參數做法很多,這裡我要實作根據不同的 Build 切換組態。
首先,在方案組態新增一個 QA Build Config
從 Debug Config 複製並更名為 QA
完成方案組態之後,應該可以在專案看到 QA Build Config,自動幫我們加入 QA Symbol
增加 appsettings.QA.json,內容如下:
{
"ConnectionStrings": {
"DefaultConnectionString": "Server=(localdb)\\mssqllocaldb;Database=ConsoleApp.NewDb.QA;Trusted_Connection=True;"
},
"Player": {
"AppId": "qa",
"Key": "qa12345"
}
}
再增加一個 .AddJsonFile($"appsettings.{environmentName}.json", true, true) 讀取 appsettings.QA.json,由於後面會蓋掉前面組態的特性,這樣就能完成替換組態
var configBuilder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", false, true)
.AddJsonFile($"appsettings.qa.json", true, true)
;
為了在不同的 Build 要能夠切換各種不同的環境,通過 Configuration Define constants 來指定環境
[TestMethod]
public void 切換組態()
{
string environmentName;
#if DEBUG
environmentName = "Development";
#elif QA
environmentName = "QA";
#elif STAGING
environmentName = "Staging";
#elif RELEASE
environmentName = "Production";
#endif
var configBuilder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", false, true)
.AddJsonFile($"appsettings.{environmentName}.json", true, true)
;
var configRoot = configBuilder.Build();
//讀取組態
Console.WriteLine($"AppId = {configRoot["Player:AppId"]}");
Console.WriteLine($"Key = {configRoot["Player:Key"]}");
Console.WriteLine($"Connection String = {configRoot["ConnectionStrings:DefaultConnectionString"]}");
}
在 Debug Build 下執行結果如下:
AppId = player1
Key = 1234567890
Connection String = Server=(localdb)\mssqllocaldb;Database=EFGetStarted.ConsoleApp.NewDb;Trusted_Connection=True;
在 QA Build 下執行結果如下:
AppId = qa
Key = qa1234567890
Connection String = Server=(localdb)\mssqllocaldb;Database=ConsoleApp.NewDb.QA;Trusted_Connection=True;
上述的作法是放在專案組態裡面,如果說不同的服務需要用到相同的參數,往作業系統的環境去擺放是一種選項,微軟也幫你準備好了 Environment variable configuration provider
根據不同的環境變數切換組態
在電腦設定環境變數 DOTNET_ENVIRONMENT2=Test,通過 Environment variable configuration provider 取得環境變數,再載入 Json 組態檔
[TestMethod]
public void 切換組態設定()
{
var builder = Host.CreateDefaultBuilder()
.ConfigureAppConfiguration((hosting, configBuilder) =>
{
// config.Sources.Clear();
var environmentName = hosting.Configuration["ENVIRONMENT2"];
configBuilder.AddJsonFile("appsettings.json",false,true);
configBuilder.AddJsonFile($"appsettings.{environmentName}.json",true,true);
var configRoot = configBuilder.Build();
//讀取組態
Console.WriteLine($"AppId = {configRoot["Player:AppId"]}");
Console.WriteLine($"Key = {configRoot["Player:Key"]}");
Console.WriteLine($"Connection String = {configRoot["ConnectionStrings:DefaultConnectionString"]}");
})
;
builder.Build();
}
執行結果如下:
AppId = test
Key = test1234567890
Connection String = Server=(localdb)\mssqllocaldb;Database=EFGetStarted.ConsoleApp.NewDb.Test;Trusted_Connection=True;
注入 IConfiguration
上述的範例都是在 Host 演練讀取檔案,但是在真實的世界裡,盡量不要讓你的程式依賴操作檔案,我讓它依賴 IConfiguration,然後,開放一個注入點,讓調用端決定依賴甚麼樣的檔案格式,轉換成 IConfiguration 後再注入;Host 預設也是注入 IConfiguration,而不是 Configuration Provider
新增一個 AppWorkFlow,依賴 IConfiguration 並在建構函數開放一個注入點 IConfiguration,讓外部可以注入
public class AppWorkFlow
{
private readonly IConfiguration _config;
public AppWorkFlow(IConfiguration config)
{
this._config = config;
}
public string GetPlayerId()
{
return this._config.GetSection("Player:AppId").Value;
}
}
通過 Microsoft.Extensions.Hosting.ConfigureAppConfiguration 擴充方法注入 IConfiguration
[TestMethod]
public void 注入Configuration()
{
var builder = Host.CreateDefaultBuilder(null)
.ConfigureAppConfiguration(config =>
{
config.Sources.Clear();
config.AddJsonFile("appsettings.json", true, true);
})
.ConfigureServices(service =>
{
//DI
service.AddScoped(typeof(AppWorkFlow));
});
var host = builder.Build();
var appWorkFlow = host.Services.GetService<AppWorkFlow>();
var playerId = appWorkFlow.GetPlayerId();
Console.WriteLine($"AppId = {playerId}");
}
有關 DI Container,可以參考
如何使用 DI Container for Microsoft.Extensions.DependencyInjection | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)
如何使用 Microsoft.Extensions.DependencyInjection for Autofac | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)
範例位置
若有謬誤,煩請告知,新手發帖請多包涵
Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET