依賴注入[極速ssc源碼修復]: .NET Core DI - 服務消費

包含服務極速ssc源碼修復 QQ2952777280【話仙源碼論壇】hxforum.com 註冊信息的IServiceCollection對象最終被用來創建作爲DI容器的IServiceProvider對象。當需要消費某個服務實例的時候,我們只需要指定服務類型調用IServiceProvider的GetService方法,IServiceProvider就會根據對應的服務註冊提供所需的服務實例。

01

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>方法。

02

構造函數的選擇

對於通過調用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所示的輸出結果。

圖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異常,並提示無法從兩個候選的構造函數中選擇出一個最優的來創建服務實例。

圖2 構造函數的選擇策略

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

03

服務範圍

對於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所示的“父子關係”。

圖3 IServiceScope與IServiceProvider(邏輯結構)

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

圖4 IServiceScope與IServiceProvider(物理結構)

04

三種生命週期模式

只有在充分了解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列表。

圖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回收。

05

ASP.NET Core應用下的生命週期

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

圖6 生命週期管理

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

06

服務範圍檢驗

如果我們在一個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)環境下最好將其關閉。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章