Console app 裏的依賴注入及其實例生命週期

依賴注入是 ASP.NET Core 裏的核心概念之一,咱們日常老是愉快地在Startup類的ConfigureServices方法裏往IServiceCollection裏註冊各類類型,以至有一些同窗可能誤覺得依賴注入是隻有 ASP.NET Core 纔有的特性。但實際上依賴注入也能夠用於 .NET Core 的 Console app. 別忘了, ASP.NET Core 的應用本質上也只是一個 Console app而已。今天咱們在Console app裏試試依賴注入。git

咱們的目標是建立一個Console app,在其中引入依賴注入,註冊不一樣生命週期的類型,而後建立幾個線程,每一個線程分別依靠依賴注入「建立」若干類型實例,而後觀察不一樣生命週期下這些實例變量是否指向一個實例仍是各不相同。github

ServiceCollection

如今閉上眼睛想象一下(別睡着了),咱們本身就是依賴注入的執行者,若是有一個漂亮的程序媛跟咱們說她要某某類型的一個實例,咱們應該怎麼作?咱們首先須要知道這某某類型是個什麼東西以及如何建立對吧?咱們如何知道呢?固然是她得提早告訴咱們啊,而咱們要有個地方把這些信息保留下來而後在須要的時候能夠查閱。在 .NET Core裏,能夠依賴注入的類型叫Service,而記錄這些Service信息的這地方就是ServiceCollection面試

因此,當程序運行起來以後,咱們第一件事情就是建立一個ServiceCollection,怎麼建立呢? newjson

// using Microsoft.Extensions.DependencyInjection
ServiceCollection services = new ServiceCollection();

聽起來高大上的ServiceCollection,其建立居然如此簡單。😓瀏覽器

IServiceProvider

看着 ServiceCollection裏眼花繚亂的各類類型,咱們心中充滿自信,「妹子,說吧,你想要哪一個類型的實例?」,妹子一臉不樂意「要你個頭,我兩手空空拿什麼去取類型的實例?」……對啊,咱們總得給人家一個什麼東西,而後人家能夠用它從ServiceCollection裏獲取實例啊。。。這東西就是IServiceProvider,咱們的ServiceCollection能夠生成一個IServiceProvider,而任何類型的對象,只要有這個IServiceProvider就能夠從咱們的ServiceCollection裏獲取實例。安全

ServiceCollection services = new ServiceCollection();

// 向services註冊各類類型

IServiceProvider sp = services.BuildServiceProvider();

//今後之後,任何握有 sp 的對象能夠從ServiceCollection裏獲取實例。

有趣的是, IServiceProviderSystem命名空間下的。多線程

Service的生命週期

自脫離 ASP.NET Web Form 的世界以來,已經不多聽到、看到「生命週期」這個詞了。遙想當年不管是面試仍是被面試,「ASP.NET 頁面的生命週期」那簡直是必備問題 —— 跑題了。app

仍是閉上眼睛(仍是別睡着了),想象一下,仍是那個漂亮的程序媛,她略帶嬌嗔地對咱們說:「好哥哥,幫我把這個某某類型註冊到依賴注入裏吧,能夠嗎?」,既然咱們如今有了ServiceCollection,註冊固然不成問題~,但再仔細想一想,當咱們把某某類型添加到ServiceCollection,繼而建立出一個IServiceProvider給程序媛妹子,接着程序媛妹子不停地從ServiceCollection裏獲取實例時,她獲得的是同一個實例呢仍是每次請求都給她一個新的實例?誰知道,得問她才知道。因此日常不擅言辭、從不廢話的咱們不能浪費此次交流的機會,在程序媛妹子讓咱們註冊類型的時候咱們還要問清楚她想怎樣獲得這個類型的實例,每次都給她一個新的,仍是總給她同一個?換句話說,當一個Service被註冊到ServiceCollection的時候,咱們須要同時知道它的類型和實例生命週期。ide

ServiceCollection很體貼,咱們能夠直接用不一樣的註冊方法註冊不一樣生命週期的Serviceui

// AddTransient 方法將一個類型註冊爲 Transient 生命週期。所以,每一次你從依賴注入中請求一個 MyTransientClass 實例,你都會獲得一個全新實例化的實例。請求10次,就獲得10個不一樣的實例。
service.AddTransient<MyTransientClass>();

// AddSingleton 方法將一個類型註冊爲 Singleton 生命週期。單體你們都懂,就是不管請求多少次,你從依賴注入都會獲得同一個 MySingletonClass 實例。請求10次,獲得的倒是同一個實例。
service.AddSingleton<MySingletonClass>();

// AddScoped 方法將一個類型註冊爲 Scoped 生命週期。這個生命週期比較特別。若是你的程序裏建立了若干個 "Scope",依賴注入會確保在同一個 Scope 裏,你將獲得同一個 MyScopedClass 實例,而不一樣 Scope 裏的 MyScopedClass 實例是不一樣的
// 假設你有3個Scope,每一個Scope裏請求10次,那麼你將獲得3個不一樣的 MyScopedClass 實例。其中每一個 Scope 裏一個。
// 至於 Scope 究竟是什麼含義,這就因程序而異了。好比在 ASP.NET Core 裏,一個Scope意味着一次瀏覽器請求往返。而在咱們的示例程序裏,一個Scope表明一個線程內部。
service.AddScoped<MyScopedClass>();

以上3個生命週期類型基本上涵蓋了全部可能的場景:

  1. 每次都要新實例。
  2. 永遠都只須要同一個實例。
  3. 在一個範圍以內只須要同一個實例,可是不一樣範圍以內的實例要不一樣。

醒醒,無聊的理論時間過去了,Demo 上場了

說書者曰「閒話休提,且說正話」,我們也到了「理論休提,且看Demo」的時候了。 .NET Core 的一大優勢是命令行友好,而且不用特別依靠功能強大但臃腫的 Visual Studio來開發。個人Demo是在 MacOS + .NET Core CLI (v1.1) + Visual Studio Code 環境下建立和運行的。這套環境在其它平臺下的體驗幾乎沒什麼區別。

首先打開一個命令行,建立一個目錄,而後在新建立的目錄裏執行 dotnet new命令。這將建立一個最簡單的 Console App.

create console app

注意,利用 dotnet new 建立的文件居然加了可執行屬性(因此顯示爲紅色),這應該是個bug,而且會在將來的版本里修復。最後運行 code .會把當前目錄在 Visual Studio Code 裏打開,而後咱們就能夠寫代碼了。

STEP 1: 添加對 Microsoft.Extensions.DependencyInjection 的引用

首先,咱們須要添加一個引用:Microsoft.Extensions.DependencyInjection,依賴注入的默認實現都在裏面。

打開 project.json,而後在dependencies裏添加引用。添加完成以後,project.json應該看起來是這樣的:

{
  "version": "1.0.0-*",
  "buildOptions": {
    "debugType": "portable",
    "emitEntryPoint": true
  },
  "dependencies": {
    "Microsoft.Extensions.DependencyInjection": "1.1.0"
  },
  "frameworks": {
    "netcoreapp1.1": {
      "dependencies": {
        "Microsoft.NETCore.App": {
          "type": "platform",
          "version": "1.1.0"
        }
      },
      "imports": "dnxcore50"
    }
  }
}

添加好引用以後,保存,這時 VS Code的頂部應該會有個提示,說"There are unresolved dependencies from 'project.json'. Please execute the restore command to continue.",你能夠直接點「Restore」按鈕或者手工在命令行裏運行 dotnet restore命令來還原依賴。

若是你觀察新建立的 ASP.NET Core 程序的 project.json 文件,你可能會發現依賴列表裏並無Microsoft.Extensions.DependencyInjection,那爲何咱們在這裏須要添加這個引用呢?這是由於你的 project.json 文件裏有 ASP.NET Core 的引用,好比 Microsoft.AspNetCore.Mvc,而它或者它依賴的引用裏有對Microsoft.Extensions.DependencyInjection的引用。所以你的 ASP.NET Core程序實際上是間接的引用了Microsoft.Extensions.DependencyInjection。咱們的示例程序裏「乾淨」得很,因此必須直接添加對Microsoft.Extensions.DependencyInjection的引用。

注意,我是使用 1.1 版本的 .NET Core,因此引用的版本號都是「1.1.0」,若是你使用的是1.0.0或1.0.1版本的 .NET Core,那麼這裏的版本號會有所不一樣。

STEP2: 準備工做

在咱們的示例程序引入依賴注入以前,有幾項準備工做要作。

首先, 咱們須要一個能夠註冊到依賴注入的類型,這個簡單:

public class MyClass { }

其次, 咱們須要某種方法來檢測從依賴注入中獲得的類型實例是相同的仍是不一樣的。什麼叫相同?就是這些實例都指向內存裏的同一個對象。對此,咱們能夠利用Object類型的靜態方法ReferenceEquals來檢測。顧名思義,無需解釋。可是這個方法自己只能針對2個實例進行檢測,咱們的示例程序想一次獲得10個實例引用,怎麼檢測這10個實例引用是相同仍是不一樣?記得寫SQL語句的時候,有個關鍵字叫Distinct,它能夠剔除集合中的重複項。而咱們引覺得傲的LINQ一樣支持Distinct,咱們能夠把全部實例放到一個集合,而後對集合進行Distinct操做,若是結果是1,說明集合裏全部的實例其實指向同一個對象;若是結果等於集合本來的元素個數,那說明集合裏每個對象都是互不相同的。

鑑於咱們使用多個線程向集合裏插入數據,咱們須要一個多線程安全的集合類型:System.Collections.Concurrent.ConcurrentBag<T>

而調用Distinct方法的時候,咱們但願它能夠明確地以ReferenceEquals的方式比較,這一點能夠經過建立一個實現IEqualityComparer<T>接口的類ReferenceEqualComparer<T>來作到。

public class ReferenceEqualComparer<T> : IEqualityComparer<T>
{
    public bool Equals(T x, T y)
    {
        return Object.ReferenceEquals(x, y);
    }

    public int GetHashCode(T obj)
    {
        return obj.GetHashCode();
    }
}

而後, 咱們建立兩個IEnumerable<T>上的擴展方法來簡化比較操做。

public static class IEnumerableExtensions
{
    public static bool AreIdentical<T>(this IEnumerable<T> bag)
    {
        return bag.Distinct(new ReferenceEqualComparer<T>()).Count() == 1;
    }

    public static bool AreDifferent<T>(this IEnumerable<T> bag)
    {
        return bag.Distinct(new ReferenceEqualComparer<T>()).Count() == bag.Count();
    }
}

最後, 咱們建立一個統一的方法,這個方法能夠傳入一個ServiceCollection對象,而後咱們從中獲取IServiceProvider,再建立10個線程分別利用IServiceProvider獲取服務實例,插入到一個集合中並返回這個集合。

public static ConcurrentBag<MyClass> GetObjectsFromDI(ServiceCollection services)
{
    int threadCount = 10;
    IServiceProvider sp = services.BuildServiceProvider();
    ConcurrentBag<MyClass> bag = new ConcurrentBag<MyClass>();
    Thread[] threads = new Thread[threadCount];

    for (int i = 0; i < threadCount; i++)
    {
        Thread thread = new Thread(RunPerThread);
        threads[i] = thread;
        thread.Start(new Tuple<IServiceProvider, ConcurrentBag<MyClass>>(sp, bag));
    }

    // 確保全部線程都執行完畢以後再繼續
    for (int i = 0; i < threadCount; i++)
    {
        threads[i].Join();
    }

    return bag;
}

public static void RunPerThread(object threadParam)
{
    Tuple<IServiceProvider, ConcurrentBag<MyClass>> args = threadParam as Tuple<IServiceProvider, ConcurrentBag<MyClass>>;
    IServiceProvider sp = args.Item1;
    ConcurrentBag<MyClass> bag = args.Item2;
    for (int i = 0; i < 10; i++)
    {
        bag.Add(sp.GetRequiredService<MyClass>());
    }
}

以上的準備工做使咱們接下來的驗證操做變得容易了不少。

STEP 3: 驗證 Singleton 生命週期

咱們建立一個方法TryOutSingleton來驗證 Singleton 生命週期

private static void TryOutSingleton()
{
    ServiceCollection services = new ServiceCollection(); // 準備好咱們的容器
    services.AddSingleton<MyClass>(); //把MyClass註冊爲 Singleton 生命週期 

    ConcurrentBag<MyClass> bag = GetObjectsFromDI(services); // 調用咱們準備好的方法,用若干線程從 IServiceProvider 中獲取 MyClass 實例,並加入到集合

    Console.WriteLine(bag.AreIdentical()); // 驗證集合中的全部元素是否指向內存中的同一個對象。
}

不出所料,最後輸出的結果是:

True

STEP 4: 驗證 Transient 生命週期

再建立一個 TryOutTransient 方法驗證 Transient 生命週期

private static void TryOutTransient()
{
    ServiceCollection services = new ServiceCollection(); // 準備好咱們的容器
    services.AddTransient<MyClass>(); //把MyClass註冊爲 Transient 生命週期 

    ConcurrentBag<MyClass> bag = GetObjectsFromDI(services); // 調用咱們準備好的方法,用若干線程從 IServiceProvider 中獲取 MyClass 實例,並加入到集合
    
    Console.WriteLine(bag.AreDifferent()); // 驗證集合中的全部元素是否各不相同
}

一樣不出意外,輸出結果是:

True

STEP 5: Scoped 生命週期

前面提到過, Scoped 生命週期比較特別,同一個Scope裏的實例是同一個,可是不一樣Scope裏的實例是不一樣的。而Scope具體的含義取決於咱們本身的定義。

具體到代碼級別,當咱們須要建立一個Scope的時候,咱們須要用到咱們以前獲得的IServiceProvider,它有一個CreateScope方法能夠建立一個類型爲Microsoft.Extensions.DependencyInjection.IServiceScope的Scope,而這個Scope實例有一個IServiceProvider類型的屬性ServiceProvider!自此,咱們應該使用這個來自IServiceScopeIServiceProvider(取代以前咱們獲得的IServiceProvider)來獲取服務實例,它會正確處理Singleton, Transient以及Scoped這3種生命週期!

ServiceCollection services = new ServiceCollection();

// ...

IServiceProvider serviceProvider = services.BuildServiceProvider();

IServiceScope scope = serviceProvider.CreateScope();

IServiceProvider newServiceProvider = scope.ServiceProvider; // 之後靠它來正確處理 Singleton, Transient 和 Scoped 生命週期的實例

MyClass obj = newServiceProvider.GetRequiredService<MyClass>(); // 不管MyClass是哪一種生命週期類型,這裏均可以獲得正確的實例。

爲了驗證Scoped生命週期,咱們如今定義Scope爲線程空間。也就是說,每個線程爲一個Scope,對於Scoped生命週期的類型,在同一個線程以內獲取的實例應該是同一個,可是不一樣線程獲取的實例是不一樣的。

在演示代碼中,咱們註冊3個不一樣的類型,分別對應3種不一樣的生命週期,看看來自IServiceScopeIServiceProvider可否正確處理每一種生命週期類型。

代碼有些囉嗦,由於不想再拆分紅更小的方法了:

/*
public class MySingleton { }
public class MyTransient { }
public class MyScoped { }
*/

private static void TryOutScoped()
{
    Console.WriteLine($"RUNNING {nameof(TryOutScoped)}");

    ServiceCollection services = new ServiceCollection();
    services.AddSingleton<MySingleton>();
    services.AddTransient<MyTransient>();
    services.AddScoped<MyScoped>();

    IServiceProvider sp = services.BuildServiceProvider();

    // 線程1執行
    ConcurrentBag<MySingleton> thread1SingletonBag = new ConcurrentBag<MySingleton>();
    ConcurrentBag<MyTransient> thread1TransientBag = new ConcurrentBag<MyTransient>();
    ConcurrentBag<MyScoped> thread1ScopedBag = new ConcurrentBag<MyScoped>();

    Thread thread1 = new Thread(RunPerThreadWithScopedLifetime);
    thread1.Start(new Tuple<IServiceProvider, ConcurrentBag<MySingleton>, ConcurrentBag<MyTransient>, ConcurrentBag<MyScoped>>(sp, thread1SingletonBag, thread1TransientBag, thread1ScopedBag));

    // 線程2執行
    ConcurrentBag<MySingleton> thread2SingletonBag = new ConcurrentBag<MySingleton>();
    ConcurrentBag<MyTransient> thread2TransientBag = new ConcurrentBag<MyTransient>();
    ConcurrentBag<MyScoped> thread2ScopedBag = new ConcurrentBag<MyScoped>();

    Thread thread2 = new Thread(RunPerThreadWithScopedLifetime);
    thread2.Start(new Tuple<IServiceProvider, ConcurrentBag<MySingleton>, ConcurrentBag<MyTransient>, ConcurrentBag<MyScoped>>(sp, thread2SingletonBag, thread2TransientBag, thread2ScopedBag));

    // 等待執行完畢
    thread1.Join();
    thread2.Join();

    // 驗證全部 MySingleton 的實例都指向內存裏同一個對象
    IEnumerable<MySingleton> singletons = thread1SingletonBag.Concat(thread2SingletonBag);
    Console.WriteLine($"Singleton: {singletons.Count()} objects are IDENTICAL? {singletons.AreIdentical()}");

    // 驗證全部 MyTransient 的實例都各不相同
    IEnumerable<MyTransient> transients = thread1TransientBag.Concat(thread2TransientBag);
    Console.WriteLine($"Transient: {transients.Count()} objects are DIFFERENT? {transients.AreDifferent()}");

    // 對於Scoped生命週期,每一個線程集合內的實例應該指向內存裏同一個對象,而2個線程集合裏的實例應該是不一樣的。
    Console.WriteLine($"collection of thread 1 has {thread1ScopedBag.Count} objects and they are IDENTICAL: {thread1ScopedBag.AreIdentical()}");
    Console.WriteLine($"collection of thread 2 has {thread2ScopedBag.Count} objects and they are IDENTICAL: {thread2ScopedBag.AreIdentical()}");
    Console.WriteLine($"the first object from thread 1 and the first object from thread 2 are IDENTICAL: {Object.ReferenceEquals(thread1ScopedBag.First(), thread2ScopedBag.First())}");

}

輸出結果爲:

RUNNING TryOutScoped
Singleton: 20 objects are IDENTICAL? True
Transient: 20 objects are DIFFERENT? True
collection of thread 1 has 10 objects and they are IDENTICAL: True
collection of thread 2 has 10 objects and they are IDENTICAL: True
the first object from thread 1 and the first object from thread 2 are IDENTICAL: False

演示代碼能夠從Github上獲取。

相關文章
相關標籤/搜索