包含服務注冊信息的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]:與第三方依賴注入框架的適配