【5min+】更好的選項(xiàng)實(shí)踐。.Net Core中的IOptions
系列介紹
【五分鐘的dotnet】是一個(gè)利用您的碎片化時(shí)間來(lái)學(xué)習(xí)和豐富.net知識(shí)的博文系列。它所包含了.net體系中可能會(huì)涉及到的方方面面,比如C#的小細(xì)節(jié),AspnetCore,微服務(wù)中的.net知識(shí)等等。
通過(guò)本篇文章您將Get:
- 不在AspNet Core的
Startup.cs中完成mvc的選項(xiàng)配置(比如在其它地方為MVC添加過(guò)濾器等操作) - 了解
Options的使用 - 了解
IOptions、IOptionsMonitor、IOptionsSnapshot的區(qū)別
時(shí)長(zhǎng)為五分鐘以?xún)?nèi),建議先投幣再上車(chē)觀看??
正文
.NET Core為咱們提供的默認(rèn)依賴(lài)注入方式[Microsoft.Extensions.DependencyInjection]相對(duì)來(lái)說(shuō)功能已經(jīng)很完善了,雖然有一些功能沒(méi)有實(shí)現(xiàn)(比如在使用factory進(jìn)行注冊(cè)時(shí)無(wú)法獲取type等),但并不影響我們令接口與實(shí)現(xiàn)進(jìn)行分離。
某些情況下,您會(huì)發(fā)現(xiàn),當(dāng)我們的業(yè)務(wù)類(lèi)被添加到依賴(lài)注入容器中時(shí),該類(lèi)構(gòu)造函數(shù)中所依賴(lài)的其它類(lèi)都得一同添加到容器(雖然有某些奇技淫巧可以規(guī)避,但是構(gòu)造函數(shù)注入依舊是規(guī)范的手段)。可是,我的一些依賴(lài)類(lèi)為選型類(lèi)型怎么辦呢?比如下面的代碼:
public class MyBusinessClass
{
public MyBusinessClass(SomeOptions options)
{
if (options.ShouldOpenTCP)
//do something.....
if (options.ShouldLogIndo)
// do something
}
}
SomeOptions是一個(gè)典型的選項(xiàng)項(xiàng)類(lèi)型,我們通過(guò)它公開(kāi)的一些屬性來(lái)對(duì)項(xiàng)目進(jìn)行配置。而當(dāng)MyBusinessClass被注入到容器的時(shí)候,意味著SomeOptions也需要被注入。
對(duì)于這種選項(xiàng)類(lèi)型,微軟給出了專(zhuān)門(mén)的處理手段:Microsoft.Extensions.Options包。我們只需要使用該包為IServiceCollection提供的擴(kuò)展方法AddOptions<TOptions>()就可以完成注入選項(xiàng):
services.AddOptions<SomeOptions>();
public class MyBusinessClass
{
public MyBusinessClass(IOptions<SomeOptions> options)
{
SomeOptions value = options.Value;
}
}
看起來(lái)這和上面的代碼好像區(qū)別也不是很大吧。都是把SomeOptions添加到容器中,那么第二種方法和第一種方法比起來(lái)有什么優(yōu)點(diǎn)呢?微軟專(zhuān)門(mén)推出該方式難道只是為了“年底沖業(yè)績(jī)”?

非也非也?第二種方式其實(shí)用了更好的解耦思想來(lái)設(shè)計(jì)。假如咱們的SomeOptions需要在其它模塊中修改怎么辦? 如果用第一種直接注入到容易的方案的話(huà),這就十分的困難。而使用AddOptions<TOptions>的方式您就可以輕而易舉。
Microsoft.Extensions.Options提供了IConfigureOptions和IPostConfigureOptions這兩種類(lèi)似于生命周期鉤子的接口,讓您能夠在讀取選項(xiàng)的時(shí)候,進(jìn)行某些操作。
在AspNetCore中試一試
在AspnetCore中就有一個(gè)很明顯的選項(xiàng):MvcOptions,該選項(xiàng)提供了咱們配置MVC項(xiàng)目的各種各樣的參數(shù)。
//Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers(options =>
{
options.Filters.Add(new MyFileter());
});
}
上面代碼是我們?cè)?code>Startup.cs中配置MvcOptions最最常見(jiàn)的步驟,這里我用添加一個(gè)全局過(guò)濾器來(lái)舉例。
如果我不想在Startup.cs中添加這句代碼怎么辦呢? 比如我寫(xiě)了一個(gè)第三方的庫(kù),庫(kù)中包含了N個(gè)過(guò)濾器,我肯定沒(méi)有辦法要求用戶(hù)在使用該庫(kù)的時(shí)候?qū)⑦@N個(gè)過(guò)濾器一個(gè)一個(gè)的添加到options中。(這里只是假設(shè),雖然可以使用特性的方式來(lái)完成同樣的過(guò)濾器功能)
這個(gè)時(shí)候就可以拿出我們上面講的一大殺器:IConfigureOptions.
internal class MvcOptionsConfigure : IConfigureOptions<MvcOptions>
{
public void Configure(MvcOptions options)
{
options.Filters.Add(new MyFileter());
}
}
services.AddSingleton<IConfigureOptions<MvcOptions>, MvcOptionsConfigure>();
這樣就完成了關(guān)注點(diǎn)的分離,我們不需要一直死守著Startup.cs文件不放,也不需要讓用戶(hù)手動(dòng)去配置。只要我們知道IServiceCollection就可以往里面添加我們自己的業(yè)務(wù)點(diǎn)。當(dāng)然,Microsoft.Extensions.Options包還提供了另外的方式讓您可以完成IConfigureOptions的同樣操作,不過(guò)這些操作都是像語(yǔ)法糖一樣,實(shí)質(zhì)上是相同的:
//和上面同樣的功能
services.Configure<MvcOptions>(Options =>
{
options.Filters.Add(new MyFileter());
});
IOptions、IOptionsMonitor和IOptionsSnapshot
在上面其實(shí)我們已經(jīng)見(jiàn)過(guò)了IOptions的尊容,我們可以通過(guò)注入IOptions<MyOptions>來(lái)獲取MyOptions實(shí)例。
但是!但是!但是!!!! IOptions還有兩個(gè)兄弟IOptionsMonitor和IOptionsSnapshot。光名字上長(zhǎng)的就很像了,它們都還有類(lèi)似于“Value”的屬性來(lái)獲取選項(xiàng)實(shí)例。
媽呀,那么它們到底有什么不同呢?什么時(shí)候該用老大,什么使用該用老二呢? 接下來(lái),年度最佳找不同大戲即將開(kāi)始………………
先來(lái)看看IOptions和IOptionsSnapshot吧,看看它們的接口定義:
/// <summary>
/// Used to access the value of <typeparamref name="TOptions"/> for the lifetime of a request.
/// </summary>
/// <typeparam name="TOptions">Options type.</typeparam>
public interface IOptionsSnapshot<out TOptions> : IOptions<TOptions> where TOptions : class, new()
{
/// <summary>
/// Returns a configured <typeparamref name="TOptions"/> instance with the given name.
/// </summary>
TOptions Get(string name);
}
我天,居然IOptionsSnapshot還繼承了IOptions,而且只是多了一個(gè)Get方法,那么是否這兩個(gè)類(lèi)其實(shí)很相似呢?我們直接來(lái)看看源碼:
services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptions<>), typeof(OptionsManager<>)));
services.TryAdd(ServiceDescriptor.Scoped(typeof(IOptionsSnapshot<>), typeof(OptionsManager<>)));
納尼?這都還不是相似不相似的問(wèn)題,這TM不是同一個(gè)實(shí)現(xiàn)嗎?只是接口類(lèi)型不同而已,實(shí)現(xiàn)都是OptionsManager<>。 那為啥要搞兩個(gè)不同的接口。
等等(手動(dòng)播放名偵探bgm),這倆生命周期咋不一樣? 一個(gè)是Singleton一個(gè)是Scoped。而再來(lái)看IOptionsSnapshot的說(shuō)明:“Used to access the value of TOptions for the lifetime of a request.”(用于在請(qǐng)求的生存期內(nèi)訪(fǎng)問(wèn)選項(xiàng)的值)。
原來(lái)如此,這樣看來(lái)就很清晰了。它倆的區(qū)別其實(shí)就是依賴(lài)注入的生命周期不同而已,為單例的IOptions意味著,只要您注入之后以后獲取的都是同一個(gè)實(shí)例,而IOptionsSnapshot呢,作為Scoped級(jí)別,每再一個(gè)新的Scoped中獲取它一次,它就會(huì)請(qǐng)求一個(gè)新的實(shí)例。
所以來(lái)舉個(gè)例子,在AspNet Core中咱們某個(gè)選項(xiàng)的值是根據(jù)一個(gè)文件的某個(gè)值來(lái)的。剛開(kāi)始文本的值是“A”,咱們?cè)谶\(yùn)行AspNet Core之后我們獲取IOptions<MyOptions>和IOptionsSnapshot<MyOptions>,此時(shí)得到的MyOptions的該屬性的值都是"A"。但是假如我們更改了文本的值,改為“B”。如果在發(fā)起一個(gè)http請(qǐng)求去獲取MyOptions的結(jié)果,此時(shí)IOptions<MyOptions>依舊是“A”,而IOptionsSnapshot<MyOptions>則更改為了B。
原因很簡(jiǎn)單,因?yàn)?code>IOptions<MyOptions>是單例的,所以從程序一開(kāi)始加載過(guò)一次之后,以后訪(fǎng)問(wèn)它都是這個(gè)結(jié)果,而IOptionsSnapshot<MyOptions>是Scoped級(jí)別的,所以每一個(gè)新的Scoped時(shí)都會(huì)又去訪(fǎng)問(wèn)文本文件獲取值,而一次Http請(qǐng)求就會(huì)開(kāi)啟一次新的Scoped,所以此時(shí)結(jié)果就成為“B”。這個(gè)時(shí)候我們大概就能讀懂上面IOptionsSnapshot<>接口的解釋了:“用于在請(qǐng)求的生存期內(nèi)訪(fǎng)問(wèn)選項(xiàng)的值”。
三兄弟一下就干掉了倆,接下來(lái)看看最后一個(gè)好兄弟(毒瘤):IOptionsMonitor。還是直接看它的源代碼呢:
services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitor<>), typeof(OptionsMonitor<>)));
納尼?單例? 那是不是意味著它也一樣,一旦啟動(dòng)了之后還是保持原有的結(jié)果呢?先不急,看看它的接口定義再說(shuō):
/// <summary>
/// Used for notifications when <typeparamref name="TOptions"/> instances change.
/// </summary>
/// <typeparam name="TOptions">The options type.</typeparam>
public interface IOptionsMonitor<out TOptions>
{
/// <summary>
/// Returns the current <typeparamref name="TOptions"/> instance with the <see cref="Options.DefaultName"/>.
/// </summary>
TOptions CurrentValue { get; }
/// <summary>
/// Returns a configured <typeparamref name="TOptions"/> instance with the given name.
/// </summary>
TOptions Get(string name);
/// <summary>
/// Registers a listener to be called whenever a named <typeparamref name="TOptions"/> changes.
/// </summary>
/// <param name="listener">The action to be invoked when <typeparamref name="TOptions"/> has changed.</param>
/// <returns>An <see cref="IDisposable"/> which should be disposed to stop listening for changes.</returns>
IDisposable OnChange(Action<TOptions, string> listener);
}
可以看出它自己是一個(gè)單獨(dú)的接口,并不像其它倆兄弟是繼承關(guān)系。而且該接口居然有一個(gè)OnChange簽名?而且該方法需要一個(gè)Action<TOptions, string>的參數(shù)。
握草(繼續(xù)手動(dòng)播放名偵探bgm),如果您有幸看過(guò)我的上一篇文章:《【5min+】 一個(gè)令牌走天下!.Net Core中的ChangeToken》,那么您可能一下就知道它扮演了什么樣的角色。(5min+系列居然是連續(xù)的.... ??)
再看看該接口的說(shuō)明:"Used for notifications when TOptions instances change."(用于在選項(xiàng)實(shí)例更改時(shí)進(jìn)行通知)。果然和我們猜的一模一樣,那么它的實(shí)現(xiàn)類(lèi)里面一定有咱們上一篇文章中提到的:ChangeToken和IChangeToken等東西。
來(lái)吧,扒開(kāi)它的具體實(shí)現(xiàn),驗(yàn)證咱們的猜想:
public OptionsMonitor(IOptionsFactory<TOptions> factory, IEnumerable<IOptionsChangeTokenSource<TOptions>> sources, IOptionsMonitorCache<TOptions> cache)
{
_factory = factory;
_sources = sources;
_cache = cache;
foreach (var source in _sources)
{
var registration = ChangeToken.OnChange(
() => source.GetChangeToken(),
(name) => InvokeChanged(name),
source.Name);
_registrations.Add(registration);
}
}
意料之中,也就是說(shuō)IOptionsMonitor<>的注入級(jí)別雖然是單例,但是因?yàn)樗哂?code>IChangeToken的實(shí)現(xiàn),所以它能夠在選項(xiàng)源改變的時(shí)候,“立馬對(duì)選項(xiàng)做出對(duì)應(yīng)的改變”。而改變依賴(lài)于IOptionsChangeTokenSource這個(gè)令牌源,目前.net core對(duì)很多常用工具都實(shí)現(xiàn)了該令牌源,比如Logger,Configuration等。所以當(dāng)我們某個(gè)選項(xiàng)依賴(lài)于IConfiguration(appsetting.json)的某一項(xiàng)時(shí),當(dāng)修改appsetting.json文件,該選項(xiàng)的值就能夠立馬得到更改。
所以來(lái)回過(guò)頭來(lái)看這三兄弟。它們的區(qū)別其實(shí)在于變更的時(shí)效性:
| 類(lèi)型 | 說(shuō)明 | 時(shí)效性 |
|---|---|---|
| IOptions | 一旦程序啟動(dòng),該選項(xiàng)的值就無(wú)法更改 | 無(wú)時(shí)效性可言 |
| IOptionsSnapshot | 當(dāng)開(kāi)啟一個(gè)新Scoped時(shí),就會(huì)重新計(jì)算選項(xiàng)的值 | 相對(duì)比較低,依賴(lài)于合適開(kāi)啟一個(gè)新的Scoped |
| IOptionsMonitor | 依賴(lài)于IChangeToken,只要令牌源變更則立刻做出反應(yīng) | 高 |
假如把IOptionsMonitor<MyOptions>添加到上面IOptions<MyOptions>和IOptionsSnapshot<MyOptions>的文件變更案例,如果在一次HTTP請(qǐng)求中,文件變更了兩次,那么IOptionsSnapshot<MyOptions>不會(huì)在第二次更改中同步更改,而IOptionsMonitor<MyOptions>則可以。
那么什么時(shí)候來(lái)使用什么樣的接口呢?相信這個(gè)時(shí)候,您的心里比我還要清楚。當(dāng)您的選項(xiàng)只是負(fù)責(zé)一次性處理的時(shí)候,應(yīng)用啟動(dòng)了就不需要更改,那么考慮使用IOptions<MyOptions>,如果是對(duì)數(shù)據(jù)源的變更要求很?chē)?yán)格,比如開(kāi)啟了一個(gè)“BackgroundJob”在后臺(tái)運(yùn)行,該job需要一個(gè)選項(xiàng)類(lèi)型,而該類(lèi)型依賴(lài)于配置文件,需要對(duì)配置文件更改時(shí)即刻做出改變,那么請(qǐng)考慮使用IOptionsMonitor<MyOptions>。
最后回過(guò)頭來(lái)看微軟官方文檔上關(guān)于“Options”的兩個(gè)點(diǎn)(ISP和關(guān)注點(diǎn)分離),您應(yīng)該一下就能理解。

如果您有興趣的話(huà)可以跳轉(zhuǎn)至官方文檔進(jìn)行閱讀:《ASP.NET Core 中的選項(xiàng)模式》
好啦,今天的車(chē)就開(kāi)完了,如果您喜歡該系列文章可以點(diǎn)擊關(guān)注。??
最后,偷偷說(shuō)一句:創(chuàng)作不易,點(diǎn)個(gè)推薦吧.....

我的博客即將同步至騰訊云+社區(qū),邀請(qǐng)大家一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=1fam5s82ttu5f

浙公網(wǎng)安備 33010602011771號(hào)