共用方式為


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

本教學課程示範如何在 .NET中使用 相依性插入 (DI)。 使用 Microsoft Extensions,DI 會藉由新增服務並在 IServiceCollection中設定它們來管理。 IHost 介面會公開 IServiceProvider 實例,做為所有已註冊服務的容器。

在本教學課程中,您將瞭解如何:

  • 建立使用相依性插入的 .NET 控制台應用程式
  • 建置和設定 泛型主機
  • 撰寫數個介面和對應的實作
  • 使用服務生命周期和 DI 的範圍設定

先決條件

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

建立新的主控台應用程式

使用 dotnet new 命令或 IDE 新專案精靈,建立名為 ConsoleDI 的新 .NET 控制台應用程式。Example。 將 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.Configuration.Binder" Version="9.0.3" />
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.3" />
  </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 明確地以 IReportServiceLifetime.Lifetime 值實作 ServiceLifetime.Transient

新增預設實作

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

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,並實作其對應的介面。 它們不需要 internalsealed,不過,通常將實作視為 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 指示詞。
  • 減少在usingProgram 類別中通常呼叫這些擴充方法時所需的 Startup 指示詞數目。

應用程式:

  • 使用 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 服務一律相同,新實例只會建立一次。

另請參閱