ASP.NET Core

2023-03-23 15:00:25

上一篇 ASP.NET Core - 選項系統之選項設定 中提到 IOptions、IOptionsMonitor 和 IOptionsSnapshot 三個介面,通過這三個介面都可以從依賴注入容器中解析出已經設定的選項類,在我們通過 Configure 方法設定選項時,這三個介面會被同時註冊,但三個介面是有區別的,適用場景也有所不同。

1. IOptions

  • IOptions 物件的生命週期是 Singleton (單例),它可以在任意地方進行注入使用
  • 該介面物件在第一次使用的時候被範例化,並且選項類中的內容會一直保持不變,前面也提過選項類內容可以在設定來源修改之後更新,但是通過 IOption 解析的選項類不會隨著更新而改變
  • IOptions 介面不支援命名選項模式,它是沒有 get 方法的,也並不會預設讀取第一個,它只能讀取 String.Empty 預設命名的選項,如果沒有設定預設選項的話,雖然也能解析出 Options 選項類物件,但是物件的屬性都是相應型別的預設值(參照型別是 null,值型別是 0,其他的也都是相應型別的預設值)
public class OptionController : ControllerBase
{
	private readonly BlogOptions _blogOptions;
	public OptionController(IOptions<BlogOptions> options)
	{
		// 通過 IOptions<TOptions> 介面的 Value 屬性讀取選項類
		// 選項類始終是程式啟動時載入的值,不會改變
		_blogOptions = options.Value;
	}
}

2. IOptionsMonitor

  • IOptionsMonitor 物件的生命週期是 Scoped(作用域),Scoped 生命週期的特點是不能注入到 Singleton 服務中
  • 在作用域中(最常見的一次Http請求),建立 IOptionsSnapshot 物件範例時,會從設定中讀取最新選項值作為快照,並在當前作用域中始終使用該快照。也就是說一次請求中選項類內容保持不變,但是不同請求中可能因為設定來源的修改而不同
  • IOptionsMonitor 支援命名選項
public class OptionController : ControllerBase
{
	private readonly BlogOptions _blogOptions;
	public OptionController(IOptionsSnapshot<BlogOptions> optionsSnapshot)
	{
		// IOptionsSnapshot<TOptions> 可以通過 Value 屬性讀取預設的命名的選項類, Options 物件範例建立時讀取的設定快照
		_blogOptions = optionsSnapshot.Value;
		// 也可以通過 Get 方法獲取某一個命名選項,沒有指定命名時,預設命名為 string.Empty
		//_blogOptions = optionsSnapshot.Get(string.Empty);
	}
}

3. IOptionsSnapshot

  • IOptionsSnapshot 物件的生命週期也是 Singleton (單例)
  • 通過 IOptionsSnapshot 介面注入的物件每次讀取選項值時,都是從設定中讀取最新選項值,能夠實時獲取設定來源的更改
  • 該介面支援命名選項模式
  • 除了可以檢視 TOptions 的值,還可以監控 TOptions 設定的更改,支援重新載入設定(CurrentValue),並當設定發生更改時,進行通知(OnChange),支援快取與快取失效 (IOptionsMonitorCache),每次呼叫範例的 CurrentValue 時,會先檢查快取(IOptionsMonitorCache)是否有值,如果有值,則直接用,如果沒有,則從設定中讀取最新選項值,並記入快取。當設定發生更改時,會將快取清空。
public class OptionController : ControllerBase
{
	private readonly BlogOptions _blogOptions;
	public OptionController(IOptionsMonitor<BlogOptions> optionsMonitor)
	{
		// IOptionsMonitor<TOptions> 介面沒有 Value 屬性,通過 CurrentValue 獲取選項類物件,
		// 每次呼叫 CurrentValue都會實時讀取設定源,始終是最新設定的值
		_blogOptions = optionsMonitor.CurrentValue;
		// 該介面也支援通過 Get 方法獲取命名選項
		_blogOptions = optionsMonitor.Get(string.Empty);
		// 可以通過 OnChange 註冊事件,當設定被載入時會觸發事件
		optionsMonitor.OnChange(OnOptionsChange);
	}

	[HttpGet]
	public Task<BlogOptions> Get()
	{
		return Task.FromResult(_blogOptions);
	}

	private void OnOptionsChange(BlogOptions options)
	{
		Console.WriteLine(JsonSerializer.Serialize(options));
	}
}

啟動應用,呼叫一次 Get 介面,在 Api 控制器建構函式中註冊了設定載入觸發事件,之後修改 appsettings.json 組態檔中選項類對於的設定節點內容,可以看到事件觸發,控制檯中輸出了改變之後的選項類內容。

4. 三個介面的選項讀取機制演示

三個介面解析的選項類的差別,可以通過以下測試清楚得看出:

組態檔中初始選項節點如下:

"Blog": {
    "Title": "ASP.NET Core Options11",
    "Content": "This is a blog about Options System in ASP.NET Core Framework.",
    "CreateTime": "2022-12-06"
}

這裡為了方便看出 Scoped 生命週期 IOptionSnapeshoot 介面的變化,所有通過 Web Api 介面來測試

public class OptionController : ControllerBase
{
	private readonly IOptions<BlogOptions> _blogOptions;
	private readonly IOptionsSnapshot<BlogOptions> _blogSnapshotOptions;
	private readonly IOptionsMonitor<BlogOptions> _blogMonitorOptions;
	public OptionController(
		 IOptions<BlogOptions> options,
		IOptionsSnapshot<BlogOptions> optionsSnapshot,
		IOptionsMonitor<BlogOptions> optionsMonitor
		)
	{
// 注意這裡不能再把選項類物件先讀取出來,否則選項類物件也不會再改變了
		_blogOptions = options;
		_blogSnapshotOptions = optionsSnapshot;
		_blogMonitorOptions = optionsMonitor;
	}

	[HttpGet]
	public Task Get()
	{
		Console.WriteLine("第一次讀取設定:");
		Console.WriteLine("IOptions<TOptions>:" + JsonSerializer.Serialize(_blogOptions.Value));
		Console.WriteLine("IOptionsSnapshot<TOptions>:" + JsonSerializer.Serialize(_blogSnapshotOptions.Value));
		Console.WriteLine("IOptionsMonitor<TOptions>:" + JsonSerializer.Serialize(_blogMonitorOptions.CurrentValue));

		Console.WriteLine("請修改組態檔!");
		Console.ReadKey();

		Console.WriteLine("第二次讀取設定:");
		Console.WriteLine("IOptions<TOptions>:" + JsonSerializer.Serialize(_blogOptions.Value));
		Console.WriteLine("IOptionsSnapshot<TOptions>:" + JsonSerializer.Serialize(_blogSnapshotOptions.Value));
		Console.WriteLine("IOptionsMonitor<TOptions>:" + JsonSerializer.Serialize(_blogMonitorOptions.CurrentValue));

		return Task.CompletedTask;
	}
}

之後啟動應用呼叫 Get 介面,並在過程中將組態檔內容修改為以下:

"Blog": {
    "Title": "ASP.NET Core Options222",
    "Content": "This is a blog about Options System in ASP.NET Core Framework.",
    "CreateTime": "2022-12-06"
}

可以看到控制檯的輸出中,第二次讀取設定的時候,IOptionsMonitor 介面讀取到的內容已經改變

之後不要關閉應用,再調一次 Get 介面,並在過程中再次修改設定如下:

"Blog": {
    "Title": "ASP.NET Core Options333",
    "Content": "This is a blog about Options System in ASP.NET Core Framework.",
    "CreateTime": "2022-12-06"
}

這一次的 Get 請求的輸出結果如下:

可以看到 IOptionsMonitor 介面每次都能獲取到組態檔的實時值,IOptionsSnapshot 介面相較於第一次呼叫 Get 介面的時候已經改變,獲取到了之前修改的值,但是之後的修改它又獲取不到了,因為它是 Scoped 生命週期,在一次請求內是保持一致的,而 IOptions 介面獲取到的選項類物件是一致不變的。



參考文章:
ASP.NET Core 中的選項模式 | Microsoft Learn
選項模式 - .NET | Microsoft Learn
面向 .NET 庫建立者的選項模式指南 - .NET | Microsoft Learn
理解ASP.NET Core - 選項(Options)



ASP.NET Core 系列:

目錄:ASP.NET Core 系列總結
上一篇:ASP.NET Core - 選項系統之選項設定