ASP.NET Core 6框架揭秘實例演示[04]:自定義依賴注入框架

ASP.NET Core框架建立在一個依賴注入框架之上,已注入的方式消費服務已經成為了ASP.NET Core基本的編程模式。為了使讀者能夠更好地理解原生的注入框架框架,我按照類似的設計創建了一個簡易版本的依賴注入框架,並它命名為「Cat」。本篇提供的四個實例主要體現了針對Cat的用法,《一個Mini版的依賴注入框架》提供了針對設計和實現原理的介紹。

[201]模擬容器Cat-普通服務的註冊和提取(源代碼
[202]模擬容器Cat-針對泛型服務類型的支持(源代碼
[203]模擬容器Cat-為同一類型提供多個服務註冊(源代碼
[204]模擬容器Cat-服務實例的生命周期(源代碼

[201]模擬容器Cat-普通服務的註冊和提取

我們定義了如下所示的接口和對應的實現類型來演示針對Cat的服務註冊。Foo、Bar、Baz和Qux分別實現了對應的接口IFoo、IBar、IBaz和IQux,其中Qux類型上標註的MapToAttribute特性註冊了與對應接口IQux之間的映射。四個類型派生於的基類Base實現了IDisposable接口,我們在其構造函數和實現的Dispose方法中輸出相應的文本,以確定對應的實例何時被創建和釋放。我們還定義了一個泛型的接口IFoobar<T1, T2>和對應的實現類Foobar<T1, T2>,用來演示Cat針對泛型服務實例的提供。

public interface IFoo {}
public interface IBar {}
public interface IBaz {}
public interface IQux {}
public interface IFoobar<T1, T2> {}

public class Base : IDisposable
{
    public Base()   => Console.WriteLine($"Instance of {GetType().Name} is created.");
    public void Dispose()  => Console.WriteLine($"Instance of {GetType().Name} is disposed.");
}

public class Foo : Base, IFoo{ }
public class Bar : Base, IBar{ }
public class Baz : Base, IBaz{ }
[MapTo(typeof(IQux), Lifetime.Root)]
public class Qux : Base, IQux { }
public class Foobar<T1, T2>: IFoobar<T1,T2>
{
    public T1 Foo { get; }
    public T2 Bar { get; }
    public Foobar(T1 foo, T2 bar)
    {
        Foo = foo;
        Bar = bar;
    }
}

Lifetime是一個代表服務實例生命周期的枚舉,它代表的三種生命周期模式定義如下。

public enum Lifetime
{
    Root,
    Self,
    Transient
}

如下所示的代碼片段創建了一個Cat對象,並採用上面提到的方式針對接口IFoo、IBar和IBaz註冊了對應的服務,它們採用的生命周期模式分別為Transient、Self和Root。另外,我們還調用了另一個將當前入口程序集作為參數的Register方法,該方法會解析指定程序集中標註了MapToAttribute特性的類型並進行批量服務註冊。對於我們演示的程序來說,該方法會完成針對IQux/Qux類型的服務註冊。接下來我們利用Cat對象創建了它的兩個子容器,並調用子容器的GetService<T>方法來提供相應的服務實例。

using App;

var root = new Cat()
    .Register<IFoo, Foo>(Lifetime.Transient)
    .Register<IBar>(_ => new Bar(), Lifetime.Self)
    .Register<IBaz, Baz>(Lifetime.Root)
    .Register(typeof(Foo).Assembly);
var cat1 = root.CreateChild();
var cat2 = root.CreateChild();

void GetServices<TService>(Cat cat) where TService : class
{
    cat.GetService<TService>();
    cat.GetService<TService>();
}

GetServices<IFoo>(cat1);
GetServices<IBar>(cat1);
GetServices<IBaz>(cat1);
GetServices<IQux>(cat1);
Console.WriteLine();
GetServices<IFoo>(cat2);
GetServices<IBar>(cat2);
GetServices<IBaz>(cat2);
GetServices<IQux>(cat2);

上面的程序運行之後會在控制台上輸出圖1所示的結果。由於服務IFoo被註冊為Transient服務,所以Cat針對四次請求都會創建一個全新的Foo對象。IBar服務的生命周期模式為Self,對於同一個Cat只會創建一個Bar對象,所以整個過程中會創建兩個Bar對象。IBaz和IQux服務採用Root生命周期,所以同根的兩個Cat對象提供的其實是同一個Baz/Qux對象。

image

圖1Cat按照服務註冊對應的生命周期模式提供服務實例

[202]模擬容器Cat-針對泛型服務類型的支持

Cat同樣可以提供泛型服務實例。如下面的代碼片段所示,在為創建的Cat對象添加了針對IFoo和IBar接口的服務註冊之後,我們調用Register方法註冊了針對泛型定義IFoobar<,>的服務註冊,具體的實現類型為Foobar<,>。當我們利用Cat對象提供一個類型為IFoobar<IFoo, IBar>的服務實例時,它會創建並返回一個Foobar<Foo, Bar>對象。

using App;
using System.Diagnostics;

var cat = new Cat()
    .Register<IFoo, Foo>(Lifetime.Transient)
    .Register<IBar, Bar>(Lifetime.Transient)
    .Register(typeof(IFoobar<,>), typeof(Foobar<,>), Lifetime.Transient);

var foobar = (Foobar<IFoo, IBar>?)cat.GetService<IFoobar<IFoo, IBar>>();
Debug.Assert(foobar?.Foo is Foo);
Debug.Assert(foobar?.Bar is Bar);

[203]模擬容器Cat-為同一類型提供多個服務註冊

我們可以為同一個類型提供多個服務註冊。雖然添加的所有服務註冊均是有效的,但由於GetService<TService>擴展方法總是返回一個服務實例,我們對該方法應用了「後來居上」的策略,即採用最近添加的服務註冊創建服務實例。另一個GetServices<TService>擴展方法將返回根據所有服務註冊提供的服務實例。下面的代碼片段為創建的Cat對象添加了三個針對Base類型的服務註冊,對應的實現類型分別為Foo、Bar和Baz。我們調用了Cat對象的GetServices<Base>方法返回包含三個Base對象的集合,集合元素的類型分別為Foo、Bar和Baz。

using App;
using System.Diagnostics;

var services = new Cat()
    .Register<Base, Foo>(Lifetime.Transient)
    .Register<Base, Bar>(Lifetime.Transient)
    .Register<Base, Baz>(Lifetime.Transient)
    .GetServices<Base>();
Debug.Assert(services.OfType<Foo>().Any());
Debug.Assert(services.OfType<Bar>().Any());
Debug.Assert(services.OfType<Baz>().Any());

[204]模擬容器Cat-服務實例的生命周期

如果提供服務實例的類型實現了IDisposable接口,我們必須在適當的時候調用其Dispose方法釋放它。由於服務實例的生命周期完全由作為依賴注入容器的Cat對象來管理,所以通過調用Dispose方法針對服務實例的釋放也由它負責。Cat對象針對提供服務實例的釋放策略取決於採用的生命周期模式,具體的策略如下。

  • TransientSelf:所有實現了IDisposable接口的服務實例會被當前Cat對象保存起來,當Cat對象自身的Dispose方法被調用的時候,這些服務實例的Dispose方法會隨之被調用。
  • Root:由於服務實例保存在作為根容器的Cat對象上,所以當作為根的Cat對象的Dispose方法被調用的時候,這些服務實例的Dispose方法會隨之被調用。

上述釋放策略可以通過如下演示實例來印證。如下代碼片段所示,我們創建了一個Cat對象並添加了相應的服務註冊。我們調用它的CreateChild方法創建了代表子容器的Cat對象,並用它提供了四個註冊服務對應的實例。

using App;
using (var root = new Cat()
            .Register<IFoo, Foo>(Lifetime.Transient)
            .Register<IBar>(_ => new Bar(), Lifetime.Self)
            .Register<IBaz, Baz>(Lifetime.Root)
            .Register(typeof(IFoo).Assembly))
{
    using (var cat = root.CreateChild())
    {
        cat.GetService<IFoo>();
        cat.GetService<IBar>();
        cat.GetService<IBaz>();
        cat.GetService<IQux>();
        Console.WriteLine("Child cat is disposed.");
    }
    Console.WriteLine("Root cat is disposed.");
}

由於兩個Cat對象的創建都是在using塊中進行的,所以它們的Dispose方法都會在using塊結束的地方被調用。該程序運行之後會在控制台上輸出圖2所示的結果,我們可以看到當作為子容器的Cat對象的Dispose方法被調用時,由它提供的兩個生命周期模式分別為Transient和Self的服務實例(Foo和Bar)被正常釋放。而生命周期模式為Root的服務實例(Baz和Qux對象)的Dispose方法會延遲到作為根容器的Cat對象的Dispose方法被調用的時候。

image
圖2 服務實例的釋放