教學課程:在 .NET 中使用相依性插入
本教學課程示範如何在 .NET中使用
在本教學課程中,您將瞭解如何:
- 建立使用相依性插入的 .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
,並實作其對應的介面。 它們不需要 internal
或 sealed
,不過,通常將實作視為 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
會定義一個建構函式,要求上述每個服務介面,也就是 IExampleTransientService
、IExampleScopedService
和 IExampleSingletonService
。 物件會公開單一方法,讓取用者使用指定的 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
指示詞。 - 減少在
using
或Program
類別中通常呼叫這些擴充方法時所需的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 服務一律相同,新實例只會建立一次。