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) 被呼叫



2019年6月11日 星期二

.NET Core 的相依性注入的 服務定位器 Service Locator ,如何使用 ServiceProvider 教學

.NET Core 的相依性注入的 服務定位器 Service Locator ,如何使用 ServiceProvider 教學

在 .NET Core 開發框架下,微軟 Microsoft 有提供一個 Microsoft.Extensions.DependencyInjection NuGet 套件,這提供了一個 DI / IoC Container 相依性注入/相依反轉 容器類別庫,雖然這個 DI 容器並沒有提供如同其他 DI 容器的多樣性功能,但是對於要使用相依性注入的開發專案上,已經相當充分與足夠了。
對於相依性注入容器的主要功能,是要能夠提供 3R ,也就是 Register 註冊、解析 Resolve、釋放 Release ,這三大功能。
當建立一個 ASP.NET Core 專案,可以從 Startup.cs 類別中,看到 public void ConfigureServices(IServiceCollection services) 這個方法內,提供了 IServiceCollection 具體實作的物件,開發人員可以透過 IServiceCollection 物件來針對 DI Container 容器來進行抽象型別與具體實作型別(或者只針對具體實作類別) 來進行註冊 Registration。
這篇文章的範例程式碼可以從 GitHub 取得
現在,使用 Visual Studio 2019 建立一個 ASP.NET Coer Web API 類型的專案,接著,打開 Startup.cs 這個檔案,在這裡建立一個新的介面 IMessage 與這個介面的具體實作類別 ConsoleMessage ,如下面的程式碼。
C Sharp / C#
public interface IMessage
{
    string Write(string message);
}
public class ConsoleMessage : IMessage
{
    public string Write(string message)
    {
        string result = $"[Console 輸出] {message}";
        Console.WriteLine(result);
        return result;
    }
}
接下來將需要使用 MS.DI 這個套件,做到服務定位器的功能,也就是說希望能夠透過服務定位器這個物件,進行解析出 IMessage 這個介面所需要用到的具體實作物件。現在需要在 Startup.ConfigureServices 方法內,透過 IServiceCollection services 參數,來進行這個介面與具體實作類別的註冊,也就是要執行 services.AddTransient<IMessage, ConsoleMessage>(); 這個方法,這裡使用了底下的程式碼。
C Sharp / C#
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
    services.AddTransient<IMessage, ConsoleMessage>();
}
請在 Startup 類別內,找到 Startup.Configure 方法,public void Configure(IApplicationBuilder app, IHostingEnvironment env) ,透過注入的 IApplicationBuilder app 這個物件,裡面有個 ApplicationServices 屬性,這個屬性的型別為 IServiceProvider,在這裡請將這個屬性值指定到新設定的靜態變數 public static IServiceProvider serviceProvider; 內,使用這個敘述 serviceProvider = app.ApplicationServices;
C Sharp / C#
public static IServiceProvider serviceProvider;
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    serviceProvider = app.ApplicationServices;
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseMvc();
}
為了要能夠使用 DI 容器來進行要注入的型別解析需求,所以,需要取得 IServiceProvider 這個介面的實作物件;不過,要取得這個 IServiceProvider 物件,可以從不同的地方取得,那麼,從不同地方取得的 IServiceProvider 物件,表現上是有什麼不同呢?現在,可以打開 Controllers 資料夾上的 ValuesController.cs 這個檔案,填入底下的程式碼。
在這個 ValuesController 控制器中,將會從底下幾個地方,取得 IServiceProvider:
  • 建構式函式來注入 IServiceProvider serviceProvider
  • 在 Get 方法中,使用 FromServices 屬性,注入 IServiceProvider serviceProvider
  • 在上面的說明內容,有實作出 IApplicationBuilder.ApplicationServices 這個屬性值,並且存放到 Startup.serviceProvider 這個靜態屬性上
  • 最後,透過這個 Action 動作方法中,可以使用 HttpContext.RequestServices 取得 IServiceProvider 這個物件
現在,先來看看這個 Get 動作程式碼產生什麼樣的輸出結果;從這個輸出結果可以看出兩件事情。
  • 對於 IApplicationBuilder.ApplicationServices 得到的 ServiceProvider 物件 (34921712),與另外三個管道所取得的 ServiceProvider 的物件 (39157888) 是不同的。
  • 不管是哪個 ServiceProvider 物件,都可以使用 IServiceProvider.GetService() 這個敘述,來取得 IMessage 這個介面所註冊的對應具體實作類別的物件。因為此次註冊的介面與類別,使用的是 AddTransient ,所以,可以看到這裡將會得到不同的 IMessage 實作物件,分別是 : 27973187 , 50833863 , 5822459 , 60684095
[
"從 IApplicationBuilder.ApplicationServices 取得的 ServiceProvider 34921712",
"從 IApplicationBuilder.ApplicationServices 解析出 IMessage 27973187",
"建構式注入取得的 ServiceProvider 39157888",
"從 建構式注入取得的 ServiceProvider 解析出 IMessage 50833863",
"使用 FromServices 屬性取得的 ServiceProvider 39157888",
"使用 FromServices 屬性取得的 ServiceProvider 解析出 IMessage 5822459",
"從 RequestServices 取得的 ServiceProvider 39157888",
"從 RequestServices 取得的 ServiceProvider 解析出 IMessage 60684095"
]
C Sharp / C#
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    private readonly IServiceProvider serviceProvider;

    public ValuesController(IServiceProvider serviceProvider)
    {
        this.serviceProvider = serviceProvider;
    }
    // GET api/values
    [HttpGet]
    public ActionResult<IEnumerable<string>> Get([FromServices] IServiceProvider actionServiceProvider)
    {
        List<string> result = new List<string>();
        result.Add($"從 IApplicationBuilder.ApplicationServices 取得的 ServiceProvider {Startup.serviceProvider.GetHashCode().ToString()}");
        result.Add($"從 IApplicationBuilder.ApplicationServices 解析出 IMessage {Startup.serviceProvider.GetService<IMessage>().GetHashCode().ToString()}");
        result.Add($"建構式注入取得的 ServiceProvider {serviceProvider.GetHashCode().ToString()}");
        result.Add($"從 建構式注入取得的 ServiceProvider 解析出 IMessage {serviceProvider.GetService<IMessage>().GetHashCode().ToString()}");
        result.Add($"使用 FromServices 屬性取得的 ServiceProvider {actionServiceProvider.GetHashCode().ToString()}");
        result.Add($"使用 FromServices 屬性取得的 ServiceProvider 解析出 IMessage {actionServiceProvider.GetService<IMessage>().GetHashCode().ToString()}");
        result.Add($"從 RequestServices 取得的 ServiceProvider {HttpContext.RequestServices.GetHashCode().ToString()}");
        result.Add($"從 RequestServices 取得的 ServiceProvider 解析出 IMessage {HttpContext.RequestServices.GetService<IMessage>().GetHashCode().ToString()}");
        return result;
    }

    // GET api/values/5
    [HttpGet("{id}")]
    public ActionResult<string> Get(int id)
    {
        return "value";
    }

    // POST api/values
    [HttpPost]
    public void Post([FromBody] string value)
    {
    }

    // PUT api/values/5
    [HttpPut("{id}")]
    public void Put(int id, [FromBody] string value)
    {
    }

    // DELETE api/values/5
    [HttpDelete("{id}")]
    public void Delete(int id)
    {
    }
}
接下來,在 Startup.ConfigureServices 方法內,將 AddTransient 改成 AddSingleton 這個方法,也就是說,當要進行 IMessage 介面的具體實作物件注入的時候,使用 Singleton 的注入方式。
現在,先來看看這個 Get 動作程式碼產生什麼樣的輸出結果;從這個輸出結果可以看出兩件事情。
  • 對於 IApplicationBuilder.ApplicationServices 得到的 ServiceProvider 物件 (48948582),與另外三個管道所取得的 ServiceProvider 的物件 (28785966) 是不同的。
  • 不管是哪個 ServiceProvider 物件,使用 IServiceProvider.GetService() 這個敘述,來取得 IMessage 這個介面所註冊的對應具體實作類別的物件,都是同一個物件 (53046438)
[
"從 IApplicationBuilder.ApplicationServices 取得的 ServiceProvider 48948582",
"從 IApplicationBuilder.ApplicationServices 解析出 IMessage 53046438",
"建構式注入取得的 ServiceProvider 28785966",
"從 建構式注入取得的 ServiceProvider 解析出 IMessage 53046438",
"使用 FromServices 屬性取得的 ServiceProvider 28785966",
"使用 FromServices 屬性取得的 ServiceProvider 解析出 IMessage 53046438",
"從 RequestServices 取得的 ServiceProvider 28785966",
"從 RequestServices 取得的 ServiceProvider 解析出 IMessage 53046438"
]
最後,在 Startup.ConfigureServices 方法內,將 AddSingleton 改成 AddScoped 這個方法,也就是說,當要進行 IMessage 介面的具體實作物件注入的時候,若在同一個 Request 下,所注入的 IMessage 物件,就像是 Singleton 的用法,像是使用 Singleton 的注入方式。
現在,先來看看這個 Get 動作程式碼產生什麼樣的輸出結果;從這個輸出結果可以看出兩件事情。
現在,當執行到 result.Add($"從 IApplicationBuilder.ApplicationServices 解析出 IMessage {Startup.serviceProvider.GetService<IMessage>().GetHashCode().ToString()}"); 敘述之後,竟會得到底下的例外異常訊息:
System.InvalidOperationException: 'Cannot resolve scoped service 'CoreServiceLocator.IMessage' from root provider.'
所以,請把這個 Get 動作內的這兩行敘述先註解起來,然後重新執行一次。
C Sharp / C#
// result.Add($"從 IApplicationBuilder.ApplicationServices 取得的 ServiceProvider {Startup.serviceProvider.GetHashCode().ToString()}");
// result.Add($"從 IApplicationBuilder.ApplicationServices 解析出 IMessage {Startup.serviceProvider.GetService<IMessage>().GetHashCode().ToString()}");
  • 現在已經把 IApplicationBuilder.ApplicationServices 的執行程式碼移除了,而透過另外三個管道所取得的 ServiceProvider 的物件 (59274039) 還是都相同的。
  • 不管是哪個 ServiceProvider 物件,使用 IServiceProvider.GetService() 這個敘述,來取得 IMessage 這個介面所註冊的對應具體實作類別的物件,都是同一個物件 (1177678)
若在同一個 Request 下所做的 Scope 注入行為,就類似於 Singleton 的註冊行為。
[
"建構式注入取得的 ServiceProvider 59274039",
"從 建構式注入取得的 ServiceProvider 解析出 IMessage 1177678",
"使用 FromServices 屬性取得的 ServiceProvider 59274039",
"使用 FromServices 屬性取得的 ServiceProvider 解析出 IMessage 1177678",
"從 RequestServices 取得的 ServiceProvider 59274039",
"從 RequestServices 取得的 ServiceProvider 解析出 IMessage 1177678"
]