2024年1月11日 星期四

使用相同的 Interface,透過 .NET C# 程式碼,註冊多個服務,並且可以取得不同條件的實作服務物件

使用相同的 Interface,透過 .NET C# 程式碼,註冊多個服務,並且可以取得不同條件的實作服務物件

這篇文章主要是要探討,如何使用相同的 Interface,透過 .NET C# 程式碼,註冊多個服務,並且可以取得不同條件的實作服務物件。若要完成這樣的需求,究竟需要使用甚麼方式來進行設計與開發呢?這篇文章將會透過 .NET C# 程式碼,來說明如何完成這樣的需求。

由於 .NET8 平台有提供 .NET 泛型主機 的功能,在 .NET 泛型主機 服務內,將會提供 DI 相依性注入容器功能,因此,便可以直接使用 .NET 8 內建的 DI 容器來時做出這篇文章的要探討目的。

建立測試專案

  • 打開 Visual Studio 2022 IDE 應用程式
  • 從 [Visual Studio 2022] 對話窗中,點選右下方的 [建立新的專案] 按鈕
  • 在 [建立新專案] 對話窗右半部
    • 切換 [所有語言 (L)] 下拉選單控制項為 [C#]
    • 切換 [所有專案類型 (T)] 下拉選單控制項為 [主控台]
  • 在中間的專案範本清單中,找到並且點選 [主控台應用程式] 專案範本選項

    專案,用於建立可在 Windows、Linux 及 macOS 於 .NET 執行的命令列應用程式

  • 點選右下角的 [下一步] 按鈕
  • 在 [設定新的專案] 對話窗
  • 找到 [專案名稱] 欄位,輸入 csDISameInterface 作為專案名稱
  • 在剛剛輸入的 [專案名稱] 欄位下方,確認沒有勾選 [將解決方案與專案至於相同目錄中] 這個檢查盒控制項
  • 點選右下角的 [下一步] 按鈕
  • 現在將會看到 [其他資訊] 對話窗
  • 在 [架構] 欄位中,請選擇最新的開發框架,這裡選擇的 [架構] 是 : .NET 8.0 (標準字詞支援)
  • 在這個練習中,需要去勾選 [不要使用最上層陳述式(T)] 這個檢查盒控制項

    這裡的這個操作,可以由讀者自行決定是否要勾選這個檢查盒控制項

  • 請點選右下角的 [建立] 按鈕

稍微等候一下,這個主控台專案將會建立完成

安裝要用到的 NuGet 開發套件

因為開發此專案時會用到這些 NuGet 套件,請依照底下說明,將需要用到的 NuGet 套件安裝起來。

安裝 Microsoft.Extensions.Hosting 套件

關於 Microsoft.Extensions.Hosting 套件,可以參考 https://www.nuget.org/packages/Microsoft.Extensions.Hosting 這個網址。

  • 滑鼠右擊 [方案總管] 視窗內的 [專案節點] 下方的 [相依性] 節點
  • 從彈出功能表清單中,點選 [管理 NuGet 套件] 這個功能選項清單
  • 此時,將會看到 [NuGet: csDISameInterface] 視窗
  • 切換此視窗的標籤頁次到名稱為 [瀏覽] 這個標籤頁次
  • 在左上方找到一個搜尋文字輸入盒,在此輸入 Microsoft.Extensions.Hosting
  • 稍待一會,將會在下方看到這個套件被搜尋出來
  • 點選 [Microsoft.Extensions.Hosting] 套件名稱
  • 在視窗右方,將會看到該套件詳細說明的內容,其中,右上方有的 [安裝] 按鈕
  • 點選這個 [安裝] 按鈕,將這個套件安裝到專案內

開始進行程式設計

  • 在此專案節點下,找到並且打開 [Program.cs] 這個檔案
  • 使用底下 C# 程式碼替換掉 [Program.cs] 檔案內所有程式碼內容
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace csDISameInterface;

public interface IService
{
    void Get();
}
public class ServiceA : IService
{
    public void Get()
    {
        Console.WriteLine("ServiceA");
    }
}

public class ServiceB : IService
{
    public void Get()
    {
        Console.WriteLine("ServiceB");
    }
}

public class ServiceC : IService
{
    public void Get()
    {
        Console.WriteLine("ServiceC");
    }
}
internal class Program
{
    delegate IService ServiceResolver(string key);
    static void Main(string[] args)
    {
        IHost host = Host.CreateDefaultBuilder()
            .ConfigureServices((context, services) =>
            {
                services.AddTransient<ServiceA>();
                services.AddTransient<ServiceB>();
                services.AddTransient<ServiceC>();
                services.AddTransient<Func<string, IService>>(serviceProvider => key =>
                {
                    return key switch
                    {
                        "A" => serviceProvider.GetService<ServiceA>(),
                        "B" => serviceProvider.GetService<ServiceB>(),
                        "C" => serviceProvider.GetService<ServiceC>(),
                        _ => throw new KeyNotFoundException()
                    };
                });

                services.AddTransient<ServiceResolver>(serviceProvider => key =>
                {
                    switch (key)
                    {
                        case "A":
                            return serviceProvider.GetService<ServiceA>();
                        case "B":
                            return serviceProvider.GetService<ServiceB>();
                        case "C":
                            return serviceProvider.GetService<ServiceC>();
                        default:
                            throw new KeyNotFoundException();
                    }
                });
            }).Build();

        var serviceAccessor1 = host.Services.GetRequiredService<Func<string, IService>>();
        var serviceAccessor2 = host.Services.GetRequiredService<ServiceResolver>();
        IService service = serviceAccessor1("A");
        service.Get();
        service = serviceAccessor2("C");
        service.Get();
    }
}

在這裡首先會設計一個介面 [IService] ,該介面內將會宣告一個 Get 方法,並且設計三個實作類別 [ServiceA]、[ServiceB]、[ServiceC],這三個實作類別都會實作 [IService] 這個介面。

在實作的類別內,僅僅使用 Console.WriteLine 方法來輸出訊息,以便於在執行時,可以看到是哪個實作類別被呼叫。

接著,透過 .NET 8 內建的 DI 容器,來註冊這三個實作類別,並且註冊一個 Func 委派,這個委派可以透過傳入不同的參數,來取得不同的實作類別物件。

在程式進入點的第一行,將會使用 [Host.CreateDefaultBuilder()] 方法來建立一個泛型主機物件,並且在這個泛型主機物件內,透過 [ConfigureServices()] 方法來註冊服務到 DI 容器內,這裡會註冊三個具體實作類別,並且註冊一個 Func 委派,這個委派可以透過傳入不同的參數,來取得不同的實作類別物件。

註冊這個 Func 委派的程式碼如下:

services.AddTransient<Func<string, IService>>(serviceProvider => key =>
{
    return key switch
    {
        "A" => serviceProvider.GetService<ServiceA>(),
        "B" => serviceProvider.GetService<ServiceB>(),
        "C" => serviceProvider.GetService<ServiceC>(),
        _ => throw new KeyNotFoundException()
    };
});

在這裡註冊一個委派方法,一旦透過 DI 容器完成註冊之後,就可以透過相依性注入的方式來注入 Func<string, IService> 物件,該委派方法將會傳入一個字串,此時,將會依據傳入的字串,來決定要取得哪一個實作類別物件。

當在傳入字串之後,將會透過 switch case 來判斷傳入的字串,並且依據傳入的字串,來決定要取得哪一個實作類別物件,最後,將會透過 serviceProvider.GetService<ServiceA>() 這個方法來取得實作類別物件,並且將該物件回傳給呼叫端。

對於 AddTransient 方法,將會使用底下函式簽章

public static IServiceCollection AddTransient<TService>(
    this IServiceCollection services,
    Func<IServiceProvider, TService> implementationFactory)
    where TService : class;

在這裡 AddTransient 方法,將會傳入一個 Func 委派,這個委派將會傳入一個 IServiceProvider 物件,並且回傳一個 TService 物件,這裡的 TService 物件,將會是 IService 介面的實作類別物件。因此,這裡的委派方法,將會得到一個 IServiceProvider 物件,並且透過 IServiceProvider 物件來取得 IService 介面的實作類別物件,最後,將該物件回傳給呼叫端。

想要取得這個委派物件,可以透過底下程式碼來取得

var serviceAccessor1 = host.Services.GetRequiredService<Func<string, IService>>();

一旦取得了 serviceAccessor1 物件之後,便可以透過傳入不同的字串,來取得不同的實作類別物件,例如,透過傳入 "A" 這個字串,便可以取得 ServiceA 這個實作類別物件,透過傳入 "B" 這個字串,便可以取得 ServiceB 這個實作類別物件,透過傳入 "C" 這個字串,便可以取得 ServiceC 這個實作類別物件。

在這裡又取得了 [IService] 型別的 [service] 物件之後,便可以呼叫這個 [service] 物件的 Get 方法,來輸出訊息。

IService service = serviceAccessor1("A"); 

service.Get(); 








沒有留言:

張貼留言