依賴注入是 ASP.NET Core 里的核心概念之一,我們平常總是愉快地在Startup類的ConfigureServices方法里往IServiceCollection里注冊各種類型,以致有一些同學可能誤以為依賴注入是只有 ASP.NET Core 才有的特性。但實際上依賴注入也可以用於 .NET Core 的 Console app. 別忘了, ASP.NET Core 的應用本質上也只是一個 Console app而已。今天我們在Console app里試試依賴注入。
我們的目標是創建一個Console app,在其中引入依賴注入,注冊不同生命周期的類型,然后創建幾個線程,每個線程分別依靠依賴注入“創建”若干類型實例,然后觀察不同生命周期下這些實例變量是否指向一個實例還是各不相同。
ServiceCollection
現在閉上眼睛想象一下(別睡着了),我們自己就是依賴注入的執行者,如果有一個漂亮的程序媛跟我們說她要某某類型的一個實例,我們應該怎么做?我們首先需要知道這某某類型是個什么東西以及如何創建對吧?我們如何知道呢?當然是她得提前告訴我們啊,而我們要有個地方把這些信息保留下來然后在需要的時候可以查閱。在 .NET Core里,可以依賴注入的類型叫Service,而記錄這些Service信息的這地方就是ServiceCollection。
所以,當程序運行起來之后,我們第一件事情就是創建一個ServiceCollection,怎么創建呢? new 唄
// 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里獲取實例。
有趣的是, IServiceProvider是System命名空間下的。
Service的生命周期
自脫離 ASP.NET Web Form 的世界以來,已經很少聽到、看到“生命周期”這個詞了。遙想當年無論是面試還是被面試,“ASP.NET 頁面的生命周期”那簡直是必備問題 —— 跑題了。
還是閉上眼睛(還是別睡着了),想象一下,還是那個漂亮的程序媛,她略帶嬌嗔地對我們說:“好哥哥,幫我把這個某某類型注冊到依賴注入里吧,可以嗎?”,既然我們現在有了ServiceCollection,注冊當然不成問題~,但再仔細想想,當我們把某某類型添加到ServiceCollection,繼而創建出一個IServiceProvider給程序媛妹子,接着程序媛妹子不停地從ServiceCollection里獲取實例時,她得到的是同一個實例呢還是每次請求都給她一個新的實例?誰知道,得問她才知道。所以平常不擅言辭、從不廢話的我們不能浪費這次交流的機會,在程序媛妹子讓我們注冊類型的時候我們還要問清楚她想怎樣得到這個類型的實例,每次都給她一個新的,還是總給她同一個?換句話說,當一個Service被注冊到ServiceCollection的時候,我們需要同時知道它的類型和實例生命周期。
ServiceCollection很體貼,我們可以直接用不同的注冊方法注冊不同生命周期的Service:
// 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個生命周期類型基本上涵蓋了所有可能的場景:
- 每次都要新實例。
- 永遠都只需要同一個實例。
- 在一個范圍之內只需要同一個實例,但是不同范圍之內的實例要不同。
醒醒,無聊的理論時間過去了,Demo 上場了
說書者曰“閑話休提,且說正話”,咱們也到了“理論休提,且看Demo”的時候了。 .NET Core 的一大優點是命令行友好,並且不用特別依靠功能強大但臃腫的 Visual Studio來開發。我的Demo是在 MacOS + .NET Core CLI (v1.1) + Visual Studio Code 環境下創建和運行的。這套環境在其它平台下的體驗幾乎沒什么區別。
首先打開一個命令行,創建一個目錄,然后在新創建的目錄里執行 dotnet new命令。這將創建一個最簡單的 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!自此,我們應該使用這個來自IServiceScope的IServiceProvider(取代之前我們得到的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種不同的生命周期,看看來自IServiceScope的IServiceProvider能否正確處理每一種生命周期類型。
代碼有些啰嗦,因為不想再拆分成更小的方法了:
/*
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上獲取。
