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




2019年6月12日 星期三

在非同步工作中使用 ConfigureAwait ,讓非同步方法當作同步方法呼叫,卻不會造成執行緒封鎖的做法

在非同步工作中使用 ConfigureAwait ,讓非同步方法當作同步方法呼叫,卻不會造成執行緒封鎖的做法

根據微軟官方文件上的說明,ConfigureAwait 代表的意思是 : 設定用來等候這個 Task 的 awaiter,而傳遞的參數為 continueOnCapturedContext ,這是一個 Boolean 型別,若該參數值為 true 表示嘗試將接續封送處理回原始擷取的內容,否則為 false。所以,當想要使用同步呼叫方式來呼叫非同步作業的時候,理論上應該是可以解決掉因為當非同步作業處理結束之後,要回到 同步內容 SynchronizationContext 後,卻不會造成執行緒封鎖的問題,不過,需要使用呼叫這個方法的時候,使用 false 引數, ConfigureAwait(false)
現在,來實際體驗一下,在此建立一個 WPF 的專案,因為 WPF 專案內的 UI 執行緒內會有使用到 同步內容 SynchronizationContext,用來測試要如何使用 ConfigureAwait ,但是卻不會造成執行緒封鎖的問題,在這裡的範例專案原始碼,可以從 GitHub 中取得。
在這個 WPF 專案內,建立出三個按鈕,分別用來展示出如何正確使用 ConfigureAwait(false),但是卻不會造成執行緒封鎖的問題。
xaml
<Window x:Class="HowConfigureAwait.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:HowConfigureAwait"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="50"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <Button
            x:Name="btnWillBlock" Content="會封鎖" Click="BtnWillBlock_Click"
            FontSize="20"/>
        <Button
            Grid.Column="1"
            x:Name="btnWillNotBlock" Content="不會封鎖(非同步工作使用ConfigureAwait)" Click="BtnWillNotBlock_Click"
            FontSize="20"/>
        <Button
            Grid.Column="0" Grid.Row="1"
            x:Name="btnWillNotBlockByAsyncMethod" Content="會封鎖(非同步方法使用ConfigureAwait)" Click="BtnWillNotBlockByAsyncMethod_Click"
            FontSize="20"/>
        <TextBlock
            Grid.Row="2" Grid.ColumnSpan="2"
            x:Name="tbMessage" FontSize="20" Foreground="Red"/>
    </Grid>
</Window>

沒有使用 ConfigureAwait(false),而直接把非同步方法當作同步方式來呼叫

首先,左上角的按鈕,名稱為:會封鎖,當按下這個按鈕,將會使用同步方式 Method1Async().Result 來呼叫設計的非同步方法 async Task<string> Method1Async()。在 Method1Async 非同步方法內,當執行這個 new HttpClient().GetStringAsync(url); 敘述之後,將會得到一個非同步工作物件,接著執行 await task.ConfigureAwait(true) 這個敘述。所以,底下的程式碼將會執行到 string result = task.Result; 敘述之後將會造成執行緒封鎖,因為,當剛剛的 new HttpClient().GetStringAsync(url); 非同步工作執行完畢之後,會需要回到 UI 執行緒下的 同步內容 SynchronizationContext,可是,因為 UI 執行緒下因為執行了 task.Result ,所以,UI 執行緒就會就會進入到封鎖階段,等候非同步工作結束執行,非同步工作執行完畢之後,也想要回到 UI 執行緒,這樣就造成了打死結了。
C Sharp / C#
private async void BtnWillBlock_Click(object sender, RoutedEventArgs e)
{
    string result = Method1Async().Result;
    Debug.WriteLine(result);
}
async Task<string> Method1Async()
{
    Console.WriteLine($"呼叫 Method1Async 之前,執行緒 ID {Thread.CurrentThread.ManagedThreadId}");
    Task<string> task = new HttpClient().GetStringAsync(url);
    await task.ConfigureAwait(true);
    string result = task.Result;
    Console.WriteLine($"呼叫 Method1Async 之後,執行緒 ID {Thread.CurrentThread.ManagedThreadId}");
    return " @@恭喜你 - Method1Async({result}) 已經執行完畢了@@ ";
}

呼叫非同步工作且使用 ConfigureAwait(false),而直接把非同步方法當作同步方式來呼叫

現在,來看看右上角的按鈕,名稱為:不會封鎖(非同步工作使用ConfigureAwait),當按下這個按鈕之後,將不會造成整個應用程式進入凍結的狀態,也就是沒有打死結。在這個按鈕事件被呼叫之後,同樣的使用同步方式來呼叫非同步方法,這裡使用的敘述是 Method2Async().Result
在非同步方法 async Task<string> Method2Async() 內,與上一個按鈕中使用到的 async Task<string> Method1Async() 非同步方法十分類似,唯一的差異就是要等候非同步工作執行完畢的時候,使用了 await task.ConfigureAwait(false); 敘述,也就是指定 await 這個運算子關鍵字,不需要捕捉當前的 同步內容 SynchronizationContext 物件,所以,當非同步工作執行完成之後,也就不會回到 同步內容 SynchronizationContext 的執行緒下。在這樣的程式碼,當非同步方法內的敘述 string result = task.Result; 雖然這個敘述是使用同步的方式來等待非同步的執行結果,但是,當非同步工作完成之後,這個敘述將會得到此非同步工作的執行結果,也不會造成專案凍結與執行緒封鎖的問題。
C Sharp / C#
private void BtnWillNotBlock_Click(object sender, RoutedEventArgs e)
{
    string result = Method2Async().Result;
    Debug.WriteLine(result);
    tbMessage.Text = result;
}
async Task<string> Method2Async()
{
    Console.WriteLine($"呼叫 Method2Async 之前,執行緒 ID {Thread.CurrentThread.ManagedThreadId}");
    Task<string> task = new HttpClient().GetStringAsync(url);
    await task.ConfigureAwait(false);
    string result = task.Result;
    Console.WriteLine($"呼叫 Method2Async 之後,執行緒 ID {Thread.CurrentThread.ManagedThreadId}");
    return $" @@恭喜你 - Method2Async({result}) 已經執行完畢了@@ ";
}

呼叫非同步方法且使用 ConfigureAwait(false),而直接把非同步方法當作同步方式來呼叫

接著,來看看左上角的按鈕,名稱為:會封鎖(非同步方法使用ConfigureAwait),當按下這個按鈕之後,將整個應用程式進入凍結的狀態,也就是打死結了。
來看看這個按鈕事件程式碼,當這個事件被呼叫之後, Task<string> task = Method3Async() 會使用這個方法來取得 Method3Async 非同步方法的工作物件,緊接著使用 task.ConfigureAwait(false) 敘述,指定這個非同步方法不要捕捉當前的 同步內容 SynchronizationContext,當然,當執行完畢之後,也就不會透過 同步內容 SynchronizationContext 回到當時的 UI 執行緒下。而在非同步方法 Method3Async ,將會與第一個,左上角的按鈕,會封鎖 按鈕相同,使用一般的等候工作做法,通常都會使用這樣的敘述 string result = await new HttpClient().GetStringAsync(url) 是相同的。
不幸的是,這樣的設計方式卻會造成整個應用程式凍結、打死結的狀態,可是,為什麼會這樣的,在這裡不是有使用到 task.ConfigureAwait(false) 敘述嗎?但是還是會打死結,問題地底出在哪裡呢?
請在 Method3Async 方法內的 await task.ConfigureAwait(true); 敘述上設定一個除錯中斷點,執行這個專案,並且按下左下角的按鈕,現在這個程式執行到剛剛設定的中斷點上,接著,按下 F10 不進入函式,然後,就看到了悲慘狀況,整個應用程式凍結了;因此,在按鈕事件上,要呼叫非同步方法的時候,使用 task.ConfigureAwait(false) 敘述是沒有問題,因為整個專案不是凍結在按鈕事件的任何一個敘述,而是凍結在非同步方法內的非同步工作呼叫上 await task.ConfigureAwait(true) (這個敘述與 await task 相同的執行結果) 這個敘述。更進一步的解釋那就是,當呼叫這個敘述的時候,當前的執行緒依舊還是 UI 執行緒,並且因為 這個敘述 await task.ConfigureAwait(true) 指定要捕捉 同步內容 SynchronizationContext 物件,並於非同步工作完成之後,使用這個 同步內容 SynchronizationContext 物件回到 UI 執行緒下繼續來執行,結果就是同樣一句話,當前 UI 執行緒正處於封鎖狀態,所以,整個專案就打死結了。
C Sharp / C#
private void BtnWillNotBlockByAsyncMethod_Click(object sender, RoutedEventArgs e)
{
    Task<string> task = Method3Async();
    task.ConfigureAwait(false);
    string result = task.Result;
    Debug.WriteLine(result);
}
async Task<string> Method3Async()
{
    Console.WriteLine($"呼叫 Method1Async 之前,執行緒 ID {Thread.CurrentThread.ManagedThreadId}");
    Task<string> task = new HttpClient().GetStringAsync(url);
    await task.ConfigureAwait(true);
    string result = task.Result;
    Console.WriteLine($"呼叫 Method1Async 之後,執行緒 ID {Thread.CurrentThread.ManagedThreadId}");
    return " @@恭喜你 - Method1Async({result}) 已經執行完畢了@@ ";
}

結論

透過上面的實際範例程式碼展示,想要使用 ConfigureAwait(false) ,指定不要捕捉 同步內容 SynchronizationContext 物件,避免造成執行緒封鎖 Thread Block,就請記得要在 非同步工作 後面加上 ConfigureAwait(false) 敘述,而不要在 非同步方法 後面加上 ConfigureAwait(false) 敘述。
看不懂這句話的話,請要再度細細看過上面的內容,若還是無法理解,這就需要多看些書或者上課來補充這方面的相關知識囉。




在 .NET Core 下使用 DI 容器,對於實作類別的建構函式的參數不同數量測試比較

在 .NET Core 下使用 DI 容器,對於實作類別的建構函式的參數不同數量測試比較

在這篇文章中,將要來測試 .NET Core 下的 Microsoft.Extensions.DependencyInjection 套件,若要注入的具體實作物件中有多個多載建構函式存在的時候,會發生甚麼樣的問題呢?
首先,先來建立要測試的各種介面與具體實作類別,如底下的程式碼;在這裡將會宣告出四個介面,分別是 IMyInterface1, IMyInterface2, IMyInterface3, IYourInterface,前三者的具體實作類別將會是:MyClass1, MyClass2, MyClass3。
對於 IYourInterface 將會由類別 YourClass 來實作,在這個 YourClass 類別內,將會先建立 5 個建構函式,這些建構函式內都會有 Console.WriteLine 函式,輸出這個建構函式的特性文字。
  • 建構函式 1
    為預設建構函式
  • 建構函式 2
    將會需要提供 IMyInterface1, IMyInterface2 具體實作物件,共 2 個參數
  • 建構函式 3
    將會需要提供 IMyInterface1, IMyInterfac3 具體實作物件,共 2 個參數
  • 建構函式 4
    這個建構函式將需要 2 個參數,第一個為 IMyInterface1,第二個為 字串
  • 建構函式 5
    這個建構函式將需要 3 個參數,第一個為 IMyInterface1,第二個為 字串,第三個為 整數
C Sharp / C#
interface IMyInterface1 { }
class MyClass1 : IMyInterface1 { }
interface IMyInterface2 { }
class MyClass2 : IMyInterface2 { }
interface IMyInterface3 { }
class MyClass3 : IMyInterface3 { }
interface IYourInterface { }
class YourClass : IYourInterface
{
    // 建構函式 1
    public YourClass()
    {
        Console.WriteLine("YourClass 預設建構式被呼叫");
    }
    // 建構函式 2
    public YourClass(IMyInterface1 myInterface1, IMyInterface2 myInterface2)
    {
        Console.WriteLine("YourClass 建構式(IMyInterface1, IMyInterface2) 被呼叫");
    }
    // 建構函式 3
    public YourClass(IMyInterface1 myInterface1, IMyInterface3 myInterface3)
    {
        Console.WriteLine("YourClass 建構式(IMyInterface1, IMyInterface3) 被呼叫");
    }
    // 建構函式 4
    public YourClass(IMyInterface1 myInterface1, string myString)
    {
        Console.WriteLine("YourClass 建構式(IMyInterface1, string) 被呼叫");
    }
    // 建構函式 5
    public YourClass(IMyInterface1 myInterface1, string myString, int     myInt)
    {
        Console.WriteLine("YourClass 建構式5(IMyInterface1, string, int) 被呼叫");
    }
}
在這裡將會將會透過底下的程式碼,先透過 ServiceCollection 物件,使用 DI 容器 Container 來進行抽象型別與具體實作類別的註冊,這裡將會使用 AddTransient 方法來進行 暫時性 的執行個體存留期註冊 ( 每次從服務容器要求暫時性存留期服務時都會建立它們。 此存留期最適合用於輕量型的無狀態服務 ),接者使用 ServiceCollection.BuildServiceProvider() 方法取得 IServiceProvider ,如此,最後就可以透過 ServiceProvider.GetService() 方法來取得 IYourInterface 的實作物件。
C Sharp / C#
class Program
{
    static void Main(string[] args)
    {
        ServiceCollection services = new ServiceCollection();
        services.AddTransient<IMyInterface1, MyClass1>();
        services.AddTransient<IMyInterface2, MyClass2>();
        services.AddTransient<IMyInterface3, MyClass3>();
        services.AddTransient<IYourInterface, YourClass>();
        ServiceProvider serviceProvider = services.BuildServiceProvider();

        IYourInterface yourInterface = serviceProvider.GetService<IYourInterface>();

        Console.WriteLine("Press any key for continuing...");
        Console.ReadKey();
    }
}

第 1 次 .NET Core 相依性注入的測試

把上面的程式碼建立起來之後,將 YourClass 類別內的 建構函式4, 建構函式5 這兩個建構函式註解起來,接著將這個專案執行起來,不幸的是,該專案將無法正常執行起來,因為會拋出例外異常,從這個拋出例外異常訊息可以看出,因為該具體實作類別 YourClass 內,現在共有三個多載建構函式,原則上,當 .NET Core 相依性注入容器要注入一個物件的時候,會去搜尋該具體實作類別內的多載建構函式,並且將會選取建構函式參數數量最多的作為產生該物件之後的初始化方法。
可是, 建構函式 2 與 建構函式 3 同樣是需要 2 個參數,而且這兩個參數型別都是介面,這些介面也都有使用 ServiceCollection 註冊到 DI / IoC Container 容器 內,因此,DI 容器將無法決定要使用哪個 建構函式 來做物件的初始化,最後結果將會是無法完成注入這個物件過程,並且得到例外異常錯誤訊息。
System.InvalidOperationException: 'Unable to activate type 'DIConstructorParameters.YourClass'. The following constructors are ambiguous:
Void .ctor(DIConstructorParameters.IMyInterface1, DIConstructorParameters.IMyInterface2)

第 2 次 .NET Core 相依性注入的測試

現在請將 建構函式 3 註解起來 (這個時候,建構函式 4, 5 也被註解起來了),避免剛剛產生的錯誤訊息,再度執行一次,現在在螢幕上將會得到底下內容。因為此時該 YourClass 內僅有 建構函式 1 / 建構函式 2 ,所以,當要注入 YourClass 類別之後,將會執行 建構函式 2 來做該物件的初始化。
YourClass 建構式2(IMyInterface1, IMyInterface2) 被呼叫

第 3 次 .NET Core 相依性注入的測試

接下來來把 建構函式 5 註解起來,現在在 YourClass 類別內共有三個建構函式(1,2,4),其中建構函式參數數量最多的是 建構函式 2 / 建構函式 4 ,分別需要提供 兩個 參數,不過,對於 建構函式 2 所需要的兩個參數,都是介面型別,而 建構函式 4 則需要一個介面型別參數與一個字串常數。
現在可以執行這個專案,此時竟然沒有任何例外異常訊息拋出,而是在螢幕上輸出底下內容,從這個輸出文字可以看到 建構函式 2 被呼叫了,可是,建構函式 2 / 建構函式 4 都是有兩個參數,那為什麼不會產生錯誤,而是 建構函式 2 被呼叫了。這是因為 建構函式 4 的第二個參數為 字串 型別,這是屬於 .NET 開發框架下的基本型別,因此,這個型別將不會被列入選擇建構式的條件,所以, 建構函式 2 將會被選擇,而且這是唯一的 建構函式,不會有模稜兩可的問題。
YourClass 建構式2(IMyInterface1, IMyInterface2) 被呼叫

第 4 次 .NET Core 相依性注入的測試

現在可以把 建構函式 5 解除註解,現在在 YourClass 類別內共有四個建構函式(1,2,4,5),其中建構函式參數數量最多的是 建構函式 5 ,不過該建構函式的最後兩個參數的型別都是基本型別(string 與 int),所以,實際上僅有一個 參數。
因此,當執行這個專案,當然就會選擇建構函式 2。
YourClass 建構式2(IMyInterface1, IMyInterface2) 被呼叫