依賴注入[8]: .NET Core DI框架[服務消費]


包含服務注冊信息的IServiceCollection對象最終被用來創建作為DI容器的IServiceProvider對象。當需要消費某個服務實例的時候,我們只需要指定服務類型調用IServiceProvider的GetService方法,IServiceProvider就會根據對應的服務注冊提供所需的服務實例。

目錄
一、IServiceProvider
二、構造函數的選擇
三、服務范圍
四、三種生命周期模式
五、ASP.NET Core應用下的生命周期
六、服務范圍檢驗

一、IServiceProvider

如下面的代碼片段所示,IServiceProvider接口定義了唯一的方法GetService方法根據指定的服務類型來提供對應的服務實例。當我們在利用包含服務注冊的IServiceCollection對象創建對作為DI容器的IServiceProvider對象之后,我們只需要將服務注冊的服務類型(對應於ServiceDescriptor的ServiceType屬性)作為參數調用GetService方法,后者就能根據服務注冊信息為我們提供對應的服務實例。

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

public static class ServiceCollectionContainerBuilderExtensions
{
    public static ServiceProvider BuildServiceProvider(this IServiceCollection services);
}

默認情況下調用IServiceCollection的BuildServiceProvider方法返回的一個ServiceProvider對象,但是我並不打算詳細介紹這個類型,這是因為實現在該類型中針對服務實例的提供機制一直在不斷的變化,而且這個變化趨勢在未來版本更替過程中還將繼續。除此之外,ServiceProvider涉及到一系列內部類型和接口,所以我們不打算涉及具體的細節,只講總體設計。

除了定義在IServiceProvider的這個GetService方法,DI框架為了該接口定了如下這些擴展方法。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>方法。

二、構造函數的選擇

對於通過調用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(string[] args)
    {       
        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的第二個構造函數,所有運行我們的實例程序將會在控制台上產生如圖1所示的輸出結果。

4-5
圖1構造函數的選擇策略

接下來我們對實例程序略加改動。如下面的代碼片段所示,我們只為Gux定義兩個構造函數,它們都具有兩個參數,參數類型分別為IFoo&IBar和IBar&IBaz。我們將針對IBaz/Baz的服務注冊添加到創建的ServiceCollection對象上。

class Program
{
    static void Main(string[] args)
    {       
        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來提供,但是並沒有一個構造函數的參數類型集合能夠成為所有有效構造函數參數類型集合的超集,所以ServiceProvider無法選擇出一個最佳的構造函數。運行該程序后會拋出如圖2所示的InvalidOperationException異常,並提示無法從兩個候選的構造函數中選擇出一個最優的來創建服務實例。

4-6
圖2 構造函數的選擇策略

接下來我們着重介紹服務生命周期的話題。生命周期決定了IServiceProvider采用怎樣的方式提供和釋放服務實例。雖然不同版本的DI框架在針對服務實例生命周期管理采用了不同的實現,但總的來說,實現原理還是類似的。在我們提供的DI框架Cat中,我們已經模擬了三種生命周期模式的實現原理,接下來我們結合服務范圍的概念來對這個話題做進一步講解。

三、服務范圍

對於DI框架體用的三種生命周期(Singleton、Scoped和Transient)來說,Singleton和Transient都具有明確的語義,但是Scoped代表一種怎樣的生命周期模式,很多初學者往往搞不清楚。這里所謂的Scope指的是由IServiceScope接口表示的“服務范圍”,該范圍由IServiceScopeFactory接口表示的“服務范圍工廠”來創建。如下面的代碼片段所示,IServiceProvider的擴展方法CreateScope正是利用提供的IServiceScopeFactory服務實例來創建作為服務范圍的IServiceScope對象。

public interface IServiceScope : IDisposable
{
    IServiceProvider ServiceProvider { get; }
}

public interface IServiceScopeFactory
{
    IServiceScope CreateScope();
}

public static class ServiceProviderServiceExtensions
{
   public static IServiceScope CreateScope(this IServiceProvider provider) => provider.GetRequiredService<IServiceScopeFactory>().CreateScope();
}

任何一個IServiceProvider對象都可以利用其注冊的IServiceScopeFactory服務創建一個代表服務范圍的IServiceScope對象,后者代表的“范圍”內具有一個新創建的IServiceProvider對象(對應着接口IServiceScope的ServiceProvider屬性),后者同樣具有提供服務實例的能力,它與當前IServiceProvider具在邏輯上具有如圖3所示的“父子關系”。

4-7

圖3 IServiceScope與IServiceProvider(邏輯結構)

如圖3所示的樹形層次結構只是一種邏輯結構,從對象引用層面來開,通過某個IServiceScope包裹的IServiceProvider對象不需要知道自己的“父親”是誰,它只關心作為根節點的IServiceProvider在哪里就可以了。圖4從物理層面揭示了IServiceScope/IServiceProvider對象之間的關系,任何一個IServiceProvider對象都具有針對根容器的引用。

4-8

圖4 IServiceScope與IServiceProvider(物理結構)

四、三種生命周期模式

只有在充分了解IServiceScope的創建過程以及它與IServiceProvider之間的關系之后,我們才會對三種生命周期管理模式(Singleton、Scope和Transient)具有深刻的認識。就服務實例的提供方式來說,它們之間具有如下的差異:

  • Singleton:IServiceProvider創建的服務實例保存在作為根容器的IServiceProvider上,所有多個同根的IServiceProvider對象提供的針對同一類型的服務實例都是同一個對象。

  • Scoped:IServiceProvider創建的服務實例由自己保存,所以同一個IServiceProvider對象提供的針對同一類型的服務實例均是同一個對象。

  • Transient:針對每一次服務提供請求,IServiceProvider總是創建一個新的服務實例。

IServiceProvider除了為我們提供所需的服務實例之外,對於由它提供的服務實例,它還肩負起回收釋放之責。這里所說的回收釋放與.NET Core自身的垃圾回收機制無關,僅僅針對於自身類型實現了IDisposable接口的服務實例(下面簡稱為Disposable服務實例),針對服務實例的釋放體現為調用它們的Dispose方法。IServiceProvider針對服務實例采用的回收釋放策略取決於對應服務注冊的生命周期模式,具體服務回收策略主要體現為如下兩點:

  • Singleton:提供Disposable服務實例保存在作為根容器的IServiceProvider對象上,只有后者被釋放的時候這些Disposable服務實例才能被釋放。

  • Scoped和Transient:IServiceProvider對象會保存由它提供的Disposable服務實例,當自己被釋放的時候,這些Disposable會被釋放。

綜上所述,每個作為DI容器的IServiceProvider對象都具有如圖5所示兩個列表來存放服務實例,我們將它們分別命名為“Realized Services”和“Disposable Services”,對於一個作為非根容器的IServiceProvider對象來說,由它提供的Scoped服務保存在自身的Realized Services列表中,Singleton服務實例則會保存在根容器的Realized Services列表。如果服務實現類型實現了IDisposable接口,Scoped和Singleton服務實例會被保存到自身的Disposable Services列表中,而Singleton服務實例則會保存到根容器的Disposable Services列表。

4-9

圖5 生命周期管理

對於作為容器的IServiceProvider對象來說,Singleton和Scope模式對它來說是兩種等效的生命周期模式,由它提供的Singleton和Scoped服務實例會被被存放到自身的Realized Services列表,而所有需要被釋放的服務實例則被存放到Disposable Services列表。

當某個IServiceProvider被用於提供針對指定類型的服務實例時,它會根據服務類型提取出表示服務注冊的ServiceDescriptor對象並根據后者得到對應的生命周期模式。如果生命周期模式為Singleton,並且作為根容器的Realized Services列表中包含對應的服務實例,后者將作為最終提供的服務實例。如果這樣的服務實例尚未創建,那么新的服務將會被創建出來並作為提供的服務實例。在返回之后該對象會被添加到根容器的Realized Services列表中,如果實例類型實現了IDisposable接口,創建的服務實例會被添加到根容器的Disposable Services列表中。

如果生命周期為Scoped,那么IServiceProvider會先確定自身的Realized Services列表中是否存在對應的服務實例,存在的服務實例將作為最終返回的服務實例。如果Realized Services列表不存在對應的服務實例,那么新的服務實例會被創建出來。在作為最終的服務實例被返回之前,創建的服務實例會被添加的自身的Realized Services列表中,如果實例類型實現了IDisposable接口,創建的服務實例會被添加到自身的Disposable Services列表中。

如果提供服務的生命周期為Transient,那么IServiceProvider會直接創建一個新的服務實例。在作為最終的服務實例被返回之前,創建的服務實例會被添加的自身的Realized Services列表中,如果實例類型實現了IDisposable接口,創建的服務實例會被添加到自身的Disposable Services列表中。

對於非根容器的IServiceProvider對象來說,它的生命周期是由“包裹”着它的IServiceScope對象控制的。從上面給出的定義可以看出IServiceScope實現了IDisposable接口,Dispose方法的執行不僅標志着當前服務范圍的終結,也意味着對應IServiceProvider對象生命周期的結束。

當代表服務范圍的IServiceScope對象的Dispose方法被調用的時候,它會調用對應IServiceProvider的Dispose方法。一旦IServiceProvider因自身Dispose方法的調用而被釋放的時候,它會從自身的Disposable Services列表中提取出所有需要被釋放的服務實例,並調用它們的Dispose方法。在這之后,Disposable Services和Realized Services列表會被清空,列表中的服務實例和IServiceProvider對象自身會成為垃圾對象被GC回收。

五、ASP.NET Core應用下的生命周期

DI框架所謂的服務范圍在ASP.NET Core應用中具有明確的邊界,指的是針對每個HTTP請求的上下文,也就是服務范圍的生命周期與每個請求上下文綁定在一起。如圖6所示,ASP.NET Core應用中用於提供服務實例的IServiceProvider對象分為兩種類型,一種是作為根容器並與應用具有相同生命周期的IServiceProvider,另一個類則是根據請求及時創建和釋放的IServiceProvider,我們可以將它們分別稱為Application ServiceProviderRequest ServiceProvider

4-10

圖6 生命周期管理

在ASP.NET Core應用初始化過程中,即請求管道構建過程中使用的服務實例都是由Application ServiceProvider提供的。在具體處理每個請求時,ASP.NET Core框架會利用注冊的一個中間件來針對當前請求創建一個服務范圍,該服務范圍提供的Request ServiceProvider用來提供當前請求處理過程中所需的服務實例。一旦服務請求處理完成,上述的這個中間件會主動釋放掉由它創建的服務范圍。

六、服務范圍檢驗

如果我們在一個ASP.NET Core應用中將一個服務的生命周期注冊為Scoped,實際上是希望服務實例采用基於請求的生命周期。舉個簡單的例子,如果我們在一個ASP.NET Core應用中采用Entity Framework Core來訪問數據庫,我們一般會將對應的DbContext類型(姑且命名為FoobarDbContext)注冊為一個Scoped服務,這樣既可以保證在FoobarDbContext能夠自同一個請求上下文中被重用,也可以確保FoobarDbContext在請求結束之后能夠及時將數據庫鏈接釋放掉。

但是如果我們使用作為根容器的Application ServiceProvider來提供這個DbContext對象,意味着提供的DbContext將被保存在Application ServiceProvider的Realized Services列表中,知道應用關閉時才能被釋放。即使提供該FoobarDbContext是針對請求的Request ServiceProvider,如果另一個Singleton服務(姑且命名為Foobar)具有針對它的依賴,意味着提供服務實例Foobar將會具有針對FoobarDbContext對象的引用。由於Foobar是一個Singleton服務實例,所以被它引用的FoobarDbContext也只能在應用關閉的時候才能被釋放。

為了解決這個問題,我們可以讓IServiceProvider在提供Scoped服務實例的時候進行針對性的檢驗。針對服務范圍驗證的開關由ServiceProviderOptions的ValidateScopes屬性來控制,默認情況下是關閉的。如果希望開啟針對服務范圍的驗證,我們可以在調用IServiceCollect接口的BuildServiceProvider方法的時候指定一個ServiceProviderOptions對象作為參數,或者直接調用另一個擴展方法並將傳入的參數validateScopes設置為True。

public class ServiceProviderOptions
{
    public bool ValidateScopes { get; set; }
}

public static class ServiceCollectionContainerBuilderExtensions
{
    public static ServiceProvider BuildServiceProvider(this IServiceCollection services, ServiceProviderOptions options);
    public static ServiceProvider BuildServiceProvider(this IServiceCollection services, bool validateScopes);
}

針對服務范圍的驗證對於IServiceProvider來說是一項額外附加的操作,會對性能帶來或多或少的影響,所以一般情況下這個開關只會在開發(Development)環境被開啟,對於產品(Production)或者預發(Staging)環境下最好將其關閉。

依賴注入[1]: 控制反轉
依賴注入[2]: 基於IoC的設計模式
依賴注入[3]: 依賴注入模式
依賴注入[4]: 創建一個簡易版的DI框架[上篇]
依賴注入[5]: 創建一個簡易版的DI框架[下篇]
依賴注入[6]: .NET Core DI框架[編程體驗]
依賴注入[7]: .NET Core DI框架[服務注冊]
依賴注入[8]: .NET Core DI框架[服務消費]


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM