1. 程式人生 > >[ASP.NET Core 3框架揭祕] 依賴注入[7]:服務消費

[ASP.NET Core 3框架揭祕] 依賴注入[7]:服務消費

包含服務註冊資訊的IServiceCollection集合最終被用來建立作為依賴注入容器的IServiceProvider物件。當需要消費某個服務例項的時候,我們只需要指定服務型別呼叫IServiceProvider的GetService方法即可,IServiceProvider物件就會根據對應的服務註冊提供所需的服務例項。

一、IServiceProvider

如下面的程式碼片段所示,IServiceProvider介面定義了唯一的GetService方法根據指定的型別來提供對應的服務例項。當利用包含服務註冊的IServiceCollection物件創建出IServiceProvider物件之後,我們只需要將服務註冊的服務型別(對應於ServiceDescriptor的ServiceType屬性)作為引數呼叫GetService方法,該方法就能根據服務註冊資訊為我們提供對應的服務例項。

public interface IServiceProvider
{
    object GetService(Type serviceType);
}

針對IServiceProvider物件的建立體現在IServiceCollection介面的三個BuildServiceProvider擴充套件方法過載上。如下的程式碼片段所示,這三個擴充套件方法提供的都是一個型別為ServiceProvider的物件,該物件根據提供的配置選項來建立。配置選項型別ServiceProviderOptions提供了兩個屬性,其中ValidateScopes屬性表示是否需要開啟針對服務範圍的驗證,而ValidateOnBuild屬性則表示是否需要預先檢驗作為服務註冊的每個ServiceDescriptor物件能否提供對應的服務例項。預設情況下這兩種型別的檢驗都是關閉的。

public class ServiceProviderOptions
{
    public bool ValidateScopes { get; set; }
    public bool ValidateOnBuild { get; set; }
    internal static readonly ServiceProviderOptions Default  = new ServiceProviderOptions();
}

public static class ServiceCollectionContainerBuilderExtensions
{
    public static ServiceProvider BuildServiceProvider( this IServiceCollection services)
        => BuildServiceProvider(services, ServiceProviderOptions.Default);
    public static ServiceProvider BuildServiceProvider( this IServiceCollection services, bool validateScopes)
        => services.BuildServiceProvider(new ServiceProviderOptions {  ValidateScopes = validateScopes });

    public static ServiceProvider BuildServiceProvider( this IServiceCollection services, ServiceProviderOptions options)
        => new ServiceProvider(services, options);
}

雖然呼叫IServiceCollection的BuildServiceProvider擴充套件方法返回總是一個ServiceProvider物件,但是我並不打算詳細介紹這個型別,這是因為ServiceProvider涉及到一系列內部型別和介面,並且實現在該型別中針對服務例項的提供機制一直在不斷的變化,而且這個變化趨勢在未來版本更替過程中可能還將繼續下去。

除了定義在IServiceProvider介面中的GetService方法,該介面還具有如下這些擴充套件方法來提供服務例項。GetService<T>方法以泛型引數的形式指定了服務型別,返回的服務例項也會作對應的型別轉換。如果指定服務型別的服務註冊不存在,GetService方法會返回Null,如果呼叫GetRequiredService或者GetRequiredService<T>方法則會丟擲一個InvalidOperationException型別的異常。如果所需的服務例項是必需的,我們一般會呼叫這兩個擴充套件方法。

public static class ServiceProviderServiceExtensions
{
    public static T GetService<T>(this IServiceProvider provider);

    public static T GetRequiredService<T>(this IServiceProvider provider);
    public static object GetRequiredService(this IServiceProvider provider,  Type serviceType);
    
    public static IEnumerable<T> GetServices<T>(this IServiceProvider provider);
    public static IEnumerable<object> GetServices(this IServiceProvider provider,  Type serviceType);
}

正如前面多次提到的,如果針對某個型別添加了多個服務註冊,那麼GetService方法總是會採用最新新增的服務註冊來提供服務例項。如果希望利用所有的服務註冊來建立一組服務例項列表,我們可以呼叫GetServices或者GetServices<T>方法,也可以呼叫GetService<IEnumerable<T>>方法。

二、服務例項的建立

對於通過呼叫IServiceCollection集合的BuildServiceProvider方法建立的IServiceProvider物件來說,當我們通過指定服務型別呼叫其GetService方法以獲取對應的服務例項的時候,它總是會根據提供的服務型別從服務註冊列表中找到對應的ServiceDescriptor物件,並根據它來提供所需的服務例項。

ServiceDescriptor具有三個不同的建構函式,分別對應著服務例項最初的三種提供方式,我們可以提供一個Func<IServiceProvider, object>物件作為工廠來建立對應的服務例項,也可以直接提供一個建立好的服務例項。如果我們提供的是服務的實現型別,那麼最終提供的服務例項將通過呼叫該型別的某個建構函式來建立,那麼建構函式是通過怎樣的策略被選擇出來的呢?

如果IServiceProvider物件試圖通過呼叫建構函式的方式來建立服務例項,傳入建構函式的所有引數必須先被初始化,所以最終被選擇出來的建構函式必須具備一個基本的條件,那就是IServiceProvider能夠提供建構函式的所有引數。為了讓讀者朋友能夠更加深刻地理解IServiceProvider在建構函式選擇過程中採用的策略,我們會採用例項演示的方式對此進行講述。

我們在一個控制檯應用中定義了四個服務介面(IFoo、IBar、IBaz和IGux)以及實現它們的四個類(Foo、Bar、Baz和Gux)。如下面的程式碼片段所示,我們為Gux定義了三個建構函式,引數均為我們定義了服務介面型別。為了確定IServiceProvider最終選擇哪個建構函式來建立目標服務例項,我們在建構函式執行時在控制檯上輸出相應的指示性文字。

public interface IFoo {}
public interface IBar {}
public interface IBaz {}
public interface IGux {}

public class Foo : IFoo {}
public class Bar : IBar {}
public class Baz : IBaz {}
public class Gux : IGux
{
    public Gux(IFoo foo)  => Console.WriteLine("Selected constructor: Gux(IFoo)");
    public Gux(IFoo foo, IBar bar)  => Console.WriteLine("Selected constructor: Gux(IFoo, IBar)");
    public Gux(IFoo foo, IBar bar, IBaz baz)  => Console.WriteLine("Selected constructor: Gux(IFoo, IBar, IBaz)");
}

在如下這段演示程式中我們建立了一個ServiceCollection物件並在其中新增針對IFoo、IBar以及IGux這三個服務介面的服務註冊,針對服務介面IBaz的註冊並未被新增。我們利用由它建立的IServiceProvider來提供針對服務介面IGux的例項,究竟能否得到一個Gux物件呢?如果可以,它又是通過執行哪個建構函式建立的呢?

class Program
{
    static void Main()
    {       
        new ServiceCollection()
            .AddTransient<IFoo, Foo>()
            .AddTransient<IBar, Bar>()
            .AddTransient<IGux, Gux>()
            .BuildServiceProvider()
            .GetServices<IGux>();
    }
}

對於定義在Gux中的三個建構函式來說,由於建立IServiceProvider提供的IServiceCollection集合包含針對介面IFoo和IBar的服務註冊,所以它能夠提供前面兩個建構函式的所有引數。由於第三個建構函式具有一個型別為IBaz的引數,這無法通過IServiceProvider物件來提供。根據我們前面介紹的第一個原則(IServiceProvider物件能夠提供建構函式的所有引數),Gux的前兩個建構函式會成為合法的候選建構函式,那麼IServiceProvider最終會選擇哪一個呢?

在所有合法的候選建構函式列表中,最終被選擇出來的建構函式具有這麼一個特徵:每一個候選建構函式的引數型別集合都是這個建構函式引數型別集合的子集。如果這樣的建構函式並不存在,一個InvalidOperationException型別的異常會被丟擲來。根據這個原則,Gux的第二個建構函式的引數型別包括IFoo和IBar,而第一個建構函式僅僅具有一個型別為IFoo的引數,最終被選擇出來的會是Gux的第二個建構函式,所以執行我們的例項程式將會在控制檯上產生如下圖所示的輸出結果。

接下來我們對例項程式略加改動。如下面的程式碼片段所示,我們只為Gux定義兩個建構函式,它們都具有兩個引數,引數型別分別為IFoo & IBar和IBar & IBaz。我們將針對IBaz / Baz的服務註冊新增到建立的ServiceCollection集合中。

class Program
{
    static void Main()
    {       
        new ServiceCollection()
            .AddTransient<IFoo, Foo>()
            .AddTransient<IBar, Bar>()
            .AddTransient<IBaz, Baz>()
            .AddTransient<IGux, Gux>()
            .BuildServiceProvider()
            .GetServices<IGux>();
    }
}

public class Gux : IGux
{
    public Gux(IFoo foo, IBar bar) {}
    public Gux(IBar bar, IBaz baz) {}
}

對於Gux的兩個建構函式,雖然它們的引數均能夠由IServiceProvider物件來提供,但是並沒有一個建構函式的引數型別集合能夠成為所有有效建構函式引數型別集合的超集,所以IServiceProvider無法選擇出一個最佳的建構函式。執行該程式後會丟擲如下圖所示的InvalidOperationException異常,並提示無法從兩個候選的建構函式中選擇出一個最優的來建立服務例項。(S409)

[ASP.NET Core 3框架揭祕] 依賴注入[1]:控制反轉
[ASP.NET Core 3框架揭祕] 依賴注入[2]:IoC模式
[ASP.NET Core 3框架揭祕] 依賴注入[3]:依賴注入模式
[ASP.NET Core 3框架揭祕] 依賴注入[4]:一個迷你版DI框架
[ASP.NET Core 3框架揭祕] 依賴注入[5]:利用容器提供服務
[ASP.NET Core 3框架揭祕] 依賴注入[6]:服務註冊
[ASP.NET Core 3框架揭祕] 依賴注入[7]:服務消費
[ASP.NET Core 3框架揭祕] 依賴注入[8]:服務例項的生命週期
[ASP.NET Core 3框架揭祕] 依賴注入[9]:實現概述
[ASP.NET Core 3框架揭祕] 依賴注入[10]:與第三方依賴注入框架的適配