天天看點

【5min+】更好的選項實踐。.Net Core中的IOptions

【5min+】更好的選項實踐。.Net Core中的IOptions

系列介紹

【五分鐘的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()就可以完成注入選項:

services.AddOptions();

public MyBusinessClass(IOptions<SomeOptions> options)
{
    SomeOptions value = options.Value;
}           

看起來這和上面的代碼好像差別也不是很大吧。都是把SomeOptions添加到容器中,那麼第二種方法和第一種方法比起來有什麼優點呢?微軟專門推出該方式難道隻是為了“年底沖業績”?

非也非也?第二種方式其實用了更好的解耦思想來設計。假如咱們的SomeOptions需要在其它子產品中修改怎麼辦? 如果用第一種直接注入到容易的方案的話,這就十分的困難。而使用AddOptions的方式您就可以輕而易舉。

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

public void Configure(MvcOptions options)
{
    options.Filters.Add(new MyFileter());
}           

services.AddSingleton, MvcOptionsConfigure>();

這樣就完成了關注點的分離,我們不需要一直死守着Startup.cs檔案不放,也不需要讓使用者手動去配置。隻要我們知道IServiceCollection就可以往裡面添加我們自己的業務點。當然,Microsoft.Extensions.Options包還提供了另外的方式讓您可以完成IConfigureOptions的同樣操作,不過這些操作都是像文法糖一樣,實質上是相同的:

//和上面同樣的功能

services.Configure(Options =>

options.Filters.Add(new MyFileter());           

});

IOptions、IOptionsMonitor和IOptionsSnapshot

在上面其實我們已經見過了IOptions的尊容,我們可以通過注入IOptions來擷取MyOptions執行個體。

但是!但是!但是!!!! IOptions還有兩個兄弟IOptionsMonitor和IOptionsSnapshot。光名字上長的就很像了,它們都還有類似于“Value”的屬性來擷取選項執行個體。

媽呀,那麼它們到底有什麼不同呢?什麼時候該用老大,什麼使用該用老二呢? 接下來,年度最佳找不同大戲即将開始………………

先來看看IOptions和IOptionsSnapshot吧,看看它們的接口定義:

///

/// Used to access the value of for the lifetime of a request.

/// Options type.

public interface IOptionsSnapshot : IOptions 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和IOptionsSnapshot,此時得到的MyOptions的該屬性的值都是"A"。但是假如我們更改了文本的值,改為“B”。如果在發起一個http請求去擷取MyOptions的結果,此時IOptions依舊是“A”,而IOptionsSnapshot則更改為了B。

原因很簡單,因為IOptions是單例的,是以從程式一開始加載過一次之後,以後通路它都是這個結果,而IOptionsSnapshot是Scoped級别的,是以每一個新的Scoped時都會又去通路文本檔案擷取值,而一次Http請求就會開啟一次新的Scoped,是以此時結果就成為“B”。這個時候我們大概就能讀懂上面IOptionsSnapshot<>接口的解釋了:“用于在請求的生存期内通路選項的值”。

三兄弟一下就幹掉了倆,接下來看看最後一個好兄弟(毒瘤):IOptionsMonitor。還是直接看它的源代碼呢:

services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitor<>), typeof(OptionsMonitor<>)));

納尼?單例? 那是不是意味着它也一樣,一旦啟動了之後還是保持原有的結果呢?先不急,看看它的接口定義再說:

/// Used for notifications when instances change.

/// The options type.

public interface IOptionsMonitor

/// <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的參數。

握草(繼續手動播放名偵探bgm),如果您有幸看過我的上一篇文章:《【5min+】 一個令牌走天下!.Net Core中的ChangeToken》,那麼您可能一下就知道它扮演了什麼樣的角色。(5min+系列居然是連續的.... 😭)

再看看該接口的說明:"Used for notifications when TOptions instances change."(用于在選項執行個體更改時進行通知)。果然和我們猜的一模一樣,那麼它的實作類裡面一定有咱們上一篇文章中提到的:ChangeToken和IChangeToken等東西。

來吧,扒開它的具體實作,驗證咱們的猜想:

public OptionsMonitor(IOptionsFactory factory, IEnumerable> sources, IOptionsMonitorCache 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添加到上面IOptions和IOptionsSnapshot的檔案變更案例,如果在一次HTTP請求中,檔案變更了兩次,那麼IOptionsSnapshot不會在第二次更改中同步更改,而IOptionsMonitor則可以。

那麼什麼時候來使用什麼樣的接口呢?相信這個時候,您的心裡比我還要清楚。當您的選項隻是負責一次性處理的時候,應用啟動了就不需要更改,那麼考慮使用IOptions,如果是對資料源的變更要求很嚴格,比如開啟了一個“BackgroundJob”在背景運作,該job需要一個選項類型,而該類型依賴于配置檔案,需要對配置檔案更改時即刻做出改變,那麼請考慮使用IOptionsMonitor。

最後回過頭來看微軟官方文檔上關于“Options”的兩個點(ISP和關注點分離),您應該一下就能了解。

原文位址

https://www.cnblogs.com/uoyo/p/12583149.html