2019年6月13日 星期四

.NET Core 的相依性注入的三種生命週期管理與使用 CreateScope 建立兩個 Scope 的用法差異

.NET Core 的相依性注入的三種生命週期管理與使用 CreateScope 建立兩個 Scope 的用法差異

在 .NET Core 平台下,可以透過 Microsoft.Extensions.DependencyInjection 套件,提供 DI Container 容器的服務,這裡需要先建立一個 ServiceCollection 物件,透過此物件進行各個型別對應的註冊需求,接著使用 ServiceCollection.BuildServiceProvider() 產生出一個 IServiceProvider 物件,如此,便可以透過該物件進行所需要服務物件的產生動作,這裡會透過建構函式注入的方式,將相依抽象介面或者具體類別相依的型別,產生出該服務的物件。
在 .NET Core 平台下,期 DI 容器共提供三種物件存留期 Service Lifetime ,分別是: 暫時性 Transient 、 具範圍 Scoped 、 單一 Singleton 這三種存留期模式,根據微軟官方文件上的說明,這三種存留期的意義為:
  • 暫時性 Transient
    每次從服務容器要求暫時性存留期服務時都會建立它們。 此存留期最適合用於輕量型的無狀態服務。
  • 具範圍 Scoped
    具範圍存留期服務會在每次用戶端要求 (連線)request (connection) 時建立一次。
  • 單一 Singleton
    當第一次收到有關單一資料庫存留期服務的要求時 (或是當執行 ConfigureServices 而且隨著服務註冊指定執行個體時),即會建立單一資料庫存留期服務。 每個後續要求都會使用相同的執行個體。
在這篇文章將會來探討這三種物件存留期的差異性,這篇文章的範例專案程式碼可以從 GitHub 取得
首先,先建立需要用到的介面 IMessage 與類別 ConsoleMessage,該介面內有宣告一個 Write 方法,而在 ConsoleMessage 類別內,將會在建構函式內將會取得該執行個體/物件的 Hash Code 值(等下會使用這個值來區分是否為同一個物件),接著在螢幕上顯示一段訊息,說明這個 ConsoleMessage 物件已經被建立起來了。
另外,也會建立一個解構式 ~ConsoleMessage() 方法,在此方法內將會顯示這個執行個體 Instance / 物件 Object 已經被 .NET GC Garbage Collection 資源回收了。
C Sharp / C#
public interface IMessage
{
    string Write(string message);
}
public class ConsoleMessage : IMessage
{
    int HashCode;
    public ConsoleMessage()
    {
        HashCode = this.GetHashCode();
        Console.WriteLine($"ConsoleMessage ({HashCode}) 已經被建立了");
    }
    public string Write(string message)
    {
        string result = $"[Console 輸出  ({HashCode})] {message}";
        Console.WriteLine(result);
        return result;
    }
    ~ConsoleMessage()
    {
        Console.WriteLine($"ConsoleMessage ({HashCode}) 已經被釋放了");
    }
}
現在,先來測試 暫時性 Transient 這樣的存留期的特性,在底下的測試程式碼,將會使用 serviceCollection.AddTransient<IMessage, ConsoleMessage>(); 敘述將 IMessage 與 ConsoleMessage 這兩個相依型別註冊到 DI 容器內,接著使用 serviceProvider1 = serviceCollection.BuildServiceProvider(); 敘述取得 IServiceProvider 物件。
接下來將會透過 message1 = serviceProvider1.GetService<IMessage>() 與 message2 = serviceProvider1.GetService<IMessage>() 這兩個敘述,分別請求 DI 容器來注入一個 ConsoleMessage 物件,此時,透過測試程式執行結果,將會知道 DI 容器將會產生兩個 ConsoleMessage 物件
在這篇文章中的測試方式,請在Release 建置模式來建立這個測試專案,接著切換到 CoreDILifetimeScope\CoreDILifetimeScope\bin\Release\netcoreapp2.2 目錄下,開啟 命令提示字元視窗,確認命令提示字元視窗是在這個目錄下,請執行這個測試專案,使用這個命令 dotnet CoreDILifetimeScope.dll,底下是執行結果內容。
在這兩個服務物件建立成功之後,將會執行該執行個體的 Write 方法,接著,把這兩個物件的變數設定為 null,並且使用 GC.Collect(2) 強制啟動 .NET 記憶體回收機制,若某個物件的記憶體被回收的話,將會看到類似這樣的訊息 ConsoleMessage (32854180) 已經被釋放了 這表示 message1 這個參考物件已經被系統回收記憶體空間了,不過,message2卻還沒有被記憶體回收,這是因為 message2 這個物件在呼叫記憶體回收方法之後,還會有用到,所以,該 message2 物件事沒有被回收的。
所以,當使用 暫時性 Transient 的物件存留期方式註冊到 DI 容器內,每一次請求 DI 容器解析出抽象型別的時候,都會建立一個新的對應具體類別的物件。
ConsoleMessage (32854180) 已經被建立了
[Console 輸出  (32854180)] M1 - Vulcan
[Console 輸出  (32854180)] M2 - Lee
[Console 輸出  (32854180)] M9 - Vulcan Lee
ConsoleMessage (43942917) 已經被建立了
[Console 輸出  (43942917)] M1_1 - Ada
[Console 輸出  (43942917)] M2_1 - Chan
ConsoleMessage (32854180) 已經被釋放了
[Console 輸出  (43942917)] M9_1 - Ada Chan
C Sharp / C#
IMessage message1;
IMessage message2;
IMessage message1_1;
IMessage message2_1;
IMessage message9_1;
IMessage message9;
IServiceProvider serviceProvider1;
IServiceProvider serviceProvider2;
IServiceProvider serviceProvider3;
IServiceCollection serviceCollection;
IServiceScope serviceScope2;
IServiceScope serviceScope3;

serviceCollection = new ServiceCollection();
serviceCollection.AddTransient<IMessage, ConsoleMessage>();
//serviceCollection.AddScoped<IMessage, ConsoleMessage>();
//serviceCollection.AddSingleton<IMessage, ConsoleMessage>();
serviceProvider1 = serviceCollection.BuildServiceProvider();

#region 使用預設 Scope
message1 = serviceProvider1.GetService<IMessage>();
message1.Write("M1 - Vulcan");
message2 = serviceProvider1.GetService<IMessage>();
message2.Write("M2 - Lee");
message1 = null;
message2 = null;
GC.Collect(2);
Thread.Sleep(1000);
message9 = serviceProvider1.GetService<IMessage>();
message9.Write("M9 - Vulcan Lee");
#endregion
延續剛剛的測試程式碼,請將 serviceCollection.AddTransient<IMessage, ConsoleMessage>(); 敘述註解起來,並且把這個 serviceCollection.AddSingleton<IMessage, ConsoleMessage>(); 敘述解除註解,重新建置、在命令提示字元視窗下重新執行一次,將會看到底下的內容。
在 單一 Singleton 存留期的註冊模式下,當要解析單一存留期的服務物件的時候,這個服務物件將不會被記憶體回收,直到這個 DI 容器因為應用程式結束之後,才會被回收。因此,輸出結果只會看到一次 ConsoleMessage (32854180) 已經被建立了 這樣的敘述,而每次呼叫 Write 敘述的時候,將會看到使用的是同一個物件。
ConsoleMessage (32854180) 已經被建立了
[Console 輸出  (32854180)] M1 - Vulcan
[Console 輸出  (32854180)] M2 - Lee
[Console 輸出  (32854180)] M9 - Vulcan Lee
延續剛剛的測試程式碼,請將 serviceCollection.AddSingleton<IMessage, ConsoleMessage>(); 敘述註解起來,並且把這個 serviceCollection.AddScoped<IMessage, ConsoleMessage>(); 敘述解除註解,重新建置、在命令提示字元視窗下重新執行一次,將會看到底下的內容。
在 具範圍 Scoped 存留期的註冊模式下,因為接下來進行服務物件解析的時候,所使用的 範圍 Scope 為這個 DI 容器的預設範圍,因此,當要解析 具範圍 Scoped 存留期的服務物件的時候,一旦配置這樣的服務物件之後,這個服務物件將不會被記憶體回收,直到這個 範圍 (也就是預設範圍) 物件不再使用之後,也就是 DI 容器因為應用程式結束之後,才會被回收。因此,輸出結果只會看到一次 ConsoleMessage (32854180) 已經被建立了 這樣的敘述,而每次呼叫 Write 敘述的時候,將會看到使用的是同一個物件。
ConsoleMessage (32854180) 已經被建立了
[Console 輸出  (32854180)] M1 - Vulcan
[Console 輸出  (32854180)] M2 - Lee
[Console 輸出  (32854180)] M9 - Vulcan Lee
在剛剛的練習中,具範圍 Scoped 存留期 的模式與 單一 Singleton 存留期 模式似乎運作方式都相同的,不過,其實這兩種存留期模式是不相同的;現在來測試多 具範圍 Scoped 存留期 的模式。
在 ASP.NET Core 開發框架下,每一個 HTTP 請求 Request 連線 Connection 連線請求的時候,都會自動產生 Scope,因此,在同一個 HTTP 請求連線中,若使用 具範圍 Scoped 存留期 註冊到 DI 容器下的時候,在相同的 HTTP 請求連線中所獲得的服務物件都會是同一個,而在不同的 HTTP 請求連線下,就會產生出另外一個新的服務物件。在此,就會模擬兩個 HTTP 請求連線自動建立起兩個範圍 Scope,這裡會使用多範圍物件的進行練習。
現在把這個練習專案的原始碼修改成為如下,同樣的,還是使用 serviceCollection.AddScoped<IMessage, ConsoleMessage>() 進行具範圍的存留期註冊,現在當使用 serviceProvider1 = serviceCollection.BuildServiceProvider(); 敘述取得 IServiceProvide 物件之後,就可以呼叫 serviceScope2 = serviceProvider1.CreateScope(); 方法,產生一個 IServiceScope 物件,接著透過這個 IServiceScope 物件再來產生一個 IServiceProvider 物件,這裡使用這個敘述: serviceProvider2 = serviceScope2.ServiceProvider;,現在在這個 serviceProvider2 進行注入的同一個介面型別的服務物件,都會是同一個。
然而,若使用 serviceScope3 = serviceProvider1.CreateScope(); 敘述來產生另外一個 IServiceScope 物件,緊接著使用 serviceProvider3 = serviceScope3.ServiceProvider; 物件來產生另外一個 IServiceProvider 物件,在這裡要注入多個具範圍的服務物件,都是同一個相同服務物件。
在這裡, message1, message2 都是使用 serviceProvider2 物件來注入 IMessage 介面的服務物件,從執行結果看到都是同一個物件;而 message1_1, message2_1 卻是使用 serviceProvider3 物件來注入 IMessage 介面的服務物件,從執行結果看到都是同一個物件;而透過 serviceProvider2 與 serviceProvider3 所產生的 IMessage 服務物件卻是不相同的。
ConsoleMessage (32854180) 已經被建立了
[Console 輸出  (32854180)] M1 - Vulcan
[Console 輸出  (32854180)] M2 - Lee
[Console 輸出  (32854180)] M9 - Vulcan Lee
ConsoleMessage (43942917) 已經被建立了
[Console 輸出  (43942917)] M1_1 - Ada
[Console 輸出  (43942917)] M2_1 - Chan
ConsoleMessage (32854180) 已經被釋放了
[Console 輸出  (43942917)] M9_1 - Ada Chan
現在,若把最後一行程式碼 message9.Write("M9 - Vulcan Lee"); 解除註解,並且建置、再度執行一次,將會看到底下的結果。這次的輸出結果與上面步驟的執行結果卻是不相同,差別在於這裡沒有任何物件被釋放掉。
ConsoleMessage (32854180) 已經被建立了
[Console 輸出  (32854180)] M1 - Vulcan
[Console 輸出  (32854180)] M2 - Lee
[Console 輸出  (32854180)] M9 - Vulcan Lee
ConsoleMessage (43942917) 已經被建立了
[Console 輸出  (43942917)] M1_1 - Ada
[Console 輸出  (43942917)] M2_1 - Chan
[Console 輸出  (43942917)] M9_1 - Ada Chan
[Console 輸出  (32854180)] M9 - Vulcan Lee
C Sharp / C#
IMessage message1;
IMessage message2;
IMessage message1_1;
IMessage message2_1;
IMessage message9_1;
IMessage message9;
IServiceProvider serviceProvider1;
IServiceProvider serviceProvider2;
IServiceProvider serviceProvider3;
IServiceCollection serviceCollection;
IServiceScope serviceScope2;
IServiceScope serviceScope3;

serviceCollection = new ServiceCollection();
//serviceCollection.AddTransient<IMessage, ConsoleMessage>();
serviceCollection.AddScoped<IMessage, ConsoleMessage>();
//serviceCollection.AddSingleton<IMessage, ConsoleMessage>();
serviceProvider1 = serviceCollection.BuildServiceProvider();

#region 使用兩個 Scope
serviceScope2 = serviceProvider1.CreateScope();
serviceProvider2 = serviceScope2.ServiceProvider;
message1 = serviceProvider2.GetService<IMessage>();
message1.Write("M1 - Vulcan");
message2 = serviceProvider2.GetService<IMessage>();
message2.Write("M2 - Lee");
message1 = null;
message2 = null;
GC.Collect(2);
Thread.Sleep(1000);
message9 = serviceProvider2.GetService<IMessage>();
message9.Write("M9 - Vulcan Lee");

serviceScope3 = serviceProvider1.CreateScope();
serviceProvider3 = serviceScope3.ServiceProvider;
message1_1 = serviceProvider3.GetService<IMessage>();
message1_1.Write("M1_1 - Ada");
message2_1 = serviceProvider3.GetService<IMessage>();
message2_1.Write("M2_1 - Chan");
message1_1 = null;
message2_1 = null;
GC.Collect(2);
Thread.Sleep(1000);
message9_1 = serviceProvider3.GetService<IMessage>();
message9_1.Write("M9_1 - Ada Chan");
// 若將底下的程式碼註解起來(在 AddScoped 模式),則 
// message1, message2 指向到 ConsoleMessage 會被釋放掉
//message9.Write("M9 - Vulcan Lee");
#endregion




沒有留言:

張貼留言