工作中的 C# 實作:.NET 組態系統 - IOptions 與 IConfigureOptions
前言
在 .NET Core 5+ 的開發中,處理應用程式的組態 (Configuration) 是一個很重要的議題。這套全新的組態系統,目的在解決傳統 .NET Framework 時代組態管理的痛點,讓程式碼更具彈性、更好維護,也更符合現代軟體開發的設計模式。
什麼是組態?為什麼需要它?
組態就像是你的應用程式的「設定檔」。它包含了所有在程式碼之外、但會影響程式行為的資訊。
例如:
- 資料庫連線字串:程式需要知道要連線到哪一個資料庫。
- 第三方服務的 API 金鑰:例如,寄送 Email 服務的 API 金鑰。
- 功能開關 (Feature Flags):用來動態開啟或關閉某些功能。
將這些設定與程式碼分離,有以下幾個好處:
- 環境區隔:你可以在不同的環境 (開發、測試、正式) 使用不同的設定,而不需要修改程式碼。
- 易於維護:當設定改變時,你只需要修改設定檔,而不需要重新編譯整個應用程式。
- 符合單一職責原則:程式碼只負責執行邏輯,而組態則負責提供外部設定。
.NET Core 5+ 組態系統的運作方式
在 .NET Core 5+ 中,組態的運作是一個「讀取-載入-存取」的流程。
步驟 1:讀取組態來源
框架在應用程式啟動時,會自動讀取多個組態來源。這些來源的資料會被合併,且後讀取的會覆蓋先前的。
預設的組態來源順序為:
appsettings.json
appsettings.[環境名稱].json
(例如:appsettings.Development.json
)- 使用者機密 (User Secrets,僅在開發環境)
- 環境變數 (Environment Variables)
- 命令列引數 (Command-line Arguments)
範例
假設我們有以下 appsettings.json
檔案:
1 | { |
如果 appsettings.json
和 appsettings.Development.json
都有一個名為 MyStringKey
的設定,則最終生效的會是 appsettings.Development.json
裡面的值,因為它在後面被讀取。
步驟 2:載入到 IConfiguration
物件
1 | builder.Configuration.AddJsonFile("appsettings.json") |
上方程式碼會將所有讀取到的組態資料,以鍵值對 (key-value pair) 的形式載入到記憶體中,並將它封裝成一個抽象化的 IConfiguration
物件。
這個 IConfiguration
物件就像是一個巨大的字典,你的程式碼只需要從這個字典中查詢資料,而不需要知道這些資料是從哪個檔案或來源來的。
在記憶體中,上述的 appsettings.json
檔案會被轉換為類似以下形式的鍵值對:
Logging:LogLevel:Default
->"Information"
MyStringKey
->"Hello World"
MyNumberKey
->"123"
步驟 3:使用 IConfiguration
存取組態
從 IConfiguration
讀取資料的方式主要有兩種:GetValue
和 GetSection
。
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
3var 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.json
的 EmailSettings
區塊綁定到 EmailSettingOptions
類別。
步驟 1:定義 Options 類別
1 | public class EmailSettingOptions |
步驟 2:在 Program.cs
註冊服務
1 | // Program.cs |
這行程式碼會將 EmailSettingOptions
類別註冊到 DI 容器,並告訴它:「請用 IConfiguration
中 EmailSettings
這個區塊的資料來設定這個類別」,也就是將組態綁定到類別。
如果不要用組態來設定,可以用如下:
1 | builder.Services.Configure<EmailSettingOptions>(options => |
3. 使用 IOptions
注入並存取
1 | public class MyEmailService |
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 | using Microsoft.Extensions.Configuration; |
步驟 2:在 Program.cs
註冊服務
1 | // Program.cs |
ConfigureOptions<T>
(或者 Configure<T>
) 是 .NET Core 5+ 提供的一個語法糖 (Syntactic Sugar)。
builder.Services.ConfigureOptions<ConfigureEmailSettings>();
在底層其實就等同於如下程式碼,避免開發者忘記註冊 AddOptions<T>()
:
1 | // 告訴 DI 容器:「當有人需要 IConfigureOptions<EmailSettingOptions> 這個服務時, |
總結
- 組態:應用程式的設定檔。
IConfiguration
:一個存在於記憶體中的字典,用來存取所有組態資料。IOptions
家族:將IConfiguration
的資料綁定到 C# 類別,並以不同生命週期管理。職責是「提供」C# 類別的物件。IConfigureOptions<T>
:一個介面,讓你將複雜的設定邏輯獨立成一個可重複使用的類別。職責是「直接配置」這個 T 物件本身。