.Net: 使用 IOptions 取得 appsettings.json 的設定值吧
 
因為手邊的 .Net Core API 專案越來越多,蠻常會需要讀 appsetting.json 的 Config,每次都要重找文章有點麻煩,這邊就來筆記一篇。
認識 Appsettings.json
首先簡單介紹一下 appsettings.json 是在幹嘛的:
我們開發的時候,常常會需要先設定好一些服務參數、組態設定之類的設定值,再用這些設定值用來控制我們程式的某些行為。例如:
- 「某功能的上限值是 10」
- 「某項開關在測試環境是 false」
- 「某服務信件的發送者要用 noreply9527」
另外常見的還有連線字串、寫 Log 時的 logging level 等等。
這些設定值會和程式碼拆開,放在設定檔集中管理,再讓程式碼從設定檔讀取相關的設定值來用就好,藉此來把設定值的管理和使用做個關注點分離。
有了設定檔,要調整修改也比較方便:要增加或是修改設定值,都只要先往設定檔衝就行。並且因為兩邊拆開了,我們也就可以簡單地替換這些設定值來應對不同狀況(例如正式環境和測試環境套用兩組不同的設定檔,或是在自己電腦測試的時候快速改個值之類的),彈性可說是 UPUP!
更重要的是,這樣我們就不需要把一大堆東西寫死在程式碼的各個地方,也就不會要改個值還要先搜尋整個專案再一個一個挖出來改了。我按 Shift Ctrl F 已經按到哭
把設定值抽出去丟到設定檔之後,我們就得到了:集中管理設定值、方便修改和替換、減少程式碼中又重複又寫死的臭東西等等好處。
在 .Net Core 的世界裡,這個設定檔就是 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,所以大家都會用同一組。也只有第一次建立的時候會抓設定檔的內容。後面就算跑去偷改檔案,也不會被影響(想更新?重啟站台吧)
如果設定檔不太常改動的話,直接用 IOptions 簡單做一做是最方便的,也省資源。
IOptionsMonitor
IOptionsMonitor 同樣也會註冊為 Singleton,但是它會去偷聽設定檔有沒有更新。
當設定檔有更新的時候 IOptionsMonitor 也會一起更新,所以能夠隨時取得目前版本的設定值
取值的方法名稱也很明確表達這點,大家都是 _options.Value,
但 IOptionsMonitor 的是 _options.CurrentValue
_options = _options.Value,
_optionsMonitor = _optionsMonitor.CurrentValue,
如果我們的功能非常依賴設定值,而且又希望隨時更新(像留言區 Cash 大補充的 Hot Reload)的時候,就可以考慮使用 IOptionsMonitor。
但要小心如果 API 正在處理 Request,然後又剛好正在修改設定檔的話,可能會有一些靈異現象(?)
弄這篇筆記的時候也動手試了一下 IOptionsMonitor,但有點小佔版面,就放在最後的附錄(讓我之後可以回來抄)了。
IOptionsSnapshot
IOptionsSnapshot 會註冊為 Scope,所以每個請求進來的時候,都會各自去拿一次目前的設定檔內容,並且就用這一份設定檔內容處理這一次請求
我個人感覺最中規中矩。吃得到設定檔的變動,但也不會像 IOptionsMonitor 
搞到前一秒還是 true 下一秒就是 false🤔
如果有改動 Config 的需求,又能接受下一組 Request 進來才吃到的時候(或是希望不要發生靈異現象),就可以考慮使用 IOptionsSnapshot。
大概這樣。但我個人平常都還是 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,也覺得我的文章有幫上忙的話,還請不吝給我拍拍手呦,謝謝~ ;)
