.Net: 使用 IOptions 取得 appsettings.json 的設定值吧
因為手邊的 .Net Core API 專案越來越多,蠻常會需要讀 appsetting.json 的 Config,每次都要重找文章有點麻煩,這邊就來筆記一篇。
首先簡單介紹一下 appsettings.json 是在幹嘛的:
我們開發的時候,常常會需要弄一些設定值,再用這些設定值用來控制我們程式的某些行為。
例如「某功能的上限值是 10
」、「某項開關在測試環境是 false
」、「某服務信件的發送者要用 noreply9527
」,另外常見的還有連線字串、寫 Log 時的 logging level 等等
這些設定值會和程式碼拆開,放在設定檔集中管理,再讓程式碼從設定檔讀取相關的設定值來用就好。有了設定檔,要調整修改也比較方便:要增加或是修改設定值,都只要先往設定檔衝就行。
既然兩邊拆開了,我們也就可以簡單地替換這些設定值來應對不同狀況(例如正式環境和測試環境套用兩組不同的設定檔,或是在自己電腦測試的時候快速改個值之類的),彈性可說是 UPUP
更重要的是,這樣我們就不需要把一大堆東西寫死在程式碼的各個地方,也就不會要改個值還要先搜尋整個專案再一個一個挖出來改了。我按 Shift Ctrl F 已經按到哭
把設定值抽出去丟到設定檔之後,我們就得到了:集中管理設定值、方便修改和替換、減少程式碼中又重複又寫死的臭東西等等好處。
而在 .Net Core 開始的 .Net API 框架裡,這個設定檔就是 appsettings.json。
前面提到的像是日誌等級、功能旗標之類的這些設定值,就會放在 appsettings.json 裡面。
而當我們想要從 appsettings.json 把這些設定值給讀出來的時候,
就可以使用我們的 IOptions 啦!
使用 IOptions 來註冊 & 注入
提醒:這篇的示範會用到一些些 .Net 依賴注入(DI)相關的操作。沒接觸過的朋友可以考慮先閱讀菜雞新訓記:依賴注入
假設我們有個專案,叫做大漢防禦管理系統。專案內的 appsettings.json 有以下內容:
{
"StrongholdInfo": {
"Index": 49,
"Name": "劍閣",
"Enabled": true,
"General": [
"姜維",
"廖化",
"張翼",
"董厥"
]
}
}
我們正好在開發一個新功能,需要抓到這段設定值。現在就來示範一下:
首先,讓我們建立一個類別,等等用來放設定值內容(通常後綴會用 Options)
/// <summary>
/// 關隘資訊
/// </summary>
public class StrongholdInfoOptions
{
/// <summary>
/// 關隘編號
/// </summary>
public int Index { get; set; }
/// <summary>
/// 關隘名稱
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// 關隘啟用狀態
/// </summary>
public bool Enabled { get; set; }
/// <summary>
/// 駐守人員
/// </summary>
public string[]? General { get; set; }
}
接著使用 Configure<T>
來註冊,並且用 Configuration.GetSection
來指定這段設定值在 appsettings.json 裡的位置:
// 原本就有的一些註冊...
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// 使用 Configure 註冊 Option
builder.Services.Configure<StrongholdInfoOptions>(
builder.Configuration.GetSection("StrongholdInfo"));
註冊好了之後,就可以使用 IOptions<T>
注入到我們要用的地方囉
[ApiController]
[Route("[controller]")]
public class DemoController : ControllerBase
{
private readonly StrongholdInfoOptions _info;
// 使用 IOptions<T> 注入,並用 .Value 取得內容
public DemoController(
IOptions<StrongholdInfoOptions> info)
{
_info = info.Value;
}
[HttpGet("IOption")]
public object Get()
{
return _info;
}
}
收工,搞定。就是這麼簡單。
補充:AddOptions & 對 IOptions 加入驗證
前面我們在註冊時,是使用 Configure
:
// 使用 Configure 註冊 Option
builder.Services.Configure<StrongholdInfoOptions>(
builder.Configuration.GetSection("StrongholdInfo"));
其實也可以使用 AddOptions
:
// 使用 Configure 註冊 Option
builder.Services.Configure<StrongholdInfoOptions>(
builder.Configuration.GetSection("StrongholdInfo"));
// 或是使用 AddOptions,這兩個做法最後都會呼叫 Configure<StrongholdInfoOptions>
// see: https://github.com/dotnet/extensions/issues/514
// ps: `BindConfiguration` 是比較新的語法,以前會使用 `Bind() + builder.Configuration.GetSection()`
builder.Services
.AddOptions<StrongholdInfoOptions>()
.BindConfiguration(StrongholdInfoOptions.SectionName);
這兩個語法最終會做同樣的事情,因為 Bind()
會去呼叫 Configure()
- c# - 在 ASP.NET Core 中加載配置時,services.Configure() 和 services.AddOptions
().Bind() 之間有什麼區別?- Stack Overflow - Question: AddOptions
() vs. Multiple Configure (…) · Issue #514 · dotnet/extensions (github.com)
只是 AddOptions
比較晚出現,並且後來又加入了更多自定義,用起來比較靈活。
例如我們可以用 ValidateDataAnnotations
來啟用屬性驗證:
(可參考 Microsoft Learn 選項模式 的「選項驗證」小節)
[Required]
[RegularExpression(@"^[\u4e00-\u9fa5]{1,10}$")] // 限定 1~10 個中文字
public string Name { get; set; }
builder.Services
.AddOptions<SettingsOptions>()
.Bind(Configuration.GetSection(SettingsOptions.ConfigurationSectionName))
.ValidateDataAnnotations() // 會在呼叫 .value 的時候進行驗證
// .Validate(config => {}) // 也可以自訂驗證邏輯
// .ValidateOnStart() // 也可以要求在啟動時就驗證
;
// 假如加了 ValidateDataAnnotations 的話,取值驗證失敗會噴 OptionsValidationException
try
{
SettingsOptions options = _config.Value;
}
catch (OptionsValidationException ex)
{
foreach (string failure in ex.Failures) // 畢竟可能一堆東西沒通過驗證嘛
{
_logger.LogError("Validation error: {FailureMessage}", failure);
}
}
如果需要自訂驗證器,可以搜尋 IValidateOptions
,可以自己實作 Validate()
,但我個人還沒遇到這麼複雜的狀況,這邊就不詳述。
Configure
直接又簡單,但 AddOptions
比較靈活。
我個人比較喜歡粗暴直接的做法,所以目前是都直接呼叫 Configure
而已。供各位參考
補充:IOptions、IOptionsMonitor、IOptionsSnapshot
提了 IOptions 就不能不提他的兩位哥哥:IOptionsMonitor、IOptionsSnapshot
[ApiController]
public class OptionsMonitorDemoController : ControllerBase
{
// 我們有三種 IOptions 相關的介面來取得設定檔內容
private readonly IOptions<StrongholdInfoOptions> _options;
private readonly IOptionsMonitor<StrongholdInfoOptions> _optionsMonitor;
private readonly IOptionsSnapshot<StrongholdInfoOptions> _optionsSnapshot;
}
這邊也迅速筆記一下。有興趣的朋友可以直接閱讀相關文章:
- IOptions、IOptionsMonitor 以及 IOptionsSnapshot - wenhx - 博客园
- ASP.NET Core 中的選項模式 | Microsoft Learn 的「選項介面」小節
IOptions
用起來最簡單方便,個人推👍
IOptions 會註冊為 Singleton,所以大家都會用同一組。也只有第一次建立的時候會抓設定檔的內容。後面就算跑去偷改檔案,也不會被影響(想更新?重啟站台吧)
如果設定檔不太常改動的話,直接用這個簡單做一做是最方便的,也省資源。
IOptionsMonitor
IOptionsMonitor 同樣也會註冊為 Singleton,但是它會去偷聽設定檔有沒有更新。
當設定檔有更新的時候 IOptionsMonitor 也會一起更新,所以能夠隨時取得目前版本的設定值
取值的方法名稱也很明確表達這點,大家都是 _options.Value
,
但 IOptionsMonitor 的是 _options.CurrentValue
_options = _options.Value,
_optionsMonitor = _optionsMonitor.CurrentValue,
如果我們的功能非常依賴設定值,而且又希望隨時更新的時候,就可以考慮使用。
但要小心如果 API 正在處理 Request,然後又剛好正在修改設定檔的話,可能會有一些靈異現象
弄這篇筆記的時候也動手試了一下 IOptionsMonitor,但有點小佔版面,就放在最後的附錄(讓我之後可以回來抄)了。
IOptionsSnapshot
IOptionsSnapshot 會註冊為 Scope,所以每個請求進來的時候,都會各自去拿一次目前的設定檔內容,並且就用這一份設定檔內容處理這一次請求
我個人感覺最中規中矩。吃得到設定檔的變動,但也不會像 IOptionsMonitor
搞到前一秒還是 true 下一秒就是 false🤔
如果有改動 Config 的需求,又希望同個請求之間不要太奇異的時候就可以考慮使用
大概這樣。但我個人平常都還是 IOptions 優先,
如果真的有需要即時反應設定檔的變動時,再把另外兩個拿出來討論吧。
延伸閱讀
前面介紹了 .Net Core 裡 appsettings.json
和 IOptions
的基本操作,也順便補充了一些簡單介紹。其他相關的操作,就放在延伸閱讀這邊,有興趣的朋朋們可以看看。
如果想根據不同環境(Dev, Prd 之類的)切換不同 appsettings.json:
ASP.NET Core 依環境載入不同 appsetting.json 設定 - 黑暗執行緒
如果你正在搞功能旗標(Feature Flag/Feature Toggle)然後看到這篇的話,
也可以嘗試看看 FeatureManagement
:
.Net: 使用 FeatureManagement 套件來實作 Feature Flag 功能切換吧
如果覺得到處都是 IOptions,想要降低對 IOptions 的依賴的話,可以綁到強型別裡:
[NETCore] ASP.NET Core 使用強型別取代 IOption
參考資料
- ASP.NET Core 的設定 | Microsoft Learn
- ASP.NET Core 中的選項模式 | Microsoft Learn
- [.NETCore] 如何取得 appsettings.json 組態設定 ~ m@rcus 學習筆記 (marcus116.blogspot.com)
- IOptions、IOptionsMonitor以及IOptionsSnapshot - wenhx - 博客园 (cnblogs.com)
- .NET Configuration with IOptions, IOptionsMonitor, and IOptionsSnapshot | by Ludmal De Silva | Medium
附錄:試一下 IOptionsMonitor
簡單比較一下修改 appsettings.json 後,IOptions 和 IOptionsMonitor 的資料差異,方便我以後需要複製貼上,或是哪天需要甩給朋友時使用。
public OptionsMonitorDemoController(
IOptions<StrongholdInfoOptions> options,
IOptionsMonitor<StrongholdInfoOptions> optionsMonitor)
{
_options = options;
_optionsMonitor = optionsMonitor;
}
[HttpGet("api/Demo/GetWithMonitor")]
public IEnumerable<object> GetWithMonitor()
{
var before = new
{
OptionsName = _options.Value.Name,
OptionsMonitorName = _optionsMonitor.CurrentValue.Name,
};
// 在這裡下中斷點,打開 appsettings.json 手動改資料
// 把 "Name": "劍閣" 改成 "Name": "羅馬"
System.Threading.Thread.Sleep(1000);
var after = new
{
OptionsName = _options.Value.Name,
OptionsMonitorName = _optionsMonitor.CurrentValue.Name,
};
return new object[] { before, after };
// [
// {
// "optionsName": "劍閣",
// "optionsMonitorName": "劍閣"
// },
// {
// "optionsName": "劍閣",
// "optionsMonitorName": "羅馬"
// }
// ]
}
其他文章
哈囉,如果你也有 LikeCoin,也覺得我的文章有幫上忙的話,還請不吝給我拍拍手呦,謝謝~ ;)