本教程演示如何在 .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.Configuration.Binder" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.10" />
</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 并实现其相应的接口。 它们并非必须是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指令。 - 减少
Program或Startup类中通常调用这些扩展方法所需的using指令数。
应用:
- IHostApplicationBuilder使用主机生成器设置创建实例。
- 配置服务,将它们与对应的服务生命周期一起添加。
- 调用 Build() 并分配一个 IHost 实例。
- 调用
ExemplifyServiceLifetime,传入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 服务始终相同,仅创建新实例一次。