1. 程式人生 > >[ASP.NET Core 3框架揭祕] Options[5]: 依賴注入

[ASP.NET Core 3框架揭祕] Options[5]: 依賴注入

《Options模型》介紹了組成Options模型的4個核心物件以及它們之間的互動關係,讀者對如何得到Options物件的實現原理可能不太瞭解,本篇文章主要介紹依賴注入的相關內容。既然我們能夠利用IServiceProvider物件提供的IOptions<TOptions>服務、IOptionsSnapshot<TOptions>服務和IOptionsMonitorCache<TOptions>服務來獲取對應的Options物件,那麼在這之前必然需要註冊相應的服務。回顧《配置選項的正確使用方式》演示的幾個例項可以發現,Options模式涉及的API其實不是很多,大都集中在相關服務的註冊上。Options模型的核心服務實現在IServiceCollection介面的AddOptions擴充套件方法。

一、AddOptions

AddOptions擴充套件方法的完整定義如下所示,由此可知,該方法將Options模型中的幾個核心型別作為服務註冊到了指定的IServiceCollection物件之中。由於它們都是呼叫TryAdd方法進行服務註冊的,所以我們可以在需要Options模式支援的情況下呼叫AddOptions方法,而不需要擔心是否會新增太多重複服務註冊的問題。

public static class OptionsServiceCollectionExtensions
{
    public static IServiceCollection AddOptions(this IServiceCollection services)
    {
        services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptions<>), typeof(OptionsManager<>)));
        services.TryAdd(ServiceDescriptor.Scoped(typeof(IOptionsSnapshot<>), typeof(OptionsManager<>)));
        services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitor<>), typeof(OptionsMonitor<>)));
        services.TryAdd(ServiceDescriptor.Transient(typeof(IOptionsFactory<>), typeof(OptionsFactory<>)));
        services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitorCache<>), typeof(OptionsCache<>)));
        return services;
    }
}

從給出的程式碼片段可以看出,AddOptions擴充套件方法實際上註冊了5個服務。由於這5個服務註冊非常重要,所以筆者採用表格的形式列出了它們的Service Type(服務介面)、Implementation(實現型別)和Lifetime(生命週期)(見下表)。雖然服務介面IOptions<TOptions>和IOptionsSnapshot<TOptions>對映的實現型別都是OptionsManager<TOptions>,但是它們具有不同的生命週期。具體來說,前者的生命週期為Singleton,後者的生命週期則是Scoped,後續內容會單獨講述不同生命週期對Options物件產生什麼樣的影響。

Service Type

Implementation

Lifetime

IOptions<TOptions>

OptionsManager<TOptions>

Singleton

IOptionsSnapshot<TOptions>

OptionsManager<TOptions>

Scoped

IOptionsMonitor<TOptions>

OptionsMonitor<TOptions>

Singleton

IOptionsFactory<TOptions>

OptionsFactory<TOptions>

Transient

IOptionsMonitorCache<TOptions>

OptionsCache<TOptions>

Singleton

按照上表列舉的服務註冊,如果以IOptions<TOptions>和IOptionsSnapshot<TOptions>作為服務型別從IServieProvidere物件中提取對應的服務例項,得到的都是OptionsManager<TOptions>物件。當OptionsManager<TOptions>物件被建立時,OptionsFactory<TOptions>物件會被自動創建出來並以構造器注入的方式提供給它並且被用來建立Options物件。但是由於表7-1中並沒有針對服務IConfigureOptions<TOptions>和IPostConfigureOptions<TOptions>的註冊,所以建立的Options物件無法被初始化。

二、Configure<TOptions>與PostConfigure<TOptions>

針對IConfigureOptions<TOptions>和IPostConfigureOptions<TOptions>的服務註冊是通過如下這些擴充套件方法來完成的。具體來說,針對IConfigureOptions<TOptions>服務的註冊實現在Configure<TOptions>方法中,而PostConfigure<TOptions>擴充套件方法則幫助我們完成針對IPostConfigureOptions<TOptions>的註冊。

public static class OptionsServiceCollectionExtensions
{
    public static IServiceCollection Configure<TOptions>(this IServiceCollection services, Action<TOptions> configureOptions)
        where TOptions : class
        => services.Configure(Options.Options.DefaultName, configureOptions);

    public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string name, Action<TOptions> configureOptions) where TOptions : class
        => services.AddSingleton<IConfigureOptions<TOptions>>(
        new ConfigureNamedOptions<TOptions>(name, configureOptions));
        return services;

    public static IServiceCollection PostConfigure<TOptions>(this IServiceCollection services, Action<TOptions> configureOptions)
        where TOptions : class
        => services.PostConfigure(Options.Options.DefaultName, configureOptions);

    public static IServiceCollection PostConfigure<TOptions>(this IServiceCollection services, string name,Action<TOptions> configureOptions) where TOptions : class
        => services.AddSingleton<IPostConfigureOptions<TOptions>>(new PostConfigureOptions<TOptions>(name, configureOptions));
}

從上述程式碼可以看出,這些方法註冊的服務實現型別為ConfigureNamedOptions<TOptions>和PostConfigureOptions<TOptions>,採用的生命週期模式均為Singleton。不論是ConfigureNamedOptions<TOptions>還是PostConfigureOptions<TOptions>,都需要指定一個具體的名稱,對於沒有指定具體Options名稱的Configure<TOptions>和PostConfigure<TOptions>方法過載來說,最終指定的是代表預設名稱的空字串。

三、ConfigureAll<TOptions>與PostConfigureAll<TOptions>

雖然ConfigureAll<TOptions>和PostConfigureAll<TOptions>擴充套件方法註冊的同樣是ConfigureNamedOptions<TOptions>和PostConfigureOptions<TOptions>型別,但是它們會將名稱設定為Null。通過《Options模型》的內容可知,OptionsFactory物件在進行Options物件的初始化過程中會將名稱為Null的IConfigureNamedOptions<TOptions>和IPostConfigureOptions<TOptions>物件作為公共的配置物件,並且無條件執行。

public static class OptionsServiceCollectionExtensions
{
    public static IServiceCollection ConfigureAll<TOptions>(this IServiceCollection services, Action<TOptions> configureOptions)
        where TOptions : class
        => services.Configure(name: null, configureOptions: configureOptions);

    public static IServiceCollection PostConfigureAll<TOptions>(this IServiceCollection services, Action<TOptions> configureOptions)
        where TOptions : class
        => services.PostConfigure(name: null, configureOptions: configureOptions);
}

四、ConfigureOptions

對於上面這幾個將Options型別作為泛型引數的方法來說,它們總是利用指定的Action<Options>物件來建立註冊的ConfigureNamedOptions<TOptions>物件和PostConfigureOptions<TOptions>物件 。對於自定義的實現了IConfigureOptions<TOptions>介面或者IPostConfigureOptions<TOptions>介面的型別,我們可以呼叫如下所示的3個ConfigureOptions擴充套件方法來對它們進行註冊。筆者在如下所示的程式碼片段中通過簡化的程式碼描述了這3個擴充套件方法的實現邏輯。

public static class OptionsServiceCollectionExtensions
{
    public static IServiceCollection ConfigureOptions(this IServiceCollection services, object configureInstance)
    {
        Array.ForEach(FindIConfigureOptions(configureInstance.GetType()), it => services.AddSingleton(it, configureInstance));
        return services;
    }

    public static IServiceCollection ConfigureOptions(this IServiceCollection services, Type configureType)
    {
        Array.ForEach(FindIConfigureOptions(configureType), it => services.AddTransient(it, configureType));
        return services;
    }

    public static IServiceCollection ConfigureOptions<TConfigureOptions>(this IServiceCollection services) where TConfigureOptions : class
        => services.ConfigureOptions(typeof(TConfigureOptions));

    private static Type[] FindIConfigureOptions(Type type)
    {
        Func<Type, bool> valid = it => it.IsGenericType && (it.GetGenericTypeDefinition() == typeof(IConfigureOptions<>) || it.GetGenericTypeDefinition() == typeof(IPostConfigureOptions<>));
        var types = type.GetInterfaces().Where(valid).ToArray();
        if (types.Any())
        {
            throw new InvalidOperationException();
        }
        return types;
    }
}

五、OptionsBuilder<TOptions>

Options模式涉及針對非常多的服務註冊,並且這些服務都是針對具體某個Options型別的,為了避免定義過多針對IServiceCollection介面的擴充套件方法,最新版本的Options模型採用Builder模式來完成相關的服務註冊。具體來說,可以將用來儲存服務註冊的IServiceCollection集合封裝到下面的OptionsBuilder<TOptions>物件中,並利用它提供的方法間接地完成所需的服務註冊。

public class OptionsBuilder<TOptions> where TOptions : class
{
    public string Name { get; }
    public IServiceCollection Services { get; }
    public OptionsBuilder(IServiceCollection services, string name);

    public virtual OptionsBuilder<TOptions> Configure(Action<TOptions> configureOptions);
    public virtual OptionsBuilder<TOptions> Configure<TDep>(Action<TOptions, TDep> configureOptions) where TDep : class;
    public virtual OptionsBuilder<TOptions> Configure<TDep1, TDep2>(Action<TOptions, TDep1, TDep2> configureOptions) where TDep1 : class where TDep2 : class;
    public virtual OptionsBuilder<TOptions> Configure<TDep1, TDep2, TDep3>(Action<TOptions, TDep1, TDep2, TDep3> configureOptions) where TDep1 : class where TDep2 : class where TDep3 : class;
    public virtual OptionsBuilder<TOptions> Configure<TDep1, TDep2, TDep3, TDep4>(Action<TOptions, TDep1, TDep2, TDep3, TDep4> configureOptions) where TDep1 : class where TDep2 : class where TDep3 : class where TDep4 : class;
    public virtual OptionsBuilder<TOptions> Configure<TDep1, TDep2, TDep3, TDep4, TDep5>(Action<TOptions, TDep1, TDep2, TDep3, TDep4, TDep5> configureOptions) where TDep1 : class where TDep2 : class where TDep3 : class where TDep4 : class where TDep5 : class;

    public virtual OptionsBuilder<TOptions> PostConfigure(Action<TOptions> configureOptions);
    public virtual OptionsBuilder<TOptions> PostConfigure<TDep>(Action<TOptions, TDep> configureOptions) where TDep : class;
    public virtual OptionsBuilder<TOptions> PostConfigure<TDep1, TDep2>(Action<TOptions, TDep1, TDep2> configureOptions) where TDep1 : class where TDep2 : class;
    public virtual OptionsBuilder<TOptions> PostConfigure<TDep1, TDep2, TDep3>(Action<TOptions, TDep1, TDep2, TDep3> configureOptions) where TDep1 : class where TDep2 : class where TDep3 : class;
    public virtual OptionsBuilder<TOptions> PostConfigure<TDep1, TDep2, TDep3, TDep4>(Action<TOptions, TDep1, TDep2, TDep3, TDep4> configureOptions) where TDep1 : class where TDep2 : class where TDep3 : class where TDep4 : class;
    public virtual OptionsBuilder<TOptions> PostConfigure<TDep1, TDep2, TDep3, TDep4, TDep5>(Action<TOptions, TDep1, TDep2, TDep3, TDep4, TDep5> configureOptions) where TDep1 : class where TDep2 : class where TDep3 : class where TDep4 : class where TDep5 : class;

    public virtual OptionsBuilder<TOptions> Validate(Func<TOptions, bool> validation);
    public virtual OptionsBuilder<TOptions> Validate<TDep>(Func<TOptions, TDep, bool> validation);
    public virtual OptionsBuilder<TOptions> Validate<TDep1, TDep2>(Func<TOptions, TDep1, TDep2, bool> validation);
    public virtual OptionsBuilder<TOptions> Validate<TDep1, TDep2, TDep3>(Func<TOptions, TDep1, TDep2, TDep3, bool> validation);
    public virtual OptionsBuilder<TOptions> Validate<TDep1, TDep2, TDep3, TDep4>(Func<TOptions, TDep1, TDep2, TDep3, TDep4, bool> validation);
    public virtual OptionsBuilder<TOptions> Validate<TDep1, TDep2, TDep3, TDep4, TDep5>(Func<TOptions, TDep1, TDep2, TDep3, TDep4, TDep5, bool> validation);

    public virtual OptionsBuilder<TOptions> Validate<TDep>(Func<TOptions, TDep, bool> validation, string failureMessage);
    public virtual OptionsBuilder<TOptions> Validate<TDep1, TDep2>(Func<TOptions, TDep1, TDep2, bool> validation, string failureMessage);
    public virtual OptionsBuilder<TOptions> Validate<TDep1, TDep2, TDep3>(Func<TOptions, TDep1, TDep2, TDep3, bool> validation, string failureMessage);
    public virtual OptionsBuilder<TOptions> Validate<TDep1, TDep2, TDep3, TDep4>(Func<TOptions, TDep1, TDep2, TDep3, TDep4, bool> validation, string failureMessage);
    public virtual OptionsBuilder<TOptions> Validate<TDep1, TDep2, TDep3, TDep4, TDep5>(Func<TOptions, TDep1, TDep2, TDep3, TDep4, TDep5, bool> validation, string failureMessage);
}

如下面的程式碼片段所示,OptionsBuilder<TOptions>物件不僅通過泛型引數關聯對應的Options型別,還利用Name屬性提供了Options的名稱。從上面的程式碼片段可以看出,OptionsBuilder<TOptions>型別提供的3組方法分別提供了針對IConfigureOptions<TOptions>介面、IPostConfigureOptions<TOptions>介面和IValidateOptions<TOptions>介面的18個實現型別的註冊。

當利用Builder模式來註冊這些服務的時候,只需要呼叫IServiceCollection介面的如下這兩個AddOptions<TOptions>擴充套件方法根據指定的名稱(預設名稱為空字串)創建出對應的OptionsBuilder<TOptions>物件即可。從如下所示的程式碼片段可以看出,這兩個方法最終都需要呼叫非泛型的AddOptions方法,由於該方法呼叫TryAdd擴充套件方法註冊Options模式的5個核心服務,所以不會導致服務的重複註冊。

public static class OptionsServiceCollectionExtensions
{
    public static OptionsBuilder<TOptions> AddOptions<TOptions>( this IServiceCollection services) where TOptions : class => services.AddOptions<TOptions>(Options.DefaultName);

    public static OptionsBuilder<TOptions> AddOptions<TOptions>(this IServiceCollection services, string name) where TOptions : class
    {
        services.AddOptions();
        return new OptionsBuilder<TOptions>(services, name);
    }
}

六、IOptions<TOptions>與IOptionsSnapshot<TOptions>

通過對註冊服務的分析可知,服務介面IOptions<TOptions>和IOptionsSnapshot<TOptions>的預設實現型別都是OptionsManager<TOptions>,兩者的不同之處體現在生命週期上,前者採用的生命週期模式為Singleton,後者採用的生命週期模式則是Scoped。對於一個ASP.NET Core應用來說,Singleton和Scoped對應的是針對當前應用和當前請求的生命週期,所以通過IOptions<TOptions>介面獲取的Options物件在整個應用的生命週期內都是一致的,而通過IOptionsSnapshot<TOptions>介面獲取的Options物件則只能在當前請求上下文中保持一致。這也是後者命名的由來,它表示針對當前請求的Options快照 。

下面通過一個例項來演示IOptions<TOptions>和IOptionsSnapshot<TOptions>之間的差異。下面定義了FoobarOptions型別,簡單起見,我們僅僅為它定義了兩個整型的屬性(Foo和Bar),並重寫了ToString方法。

public class FoobarOptions
{
    public int Foo { get; set; }
    public int Bar { get; set; } 
    public override string ToString() => $"Foo:{Foo}, Bar:{Bar}";
}

整個演示程式體現在如下所示的程式碼片段中。我們建立了一個ServiceCollection物件,在呼叫AddOptions擴充套件方法註冊Options模型的基礎服務之後,呼叫Configure<FoobarOptions>方法利用定義的本地函式Print將FoobarOptions物件的Foo屬性和Bar屬性設定為一個隨機數。

class Program
{
    static void Main()
    {
        var random = new Random();
        var serviceProvider = new ServiceCollection()
            .AddOptions()
            .Configure<FoobarOptions>(foobar =>
            {
                foobar.Foo = random.Next(1, 100);
                foobar.Bar = random.Next(1, 100);
            })
            .BuildServiceProvider();        

        Print(serviceProvider);
        Print(serviceProvider);

        static void Print(IServiceProvider provider)
        {
            var scopedProvider = provider
                .GetRequiredService<IServiceScopeFactory>()
                .CreateScope()
                .ServiceProvider;

            var options = scopedProvider
                .GetRequiredService<IOptions<FoobarOptions>>()
                .Value;
            var optionsSnapshot1 = scopedProvider
                .GetRequiredService<IOptionsSnapshot<FoobarOptions>>()
                .Value;
            var optionsSnapshot2 = scopedProvider
                .GetRequiredService<IOptionsSnapshot<FoobarOptions>>()
                .Value;
            Console.WriteLine($"options:{options}");
            Console.WriteLine($"optionsSnapshot1:{optionsSnapshot1}");
            Console.WriteLine($"optionsSnapshot2:{optionsSnapshot2}\n");
        }
    }
}

我們並沒有直接利用ServiceCollection物件建立的IServiceProvider物件來提供服務,而是利用它建立了一個代表子容器的IServiceProvider物件,該物件就相當於ASP.NET Core應用中針對當前請求建立的IServiceProvider物件(RequestServices)。在利用這個IServiceProvider物件分別針對IOptions<TOptions>介面和IOptionsSnapshot<TOptions>介面得到對應的FoobarOptions物件之後,我們將配置選項輸出到控制檯上。上述操作先後執行了兩次,相當於ASP.NET Core應用分別處理了兩次請求。

下圖展示了該演示程式執行後的輸出結果,由此可知,只有從同一個IServiceProvider物件獲取的IOptionsSnapshot<TOptions>服務才能提供一致的Options物件,但是對於所有源自同一個根的所有IServiceProvider物件來說,從中提取的IOptions<TOptions>服務都能提供一致的Options物件。

OptionsManager<Options>會利用一個自行建立的OptionsCache<TOptions>物件來快取Options物件,也就說,OptionsManager<Options>提供的Options物件存放在其私有快取中。雖然OptionsCache<TOptions>提供了清除快取的能力,但是OptionsManager<Options>自身無法感知原始Options資料是否發生變化,所以不會清除快取的Options物件。

這個特性決定了在一個ASP.NET Core應用中,以IOptions<TOptions>服務的形式提供的Options在整個應用的生命週期內不會發生改變,但是若使用IOptionsSnapshot<TOptions>服務,提供的Options物件只能在同一個請求上下文中提供一致的保障。如果希望即使在同一個請求處理週期內也能及時應用最新的Options屬性,就只能使用IOptionsMonitor<TOptions>服務來提供Options物件。

[ASP.NET Core 3框架揭祕] Options[1]: 配置選項的正確使用方式[上篇]
[ASP.NET Core 3框架揭祕] Options[2]: 配置選項的正確使用方式[下篇]
[ASP.NET Core 3框架揭祕] Options[3]: Options模型[上篇]
[ASP.NET Core 3框架揭祕] Options[4]: Options模型[下篇]
[ASP.NET Core 3框架揭祕] Options[5]: 依賴注入
[ASP.NET Core 3框架揭祕] Options[6]: 擴充套件與定製
[ASP.NET Core 3框架揭祕] Options[7]: 與配置系統的整合