依賴注入[6]: .NET Core DI框架[編程體驗]


毫不誇張地說,整個ASP.NET Core框架是建立在一個依賴注入框架之上的,它在應用啟動時構建請求處理管道過程中,以及利用該管道處理每個請求過程中使用到的服務對象均來源於DI容器。該DI容器不僅為ASP.NET Core框架提供必要的服務,同時作為了應用的服務提供者,依賴注入已經成為了ASP.NET Core應用基本的編程模式。在前面一系列的文章中,我們主要從理論層面講述了依賴注入這種設計模式,補充必要的理論基礎是為了能夠理解與ASP.NET Core框架無縫集成的依賴注入框架的設計原理。我們總是采用“先簡單體驗,后者深入剖析”來講述每一個知識點,所以我們利用一些簡單的實例從編程層面來體驗一下服務注冊的添加和服務實例的提取。

一、服務的注冊與消費

為了讓讀者朋友們能夠更加容易地認識依賴注入框架的實現原理和編程模式,我在《依賴注入[4]: 創建一個簡易版的DI框架[上篇]》和《依賴注入[5]: 創建一個簡易版的DI框架[下篇]》自行創建了一個名為Cat的依賴注入框架。不論是編程模式和實現原理,Cat與我們現在即將介紹的依賴注入框架都非常相似,對於后者提供的每一個特性,我們幾乎都能在Cat中找到對應物。

我在設計Cat的時候即將它作為提供服務實例的DI容器,也作為了存放服務注冊的容器,但是與ASP.NET Core框架集成的這個依賴注入框架則將這兩者分離開來。我們添加的服務注冊被保存到通過IServiceCollection接口表示的集合之中,基於這個集合創建的DI容器體現為一個IServiceProvider

由於作為DI框架的IServiceProvider具有類似於Cat的層次結構,所以兩者對提供的服務實例采用一致的生命周期管理方式。DI框架利用如下這個枚舉ServiceLifetime提供了SingletonScopedTransient三種生命周期模式是,我在Cat中則將其命名為RootSelfTransient,前者命名關注於現象,而我則關注於內部實現。

public enum ServiceLifetime
{
    Singleton,
    Scoped,
    Transient
}
應用初始化過程中添加的服務注冊是DI容器用於提供所需服務實例的依據。由於IServiceProvider總是利用指定的 服務類型 來提供對應服務實例,所以服務是基於類型進行注冊的,我們傾向於利用接口來對服務進行抽象,所以這里的服務類型一般為接口。除了以指定服務實例的形式外(默認采用Singleton模式),我們在注冊服務的時候必須指定一個具體的生命周期模式。
  • 指定注冊非服務類型和實現類型;
  • 指定一個現有的服務實例;
  • 指定一個創建服務實例的委托對象。

我們定義了如下的接口和對應的實現類型來演示針對DI框架的服務注冊和提取。其中Foo、Bar和Baz分別實現了對應的接口IFoo、IBar和IBaz,為了反映Cat對服務實例生命周期的控制,我們讓它們派生於同一個基類Base。Base實現了IDisposable接口,我們在其構造函數和實現的Dispose方法中打印出相應的文字以確定對應的實例何時被創建和釋放。我們還定義了一個泛型的接口IFoobar<T1, T2>和對應的實現類Foobar<T1, T2>來演示針對泛型服務實例的提供。

public interface IFoo {}
public interface IBar {}
public interface IBaz {}
public interface IFoobar<T1, T2> {}
public class Base : IDisposable
{
    public Base() => Console.WriteLine($"An instance of {GetType().Name} is created.");
    public void Dispose() => Console.WriteLine($"The instance of {GetType().Name} is disposed.");
}

public class Foo : Base, IFoo, IDisposable { }
public class Bar : Base, IBar, IDisposable { }
public class Baz : Base, IBaz, IDisposable { }
public class Foobar<T1, T2>: IFoobar<T1,T2>
{
    public IFoo Foo { get; }
    public IBar Bar { get; }
    public Foobar(IFoo foo, IBar bar)
    {
        Foo = foo;
        Bar = bar;
    }
}

在如下所示的代碼片段中我們創建了一個ServiceCollection(它是對IServiceCollection接口的默認實現)對象並調用相應的方法(AddTransient、AddScoped和AddSingleton)針對接口IFoo、IBar和IBaz注冊了對應的服務,從方法命名可以看出注冊的服務采用的生命周期模式分別為Transient、Scoped和Singleton。在完成服務注冊之后,我們調用IServiceCollection接口的擴展方法BuildServiceProvider創建出代表DI容器的IServiceProvider對象,並利用它調用后者的GetService<T>方法來提供相應的服務實例。調試斷言表明IServiceProvider提供的服務實例與預先添加的服務注冊是一致的。

class Program
{
    static void Main()
    {
        var provider = new ServiceCollection()
            .AddTransient<IFoo, Foo>()
            .AddScoped<IBar>(_ => new Bar())
            .AddSingleton<IBaz, Baz>()
            .BuildServiceProvider();
        Debug.Assert(provider.GetService<IFoo>() is Foo);
        Debug.Assert(provider.GetService<IBar>() is Bar);
        Debug.Assert(provider.GetService<IBaz>() is Baz); 
    }
}

除了提供類似於IFoo、IBar和IBaz這樣非泛型服務實例之外,如果具有對應的泛型定義(Generic Definition)的服務注冊,IServiceProvider同樣也能提供泛型服務實例。如下面的代碼片段所示,在為創建的ServiceCollection對象添加了針對IFoo和IBar接口的服務注冊之后,我們調用AddTransient方法注冊了針對泛型定義IFoobar<,>的服務注冊,實現的類型為Foobar<,>。當我們利用ServiceCollection創建出代表DI容器的IServiceProvider對象並利用后者提供一個類型為IFoobar<IFoo, IBar>的服務實例的時候,它會創建並返回一個Foobar<Foo, Bar>對象。

var provider = new ServiceCollection()
    .AddTransient<IFoo, Foo>()
    .AddTransient<IBar, Bar>()
    .AddTransient(typeof(IFoobar<,>), typeof(Foobar<,>))
    .BuildServiceProvider();

var foobar = (Foobar<IFoo, IBar>)provider.GetService<IFoobar<IFoo, IBar>>();
Debug.Assert(foobar.Foo is Foo);
Debug.Assert(foobar.Bar is Bar);
當我們在進行服務注冊的時候,可以為同一個類型添加多個服務注冊,實際上添加的所有服務注冊均是有效的。不過由於擴展方法GetService<T>總是返回一個唯一的服務實例,我們對該方法采用了“后來居上”的策略,即總是采用最近添加的服務注冊來創建服務實例。如果我們調用另一個擴展方法GetServices<T>,它將利用返回所有服務注冊提供的服務實例。如下面的代碼片段所示,我們為創建的 ServiceCollection 對象添加了三個針對Base類型的服務注冊,對應的實現類型分別為Foo、Bar和Baz。我們最后將Base作為泛型參數調用了GetServices<Base>方法,該方法會返回包含三個Base對象的集合,集合元素的類型分別為Foo、Bar和Baz。
var services = new ServiceCollection()
    .AddTransient<Base, Foo>()
    .AddTransient<Base, Bar>()
    .AddTransient<Base, Baz>()
    .BuildServiceProvider()
    .GetServices<Base>();
Debug.Assert(services.OfType<Foo>().Any());
Debug.Assert(services.OfType<Bar>().Any());
Debug.Assert(services.OfType<Baz>().Any());
對於IServiceProvider針對服務實例的提供還具有這么一個細節:如果我們在調用GetService或者GetService<T>方法是將服務類型設置為IServiceProvider接口類型,提供的服務實例實際上就是當前的IServiceProvider對象。這一特性意味着我們可以將代表DI容器的IServiceProvider作為服務進行注入,但是在《 依賴注入[3]: 依賴注入模式》已經提到過,一旦我們在應用中利用注入的IServiceProvider來獲取其他依賴的服務實例,意味着我們在使用“ Service Locator”模式。這是一種“反模式(Anti-Pattern)”,如果迫不得已最好不要這么做。IServiceProvider的這一特性體現在如下所示的調試斷言中。
var provider = new ServiceCollection().BuildServiceProvider();
Debug.Assert(provider.GetService<IServiceProvider>() == provider);

二、生命周期管理

IServiceProvider之間的層次結構造就了三種不同的生命周期模式:由於Singleton服務實例保存在作為根容器的IServiceProvider對象上,所以它能夠在多個同根IServiceProvider對象之間提供真正的單例保證。Scoped服務實例被保存在當前IServiceProvider上,所以它只能在當前IServiceProvider對象的“服務范圍”保證的單例的。沒有實現IDisposable接口的Transient服務則采用“即用即取,用后即棄”的策略。

接下來我們通過簡單的實例來演示三種不同生命周期模式的差異。在如下所示的代碼片段中我們創建了一個ServiceCollection對象並針對接口IFoo、IBar和IBaz注冊了對應的服務,它們采用的生命周期模式分別為Transient、Scoped和Singleton。在利用ServiceCollection創建出代表DI容器的IServiceProvider對象之后,我們調用其CreateScope方法創建了兩個所謂的“服務范圍”,后者的ServiceProvider屬性返回一個新的IServiceProvider對象,它實際上是當前IServiceProvider對象的子容器。我們最后利用作為子容器的IServiceProvider對象來提供相應的服務實例。

class Program
{
    static void Main()
    {
        var root = new ServiceCollection()
            .AddTransient<IFoo, Foo>()
            .AddScoped<IBar>(_ => new Bar())
            .AddSingleton<IBaz, Baz>()
            .BuildServiceProvider();
        var provider1 = root.CreateScope().ServiceProvider;
        var provider2 = root.CreateScope().ServiceProvider;

        void GetServices<TService>(IServiceProvider provider)
        {
            provider.GetService<TService>();
            provider.GetService<TService>();
        }

        GetServices<IFoo>(provider1);
        GetServices<IBar>(provider1);
        GetServices<IBaz>(provider1);
        Console.WriteLine();
        GetServices<IFoo>(provider2);
        GetServices<IBar>(provider2);
        GetServices<IBaz>(provider2);
    }
}
上面的程序運行之后會在控制台上輸出如圖1所示的結果。由於服務IFoo被注冊為Transient服務,所以IServiceProvider針對該接口類型的四次請求都會創建一個全新的Foo對象。IBar服務的生命周期模式為Scoped,如果我們利用同一個IServiceProvider對象來提供對應的服務實例,它只會創建一個Bar對象,所以整個程序執行過程中會創建兩個Bar對象。IBaz服務采用Singleton生命周期,所以具有同根的兩個IServiceProvider對象提供的總是同一個Baz對象,后者只會被創建一次。

4-1
圖1 IServiceProvider按照服務注冊對應的生命周期模式提供服務實例

作為DI容器的IServiceProvider不僅僅為我們提供所需的服務實例,它還幫我們管理者這些服務實例的生命周期。如果某個服務實例實現了IDisposable接口,意味着當生命周期完結的時候需要通過調用Dispose方法執行一些資源釋放操作,這些操作同樣由提供服務實例的IServiceProvider對象來驅動執行。DI框架針對提供服務實例的釋放策略取決於對應的服務注冊采用的生命周期模式,具體的策略如下:

  • Transient和Scoped:所有實現了IDisposable接口的服務實例會被作為服務提供者的當前IServiceProvider對象保存起來,當IServiceProvider對象自身被釋放的時候,這些服務實例的Dispose方法會隨之被調用。

  • Singleton:由於服務實例保存在作為根容器的IServiceProvider對象上,所以后者被釋放的時候調用會觸發針對服務實例的釋放。

對於一個ASP.NET Core應用來說,它具有一個與當前應用綁定,代表全局根容器的IServiceProvider對象。對於處理的每一次請求,ASP.NET Core框架都會利用這個根容器來創建基於當前請求的服務范圍,並利用后者提供的IServiceProvider來提供請求處理所需的服務實例。請求處理完成之后,創建的服務范圍被終結,對應的IServiceProvider對象也隨之被釋放,此時由它提供的Scoped服務實例以及實現了IDisposable接口的Transient服務實例最終得以釋放。

上述的釋放策略可以通過如下的演示實例來印證。我們在如下的代碼片段中創建了一個ServiceCollection對象,並針對不同的生命周期模式添加了針對IFoo、IBar和IBaz的服務注冊。在利用ServiceCollection創建出作為根容器的IServiceProvider之后,我們調用它的CreateScope方法創建出對應的服務范圍。接下來我們利用創建對的服務范圍得到代表子容器的IServiceProvider對象,並用后者提供了三個注冊服務對應的實例。

class Program
{
    static void Main()
    {
        using (var root = new ServiceCollection()
            .AddTransient<IFoo, Foo>()
            .AddScoped<IBar, Bar>()
            .AddSingleton<IBaz, Baz>()
            .BuildServiceProvider())
        {
            using (var scope = root.CreateScope())
            {
                var provider = scope.ServiceProvider;
                provider.GetService<IFoo>();
                provider.GetService<IBar>();
                provider.GetService<IBaz>();
                Console.WriteLine("Child container is disposed.");
            }
            Console.WriteLine("Root container is disposed.");
        }
    }
}

由於代表根容器的IServiceProvider對象和服務范圍的創建都是在using塊中進行的,所有針對它們的Dispose方法都會在using塊結束的地方被調用,為了確定方法被調用的時機,我們特意在控制台上打印了相應的文字。該程序運行之后會在控制台上輸出如圖2所示的結果,我們可以看到當作為子容器的IServiceProvider對象被釋放的時候,由它提供的兩個生命周期模式分別為Transient和Scoped的兩個服務實例(Foo和Bar)被正常釋放了。至於生命周期模式為Singleton的服務實例Baz,它的Dispose方法會延遲到作為根容器IServiceProvider對象被釋放的時候。

4-2
圖2 服務實例的釋放

三、服務范圍的檢驗

Singleton和Scoped這兩種不同生命周期是通過將提供的服務實例分別存放到作為根容器的IServiceProvider對象和當前IServiceProvider對象來實現,這意味着作為根容器的IServiceProvider對象提供的Scoped服務實例也是不能被釋放的。如果某個Singleton服務以來另一個Scoped服務,那么Scoped服務實例將被一個Singleton服務實例所引用,意味着Scoped服務實例也成了一個不會被釋放的服務實例。

在ASP.NET Core應用中,當我們將某個服務注冊的生命周期設置為Scoped的真正意圖是希望DI容器根據請求上下文來創建和釋放服務實例,但是一旦出現上述的情況下,意味着Scoped服務實例將變成一個Singleton服務實例,這樣的Scoped服務實例直到應用關閉的哪一個才會得到釋放。如果某個Scoped服務實例引用的資源(比如數據庫連接)需要被及時釋放,這可能會對應用造成滅頂之災。為了避免這種情況下,我們在利用IServiceProvider提供服務過程開啟針對服務范圍的驗證。

如果希望IServiceProvider在提供服務的過程中對服務范圍作有效性檢驗,我們只需要在調用ServiceCollection的BuildServiceProvider方法的時候將一個布爾類型的True值作為參數即可。在如下所示的演示程序中,我們定義了兩個服務接口(IFoo和IBar)和對應的實現類型(Foo和Bar),其中Foo依賴IBar。我們將IFoo和IBar分別注冊為Singleton和Scoped服務,當我們在調用BuildServiceProvider方法創建代表DI容器的IServiceProvider對象的時候將參數設置為True以開啟針對服務范圍的檢驗。我們最后分別利用代表根容器和子容器的IServiceProvider來分別提供這兩種類型的服務實例。

class Program
{
    static void Main()
    {
        var root = new ServiceCollection()
            .AddSingleton<IFoo, Foo>()
            .AddScoped<IBar, Bar>()
            .BuildServiceProvider(true);    
        var child = root.CreateScope().ServiceProvider;

        void ResolveService<T>(IServiceProvider provider)
        {
            var isRootContainer = root == provider ? "Yes" : "No";
            try
            {
                provider.GetService<T>();
                Console.WriteLine( $"Status: Success; Service Type: {typeof(T).Name}; Root: {isRootContainer}");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Status: Fail; Service Type: {typeof(T).Name}; Root: {isRootContainer}");
                  Console.WriteLine($"Error: {ex.Message}");
            }
        }

        ResolveService<IFoo>(root);
        ResolveService<IBar>(root);
        ResolveService<IFoo>(child);
        ResolveService<IBar>(child);
    }
}

public interface IFoo {}
public interface IBar {}
public class Foo : IFoo
{
    public IBar Bar { get; }
    public Foo(IBar bar) => Bar = bar;
}
public class Bar : IBar {}
上面這個演示實例啟動之后將在控制台上輸出如圖3所示的輸出結果。從輸出結果可以看出針對四個服務解析,只有一次(使用代表子容器的IServiceProvider提供IBar服務實例)是成功的。這個實例充分說明了一旦開啟了針對服務范圍的驗證,IServiceProvider對象不可能提供以單例形式存在的Scoped服務。

4-3
圖3 IServiceProvider針對服務范圍的檢驗


依賴注入[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