教學課程:在 .NET 中使用相依性插入

本教學課程會示範如何在 .NET 中使用相依性插入 (DI)。 使用 Microsoft 擴充功能時,DI 會藉由新增服務並在 IServiceCollection 中設定它們來管理。 IHost 介面會公開 IServiceProvider 執行個體,此執行個體會作為所有已註冊服務的容器。

在本教學課程中,您會了解如何:

  • 建立使用相依性插入的 .NET 主控台應用程式
  • 建置和設定一般主機
  • 撰寫數個介面和對應的實作
  • 使用服務存留期和 DI 的範圍

必要條件

  • .NET Core 3.1 SDK 或更新版本。
  • 熟悉建立新的 .NET 應用程式和安裝 NuGet 套件。

建立新的主控台應用程式

使用 dotnet new 命令或 IDE 新專案精靈,建立名為 ConsoleDI.Example 的新 .NET 主控台應用程式。 將 Microsoft.Extensions.Hosting NuGet 套件新增至專案。

新的主控台應用程式專案檔應該如下所示:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>true</ImplicitUsings>
    <RootNamespace>ConsoleDI.Example</RootNamespace>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
  </ItemGroup>

</Project>

重要

在此範例中,需要 Microsoft.Extensions.Hosting NuGet 套件才能建置和執行應用程式。 某些中繼套件可能包含 Microsoft.Extensions.Hosting 套件,在此情況下不需要明確套件參考。

新增介面

在此樣本應用程式中,您將了解相依性插入如何處理服務存留期。 您將建立代表不同服務存留期的數個介面。 將下列介面新增至專案根目錄:

IReportServiceLifetime.cs

using Microsoft.Extensions.DependencyInjection;

namespace ConsoleDI.Example;

public interface IReportServiceLifetime
{
    Guid Id { get; }

    ServiceLifetime Lifetime { get; }
}

IReportServiceLifetime 介面會定義︰

  • 表示服務唯一識別碼的 Guid Id 屬性。
  • 表示服務存留期的 ServiceLifetime 屬性。

IExampleTransientService.cs

using Microsoft.Extensions.DependencyInjection;

namespace ConsoleDI.Example;

public interface IExampleTransientService : IReportServiceLifetime
{
    ServiceLifetime IReportServiceLifetime.Lifetime => ServiceLifetime.Transient;
}

IExampleScopedService.cs

using Microsoft.Extensions.DependencyInjection;

namespace ConsoleDI.Example;

public interface IExampleScopedService : IReportServiceLifetime
{
    ServiceLifetime IReportServiceLifetime.Lifetime => ServiceLifetime.Scoped;
}

IExampleSingletonService.cs

using Microsoft.Extensions.DependencyInjection;

namespace ConsoleDI.Example;

public interface IExampleSingletonService : IReportServiceLifetime
{
    ServiceLifetime IReportServiceLifetime.Lifetime => ServiceLifetime.Singleton;
}

所有 IReportServiceLifetime 的子介面都會以預設值明確實作 IReportServiceLifetime.Lifetime。 例如,IExampleTransientService 會使用 ServiceLifetime.Transient 值明確實作 IReportServiceLifetime.Lifetime

新增預設實作

此範例實作會使用 Guid.NewGuid() 的結果來初始化其 Id 屬性。 將各種服務的預設實作類別新增至專案根目錄:

ExampleTransientService.cs

namespace ConsoleDI.Example;

internal sealed class ExampleTransientService : IExampleTransientService
{
    Guid IReportServiceLifetime.Id { get; } = Guid.NewGuid();
}

ExampleScopedService.cs

namespace ConsoleDI.Example;

internal sealed class ExampleScopedService : IExampleScopedService
{
    Guid IReportServiceLifetime.Id { get; } = Guid.NewGuid();
}

ExampleSingletonService.cs

namespace ConsoleDI.Example;

internal sealed class ExampleSingletonService : IExampleSingletonService
{
    Guid IReportServiceLifetime.Id { get; } = Guid.NewGuid();
}

每個實作都會定義為 internal sealed,並實作其對應的介面。 例如,ExampleSingletonService 會實作 IExampleSingletonService

新增需要 DI 的服務

將下列服務存留期報告程式類別新增為主控台應用程式的服務:

ServiceLifetimeReporter.cs

namespace ConsoleDI.Example;

internal sealed class ServiceLifetimeReporter(
    IExampleTransientService transientService,
    IExampleScopedService scopedService,
    IExampleSingletonService singletonService)
{
    public void ReportServiceLifetimeDetails(string lifetimeDetails)
    {
        Console.WriteLine(lifetimeDetails);

        LogService(transientService, "Always different");
        LogService(scopedService, "Changes only with lifetime");
        LogService(singletonService, "Always the same");
    }

    private static void LogService<T>(T service, string message)
        where T : IReportServiceLifetime =>
        Console.WriteLine(
            $"    {typeof(T).Name}: {service.Id} ({message})");
}

ServiceLifetimeReporter 會定義需要上述每個服務介面的建構函式,也就是 IExampleTransientServiceIExampleScopedServiceIExampleSingletonService。 物件會公開單一方法,可讓取用者使用指定的 lifetimeDetails 參數報告服務。 叫用時,ReportServiceLifetimeDetails 方法會記錄每個服務的唯一識別碼與服務存留期訊息。 記錄訊息有助於視覺化服務存留期。

註冊 DI 的服務

使用下列程式碼更新 Program.cs

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ConsoleDI.Example;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddTransient<IExampleTransientService, ExampleTransientService>();
builder.Services.AddScoped<IExampleScopedService, ExampleScopedService>();
builder.Services.AddSingleton<IExampleSingletonService, ExampleSingletonService>();
builder.Services.AddTransient<ServiceLifetimeReporter>();

using IHost host = builder.Build();

ExemplifyServiceLifetime(host.Services, "Lifetime 1");
ExemplifyServiceLifetime(host.Services, "Lifetime 2");

await host.RunAsync();

static void ExemplifyServiceLifetime(IServiceProvider hostProvider, string lifetime)
{
    using IServiceScope serviceScope = hostProvider.CreateScope();
    IServiceProvider provider = serviceScope.ServiceProvider;
    ServiceLifetimeReporter logger = provider.GetRequiredService<ServiceLifetimeReporter>();
    logger.ReportServiceLifetimeDetails(
        $"{lifetime}: Call 1 to provider.GetRequiredService<ServiceLifetimeReporter>()");

    Console.WriteLine("...");

    logger = provider.GetRequiredService<ServiceLifetimeReporter>();
    logger.ReportServiceLifetimeDetails(
        $"{lifetime}: Call 2 to provider.GetRequiredService<ServiceLifetimeReporter>()");

    Console.WriteLine();
}

每個 services.Add{LIFETIME}<{SERVICE}> 擴充方法都會新增 (並且可能會設定) 服務。 我們建議應用程式遵循此慣例。 除非您正在撰寫官方 Microsoft 套件,否則請勿將擴充方法放在 Microsoft.Extensions.DependencyInjection 命名空間中。 在 Microsoft.Extensions.DependencyInjection 命名空間內定義的擴充方法:

  • 會顯示在 IntelliSense 中 ,而不需要額外的 using 區塊。
  • 減少 ProgramStartup 類別中必要的 using 陳述式數目,其中通常會呼叫這些擴充方法。

應用程式:

  • 建立具有IHostBuilder主機產生器設定 執行個體。
  • 設定服務,並以對應的服務存留期進行新增。
  • 呼叫 Build() 並指派 IHost 的執行個體。
  • 呼叫 ExemplifyScoping ,傳入 IHost.Services

推論

在此範例應用程式中,您已建立數個介面和對應的實作。 每個服務都是唯一識別並與 ServiceLifetime 配對。 樣本應用程式會示範如何針對介面註冊服務實作,以及如何不使用支援介面註冊純類別。 然後樣本應用程式會示範如何在執行時間解析定義為建構函式參數的相依性。

執行應用程式時會顯示類似下列的輸出:

// Sample output:
// Lifetime 1: Call 1 to provider.GetRequiredService<ServiceLifetimeReporter>()
//     IExampleTransientService: d08a27fa-87d2-4a06-98d7-2773af886125 (Always different)
//     IExampleScopedService: 402c83c9-b4ed-4be1-b78c-86be1b1d908d (Changes only with lifetime)
//     IExampleSingletonService: a61f1ff4-0b14-4508-bd41-21d852484a7b (Always the same)
// ...
// Lifetime 1: Call 2 to provider.GetRequiredService<ServiceLifetimeReporter>()
//     IExampleTransientService: b43d68fb-2c7b-4a9b-8f02-fc507c164326 (Always different)
//     IExampleScopedService: 402c83c9-b4ed-4be1-b78c-86be1b1d908d (Changes only with lifetime)
//     IExampleSingletonService: a61f1ff4-0b14-4508-bd41-21d852484a7b (Always the same)
// 
// Lifetime 2: Call 1 to provider.GetRequiredService<ServiceLifetimeReporter>()
//     IExampleTransientService: f3856b59-ab3f-4bbd-876f-7bab0013d392 (Always different)
//     IExampleScopedService: bba80089-1157-4041-936d-e96d81dd9d1c (Changes only with lifetime)
//     IExampleSingletonService: a61f1ff4-0b14-4508-bd41-21d852484a7b (Always the same)
// ...
// Lifetime 2: Call 2 to provider.GetRequiredService<ServiceLifetimeReporter>()
//     IExampleTransientService: a8015c6a-08cd-4799-9ec3-2f2af9cbbfd2 (Always different)
//     IExampleScopedService: bba80089-1157-4041-936d-e96d81dd9d1c (Changes only with lifetime)
//     IExampleSingletonService: a61f1ff4-0b14-4508-bd41-21d852484a7b (Always the same)

從應用程式輸出可以看到:

  • Transient 服務一律不同,每次擷取該服務時都會建立新的執行個體。
  • Scoped 服務只會隨著新的範圍而變更,但在範圍內是相同的執行個體。
  • Singleton 服務一律相同,只會建立一次新的執行個體。

另請參閱