如何使用組態 Microsoft.Extensions.Configuration

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

FileConfigurationProvider 是讀取檔案組態的基礎類別,以下是衍生類別

實例化 IConfigurationBuilder

通過 ConfigurationBuilder 的封裝,隱藏了 Provider 的實作細節,我們只需要配置需要甚麼樣的檔案格式、檔案內容,ConfigurationBuilder  就會吐回來相關的資料給我們,我們也不需要改變原有的 Production。

IConfigurationBuilder.AddJsonFile 擴充方法,骨子裡面幫我們實例化後放到他自己的容器。

不同的 Provider 都有相對應的擴充方法

 

手動實例化 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 更多的請參考以下:

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)

 

 

範例位置

sample.dotblog/Configuration/NetCore/Lab.Config/NetFx48 at master · yaochangyu/sample.dotblog (github.com)

若有謬誤,煩請告知,新手發帖請多包涵


Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET

Image result for microsoft+mvp+logo