工作中的 C# 實作:.NET 組態系統 - IOptions 與 IConfigureOptions

AdSense

前言

在 .NET Core 5+ 的開發中,處理應用程式的組態 (Configuration) 是一個很重要的議題。這套全新的組態系統,目的在解決傳統 .NET Framework 時代組態管理的痛點,讓程式碼更具彈性、更好維護,也更符合現代軟體開發的設計模式。

什麼是組態?為什麼需要它?

組態就像是你的應用程式的「設定檔」。它包含了所有在程式碼之外、但會影響程式行為的資訊。

例如:

  • 資料庫連線字串:程式需要知道要連線到哪一個資料庫。
  • 第三方服務的 API 金鑰:例如,寄送 Email 服務的 API 金鑰。
  • 功能開關 (Feature Flags):用來動態開啟或關閉某些功能。

將這些設定與程式碼分離,有以下幾個好處:

  • 環境區隔:你可以在不同的環境 (開發、測試、正式) 使用不同的設定,而不需要修改程式碼。
  • 易於維護:當設定改變時,你只需要修改設定檔,而不需要重新編譯整個應用程式。
  • 符合單一職責原則:程式碼只負責執行邏輯,而組態則負責提供外部設定。

.NET Core 5+ 組態系統的運作方式

在 .NET Core 5+ 中,組態的運作是一個「讀取-載入-存取」的流程。

步驟 1:讀取組態來源

框架在應用程式啟動時,會自動讀取多個組態來源。這些來源的資料會被合併,且後讀取的會覆蓋先前的

預設的組態來源順序為:

  1. appsettings.json
  2. appsettings.[環境名稱].json (例如:appsettings.Development.json)
  3. 使用者機密 (User Secrets,僅在開發環境)
  4. 環境變數 (Environment Variables)
  5. 命令列引數 (Command-line Arguments)

範例

假設我們有以下 appsettings.json 檔案:

1
2
3
4
5
6
7
8
9
{
"Logging": {
"LogLevel": {
"Default": "Information"
}
},
"MyStringKey": "Hello World",
"MyNumberKey": 123
}

如果 appsettings.jsonappsettings.Development.json 都有一個名為 MyStringKey 的設定,則最終生效的會是 appsettings.Development.json 裡面的值,因為它在後面被讀取。

步驟 2:載入到 IConfiguration 物件

1
2
3
builder.Configuration.AddJsonFile("appsettings.json")
.AddJsonFile("appsettings.Development.json")
.AddEnvironmentVariables();

上方程式碼會將所有讀取到的組態資料,以鍵值對 (key-value pair) 的形式載入到記憶體中,並將它封裝成一個抽象化的 IConfiguration 物件

這個 IConfiguration 物件就像是一個巨大的字典,你的程式碼只需要從這個字典中查詢資料,而不需要知道這些資料是從哪個檔案或來源來的。

在記憶體中,上述的 appsettings.json 檔案會被轉換為類似以下形式的鍵值對:

  • Logging:LogLevel:Default -> "Information"
  • MyStringKey -> "Hello World"
  • MyNumberKey -> "123"

步驟 3:使用 IConfiguration 存取組態

IConfiguration 讀取資料的方式主要有兩種:GetValueGetSection

GetValue<T>:取得單一值

GetValue<T>() 用來取得一個單一、簡單的組態值,並且可以將該值自動轉換成你指定的類型。

  • 用法: builder.Configuration.GetValue<string>("鍵名")
  • 範例:
    • string logLevel = builder.Configuration.GetValue<string>("Logging:LogLevel:Default");
    • int myNumber = builder.Configuration.GetValue<int>("MyNumberKey");
  • 特色: 可以用冒號 : 來代表層級,直接取得巢狀結構中的值。

GetSection:取得一個子區塊

GetSection() 用來取得一個組態的子區塊。它回傳一個 IConfigurationSection 物件,這個物件本身也實作了 IConfiguration 介面,讓你可以繼續在它上面呼叫方法。

  • 用法: builder.Configuration.GetSection("區塊名稱")
  • 範例:
    • var loggingSection = builder.Configuration.GetSection("Logging");
    • 可以用冒號 :GetValue<T>()
      1
      2
      3
      var logLevel = builder.Configuration
      .GetSection("Logging:LogLevel")
      .GetValue<string>("Default");
  • 特色:
    • 回傳一個物件,這個物件可以繼續被查詢。
    • GetSection 不會回傳 null,即使找不到對應的區塊,它也會回傳一個空物件,避免了空指標例外 (Null Reference Exception)。
    • 這是將組態綁定到 C# 類別的起手式。
    • IConfigurationSection 提供 Bind() 將組態的區塊綁訂到 C# 類別。因此,實務上,GetSection 通常搭配 Bind() 方法使用

IOptions:將組態綁定到 C# Options 物件

雖然直接從 IConfiguration 讀取資料很方便,但如果組態很多,一個個 GetValue 會讓程式碼很凌亂。這時,我們就需要將組態綁定到一個簡單的 C# 類別,這個類別就稱為 Options 物件,例如:下方的 EmailSettingOptions。它的職責是「提供」這個 C# 類別的物件,例如:提供 EmailSettingOptions 物件。

IOptions 家族就是用來管理這些 Option 物件的介面。

1. IOptions<T> (單例 Singleton)

此介面將 Option 物件註冊為單例 (Singleton) 服務。

  • 功能: 組態值在應用程式啟動時只會被讀取並初始化一次,然後注入到你的類別中。
  • 特性: 應用程式啟動後,一旦設定完成,它的值在整個應用程式的生命週期中都不會改變
  • 適用場景: 幾乎所有情況都適用,特別是那些在程式運行期間不會改變的設定。例如資料庫連線字串、API Key 等。

2. IOptionsSnapshot<T> (範圍 Scoped)

此介面將 Option 物件註冊為範圍 (Scoped) 服務。

  • 功能: 讓你在每個 HTTP 請求中重新讀取一次組態。
  • 特性: 每次 HTTP 請求都會建立一個新的實體,同一個請求內,組態值會保持不變。這能確保單次操作中的資料一致性。。
  • 適用場景: 很少使用,主要用於當你希望在一個請求中,所有讀取的組態值都保持一致,即使組態檔案在請求期間有變動。例如,基於請求標頭 (Header) 或 URL 參數動態載入的設定。

3. IOptionsMonitor<T> (可監控單例 Singleton)

此介面將 Option 物件註冊為單例 (Singleton) 服務,但它提供監控組態變動的功能。

  • 功能: 讓你的程式碼可以監控組態的變動,並在變動時自動更新
  • 特性: 提供一個 OnChange 事件,你可以訂閱這個事件來接收組態變更的通知,並執行額外的邏輯。
  • 適用場景: 需要「熱重載」(Hot Reload) 的組態,例如動態變更記錄等級 (Log Level) 或服務的開關 (Feature Flags),而不需要重啟應用程式。

範例:用 builder.Services.Configure<T>() 將組態綁定到類別,再用 IOptions<T> 在其他 Service 取得

這是一個最常見的用法,它將 appsettings.jsonEmailSettings 區塊綁定到 EmailSettingOptions 類別。

步驟 1:定義 Options 類別

1
2
3
4
5
6
public class EmailSettingOptions
{
public string SmtpServer { get; set; }
public int Port { get; set; }
public string SenderName { get; set; }
}

步驟 2:在 Program.cs 註冊服務

1
2
// Program.cs
builder.Services.Configure<EmailSettingOptions>(builder.Configuration.GetSection("EmailSettings"));

這行程式碼會將 EmailSettingOptions 類別註冊到 DI 容器,並告訴它:「請用 IConfigurationEmailSettings 這個區塊的資料來設定這個類別」,也就是將組態綁定到類別。

如果不要用組態來設定,可以用如下:

1
2
3
4
5
builder.Services.Configure<EmailSettingOptions>(options =>
{
options.SmtpServer = "localhost"; // 直接設定一個固定值
options.Port = 25;
});

3. 使用 IOptions 注入並存取

1
2
3
4
5
6
7
8
9
10
public class MyEmailService
{
private readonly EmailSettingOptions _emailSettings;

// 透過 IOptions<T> 注入
public MyEmailService(IOptions<EmailSettingOptions> options)
{
_emailSettings = options.Value; // 透過 .Value 屬性取得 Option 物件
}
}

IConfigureOptions:將組態邏輯獨立成類別

IConfigureOptions<T> 是另一個用來設定 Options 的介面,它的職責是「直接配置」這個 T 物件本身。它的主要用途是將複雜的設定邏輯與你的啟動程式碼 (例如 Program.cs) 分離。

IConfigureOptions<T> vs. Configure<T>()

Configure<T>() (Lambda 函式) IConfigureOptions<T> (介面實作)
用法 簡潔,直接用 Lambda 函式設定 將設定邏輯封裝在一個獨立的類別
主要優點 方便、程式碼精簡 可重複使用、支援依賴注入
適用場景 簡單的設定,直接從檔案綁定 複雜的設定,需要依賴其他服務或進行額外的邏輯處理
範例 services.Configure<EmailSettingOptions>(options => { ... }); services.ConfigureOptions<ConfigureEmailSettings>();

範例:使用 IConfigureOptions

假設你的設定需要從 IConfiguration 讀取,並進行額外處理。

步驟 1:建立 IConfigureOptions 實作類別

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;

public class ConfigureEmailSettings : IConfigureOptions<EmailSettingOptions>
{
private readonly IConfiguration _configuration;

public ConfigureEmailSettings(IConfiguration configuration)
{
_configuration = configuration;
}

public void Configure(EmailSettingOptions options)
{
_configuration.GetSection("EmailSettings").Bind(options); // GetSection 搭配 Bind,將組態綁定到類別

// 額外邏輯:如果沒有設定寄件人名稱,則給一個預設值
if (string.IsNullOrEmpty(options.SmtpServer))
{
options.SmtpServer = "default.smtp.server";
}
}
}

步驟 2:在 Program.cs 註冊服務

1
2
3
4
5
6
7
// Program.cs
// 這是最簡單的註冊方式,它會自動處理底層的註冊細節
builder.Services.ConfigureOptions<ConfigureEmailSettings>();

// 或者,你也可以用底層的註冊方式:
// builder.Services.AddSingleton<IConfigureOptions<EmailSettingOptions>, ConfigureEmailSettings>();
// builder.Services.AddOptions<EmailSettingOptions>();

ConfigureOptions<T> (或者 Configure<T>) 是 .NET Core 5+ 提供的一個語法糖 (Syntactic Sugar)

builder.Services.ConfigureOptions<ConfigureEmailSettings>(); 在底層其實就等同於如下程式碼,避免開發者忘記註冊 AddOptions<T>()

1
2
3
4
5
6
7
8
// 告訴 DI 容器:「當有人需要 IConfigureOptions<EmailSettingOptions> 這個服務時,
// 請建立一個 ConfigureEmailSettings 的單例 (Singleton) 實體給他
builder.Services.AddSingleton<IConfigureOptions<EmailSettingOptions>, ConfigureEmailSettings>();

// ConfigureEmailSettings 裡會用到 EmailSettingOptions。
// 沒註冊的話,DI 容器可能不知道如何從它來建立 IOptions<T> 物件
// AddOptions<T> 是專門為 Option 系統設計的輔助方法,它會幫你完成一些預設的設定,例如註冊 IOptions<EmailSettingOptions>
builder.Services.AddOptions<EmailSettingOptions>();

總結

  • 組態:應用程式的設定檔。
  • IConfiguration:一個存在於記憶體中的字典,用來存取所有組態資料。
  • IOptions 家族:將 IConfiguration 的資料綁定到 C# 類別,並以不同生命週期管理。職責是「提供」C# 類別的物件。
  • IConfigureOptions<T>:一個介面,讓你將複雜的設定邏輯獨立成一個可重複使用的類別。職責是「直接配置」這個 T 物件本身。