系列介紹
【五分鐘的dotnet】是一個利用您的碎片化時間來學習和豐富.net知識的博文系列。它所包含了.net體系中可能會涉及到的方方面面,比如C#的小細節,AspnetCore,微服務中的.net知識等等。
通過本篇文章您將Get:
- 不在AspNet Core的
Startup.cs
中完成mvc的選項配置(比如在其它地方為MVC新增過濾器等操作) - 瞭解
Options
的使用 - 瞭解
IOptions
、IOptionsMonitor
、IOptionsSnapshot
的區別
時長為五分鐘以內,建議先投幣再上車觀看?
正文
.NET Core為我們們提供的預設依賴注入方式[Microsoft.Extensions.DependencyInjection
]相對來說功能已經很完善了,雖然有一些功能沒有實現(比如在使用factory進行註冊時無法獲取type等),但並不影響我們令介面與實現進行分離。
某些情況下,您會發現,當我們的業務類被新增到依賴注入容器中時,該類建構函式中所依賴的其它類都得一同新增到容器(雖然有某些奇技淫巧可以規避,但是建構函式注入依舊是規範的手段)。可是,我的一些依賴類為選型型別怎麼辦呢?比如下面的程式碼:
public class MyBusinessClass
{
public MyBusinessClass(SomeOptions options)
{
if (options.ShouldOpenTCP)
//do something.....
if (options.ShouldLogIndo)
// do something
}
}
複製程式碼
SomeOptions
是一個典型的選項項型別,我們通過它公開的一些屬性來對專案進行配置。而當MyBusinessClass
被注入到容器的時候,意味著SomeOptions
也需要被注入。
對於這種選項型別,微軟給出了專門的處理手段:Microsoft.Extensions.Options
包。我們只需要使用該包為IServiceCollection
提供的擴充套件方法AddOptions<TOptions>()
就可以完成注入選項:
services.AddOptions<SomeOptions>();
public class MyBusinessClass
{
public MyBusinessClass(IOptions<SomeOptions> options)
{
SomeOptions value = options.Value;
}
}
複製程式碼
看起來這和上面的程式碼好像區別也不是很大吧。都是把SomeOptions
新增到容器中,那麼第二種方法和第一種方法比起來有什麼優點呢?微軟專門推出該方式難道只是為了“年底衝業績”?
非也非也?第二種方式其實用了更好的解耦思想來設計。假如我們們的SomeOptions
需要在其它模組中修改怎麼辦? 如果用第一種直接注入到容易的方案的話,這就十分的困難。而使用AddOptions<TOptions>
的方式您就可以輕而易舉。
Microsoft.Extensions.Options
提供了IConfigureOptions
和IPostConfigureOptions
這兩種類似於生命週期鉤子的介面,讓您能夠在讀取選項的時候,進行某些操作。
在AspNetCore中試一試
在AspnetCore中就有一個很明顯的選項:MvcOptions
,該選項提供了我們們配置MVC專案的各種各樣的引數。
//Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers(options =>
{
options.Filters.Add(new MyFileter());
});
}
複製程式碼
上面程式碼是我們在Startup.cs
中配置MvcOptions
最最常見的步驟,這裡我用新增一個全域性過濾器來舉例。
如果我不想在Startup.cs
中新增這句程式碼怎麼辦呢? 比如我寫了一個第三方的庫,庫中包含了N個過濾器,我肯定沒有辦法要求使用者在使用該庫的時候將這N個過濾器一個一個的新增到options
中。(這裡只是假設,雖然可以使用特性的方式來完成同樣的過濾器功能)
這個時候就可以拿出我們上面講的一大殺器:IConfigureOptions
.
internal class MvcOptionsConfigure : IConfigureOptions<MvcOptions>
{
public void Configure(MvcOptions options)
{
options.Filters.Add(new MyFileter());
}
}
services.AddSingleton<IConfigureOptions<MvcOptions>, MvcOptionsConfigure>();
複製程式碼
這樣就完成了關注點的分離,我們不需要一直死守著Startup.cs
檔案不放,也不需要讓使用者手動去配置。只要我們知道IServiceCollection
就可以往裡面新增我們自己的業務點。當然,Microsoft.Extensions.Options
包還提供了另外的方式讓您可以完成IConfigureOptions
的同樣操作,不過這些操作都是像語法糖
一樣,實質上是相同的:
//和上面同樣的功能
services.Configure<MvcOptions>(Options =>
{
options.Filters.Add(new MyFileter());
});
複製程式碼
IOptions、IOptionsMonitor和IOptionsSnapshot
在上面其實我們已經見過了IOptions
的尊容,我們可以通過注入IOptions<MyOptions>
來獲取MyOptions
例項。
但是!但是!但是!!!! IOptions
還有兩個兄弟IOptionsMonitor
和IOptionsSnapshot
。光名字上長的就很像了,它們都還有類似於“Value”的屬性來獲取選項例項。
媽呀,那麼它們到底有什麼不同呢?什麼時候該用老大,什麼使用該用老二呢? 接下來,年度最佳找不同大戲即將開始………………
先來看看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
,而且只是多了一個Get
方法,那麼是否這兩個類其實很相似呢?我們直接來看看原始碼:
services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptions<>), typeof(OptionsManager<>)));
services.TryAdd(ServiceDescriptor.Scoped(typeof(IOptionsSnapshot<>), typeof(OptionsManager<>)));
複製程式碼
納尼?這都還不是相似不相似的問題,這TM不是同一個實現嗎?只是介面型別不同而已,實現都是OptionsManager<>
。 那為啥要搞兩個不同的介面。
等等(手動播放名偵探bgm),這倆生命週期咋不一樣? 一個是Singleton
一個是Scoped
。而再來看IOptionsSnapshot
的說明:“Used to access the value of TOptions for the lifetime of a request.”(用於在請求的生存期內訪問選項的值)。
原來如此,這樣看來就很清晰了。它倆的區別其實就是依賴注入的生命週期不同而已,為單例的IOptions
意味著,只要您注入之後以後獲取的都是同一個例項,而IOptionsSnapshot
呢,作為Scoped級別,每再一個新的Scoped中獲取它一次,它就會請求一個新的例項。
所以來舉個例子,在AspNet Core中我們們某個選項的值是根據一個檔案的某個值來的。剛開始文字的值是“A”,我們們在執行AspNet Core之後我們獲取IOptions<MyOptions>
和IOptionsSnapshot<MyOptions>
,此時得到的MyOptions
的該屬性的值都是"A"。但是假如我們更改了文字的值,改為“B”。如果在發起一個http請求去獲取MyOptions
的結果,此時IOptions<MyOptions>
依舊是“A”,而IOptionsSnapshot<MyOptions>
則更改為了B。
原因很簡單,因為IOptions<MyOptions>
是單例的,所以從程式一開始載入過一次之後,以後訪問它都是這個結果,而IOptionsSnapshot<MyOptions>
是Scoped級別的,所以每一個新的Scoped時都會又去訪問文字檔案獲取值,而一次Http請求就會開啟一次新的Scoped,所以此時結果就成為“B”。這個時候我們大概就能讀懂上面IOptionsSnapshot<>
介面的解釋了:“用於在請求的生存期內訪問選項的值”。
三兄弟一下就幹掉了倆,接下來看看最後一個好兄弟(毒瘤):IOptionsMonitor
。還是直接看它的原始碼呢:
services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitor<>), typeof(OptionsMonitor<>)));
複製程式碼
納尼?單例? 那是不是意味著它也一樣,一旦啟動了之後還是保持原有的結果呢?先不急,看看它的介面定義再說:
/// <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);
}
複製程式碼
可以看出它自己是一個單獨的介面,並不像其它倆兄弟是繼承關係。而且該介面居然有一個OnChange
簽名?而且該方法需要一個Action<TOptions, string>
的引數。
握草(繼續手動播放名偵探bgm),如果您有幸看過我的上一篇文章:《【5min+】 一個令牌走天下!.Net Core中的ChangeToken》,那麼您可能一下就知道它扮演了什麼樣的角色。(5min+系列居然是連續的.... ?)
再看看該介面的說明:"Used for notifications when TOptions instances change."(用於在選項例項更改時進行通知)。果然和我們猜的一模一樣,那麼它的實現類裡面一定有我們們上一篇文章中提到的:ChangeToken
和IChangeToken
等東西。
來吧,扒開它的具體實現,驗證我們們的猜想:
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);
}
}
複製程式碼
意料之中,也就是說IOptionsMonitor<>
的注入級別雖然是單例,但是因為它具有IChangeToken
的實現,所以它能夠在選項源改變的時候,“立馬對選項做出對應的改變”。而改變依賴於IOptionsChangeTokenSource
這個令牌源,目前.net core對很多常用工具都實現了該令牌源,比如Logger,Configuration等。所以當我們某個選項依賴於IConfiguration
(appsetting.json)的某一項時,當修改appsetting.json檔案,該選項的值就能夠立馬得到更改。
所以來回過頭來看這三兄弟。它們的區別其實在於變更的時效性:
型別 | 說明 | 時效性 |
---|---|---|
IOptions | 一旦程式啟動,該選項的值就無法更改 | 無時效性可言 |
IOptionsSnapshot | 當開啟一個新Scoped時,就會重新計算選項的值 | 相對比較低,依賴於合適開啟一個新的Scoped |
IOptionsMonitor | 依賴於IChangeToken,只要令牌源變更則立刻做出反應 | 高 |
假如把IOptionsMonitor<MyOptions>
新增到上面IOptions<MyOptions>
和IOptionsSnapshot<MyOptions>
的檔案變更案例,如果在一次HTTP請求中,檔案變更了兩次,那麼IOptionsSnapshot<MyOptions>
不會在第二次更改中同步更改,而IOptionsMonitor<MyOptions>
則可以。
那麼什麼時候來使用什麼樣的介面呢?相信這個時候,您的心裡比我還要清楚。當您的選項只是負責一次性處理的時候,應用啟動了就不需要更改,那麼考慮使用IOptions<MyOptions>
,如果是對資料來源的變更要求很嚴格,比如開啟了一個“BackgroundJob”在後臺執行,該job需要一個選項型別,而該型別依賴於配置檔案,需要對配置檔案更改時即刻做出改變,那麼請考慮使用IOptionsMonitor<MyOptions>
。
最後回過頭來看微軟官方文件上關於“Options”的兩個點(ISP和關注點分離),您應該一下就能理解。
如果您有興趣的話可以跳轉至官方文件進行閱讀:《ASP.NET Core 中的選項模式》
好啦,今天的車就開完了,如果您喜歡該系列文章可以點選關注。?
最後,偷偷說一句:創作不易,點個推薦吧.....