了解 .NET 中的依赖关系注入基础知识
在本文中,你将创建一个 .NET 控制台应用,该应用手动创建 ServiceCollection 和相应的 ServiceProvider。 了解如何使用依赖关系注入 (DI) 注册服务并解析它们。 本文使用 Microsoft.Extensions.DependencyInjection NuGet 包来演示 .NET 中 DI 的基础知识。
注意
本文不利用泛型主机功能。 有关更全面的指南,请参阅在 .NET 中使用依赖关系注入。
开始使用
若要开始,请创建新的名为 DI.Basics 的 .NET 控制台应用程序。 下面的列表中引用了创建控制台项目的一些最常见方法:
- Visual Studio:“文件”>“新建”>“项目”菜单。
- Visual Studio Code 和 C# 开发工具包扩展:“解决方案资源管理器”菜单选项。
- .NET CLI:终端中的
dotnet new console
命令。
需要在项目文件中将包引用添加到 Microsoft.Extensions.DependencyInjection。 无论采用哪种方法,请确保项目类似于 DI.Basics.csproj 文件的以下 XML:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
</ItemGroup>
</Project>
依赖关系注入基本信息
依赖关系注入是一种设计模式,可用于删除硬编码的依赖关系,并使应用程序更易于维护和测试。 DI 是一种在类与其依赖关系之间实现控制反转 (IoC) 的技术。
.NET 中 DI 的抽象在 Microsoft.Extensions.DependencyInjection.Abstractions NuGet 包中定义:
- IServiceCollection:为服务描述符集合定义协定。
- IServiceProvider:定义用于检索服务对象的机制。
- ServiceDescriptor:描述一种服务,其中包括该服务的类型、实现和生存期。
在 .NET 中,可通过添加服务并在 IServiceCollection
中配置这些服务来管理 DI。 注册服务后,通过调用 BuildServiceProvider 方法生成 IServiceProvider
实例。 IServiceProvider
充当所有已注册服务的容器,并用于解析服务。
创建 example 服务
并非所有服务都以同等方式创建。 一些服务在每次服务容器获取它们 (transient) 时都需要一个新实例,而其他服务应跨请求 (scoped) 或在应用的整个生存期 (singleton) 内共享。 有关服务生存期的详细信息,请参阅服务生存期。
同样,某些服务仅公开具体类型,而另一些服务则表示为接口和实现类型之间的协定。 创建多个服务变体来帮助演示这些概念。
创建名为 IConsole.cs 的新 C# 文件,并添加以下代码:
public interface IConsole
{
void WriteLine(string message);
}
此文件定义一个 IConsole
接口,该接口公开单个方法 WriteLine
。 接下来,创建名为 DefaultConsole.cs 的新 C# 文件,并添加以下代码:
internal sealed class DefaultConsole : IConsole
{
public bool IsEnabled { get; set; } = true;
void IConsole.WriteLine(string message)
{
if (IsEnabled is false)
{
return;
}
Console.WriteLine(message);
}
}
前面的代码表示 IConsole
接口的默认实现。 WriteLine
方法根据 IsEnabled
属性有条件地写入控制台。
提示
实现的命名是开发团队应同意的选择。 Default
前缀是一种常见约定,用于指示接口的默认实现,但不是必需的。
接下来,创建 IGreetingService.cs 文件并添加以下 C# 代码:
public interface IGreetingService
{
string Greet(string name);
}
然后,添加名为 DefaultGreetingService.cs 的新 C# 文件,并添加以下代码:
internal sealed class DefaultGreetingService(
IConsole console) : IGreetingService
{
public string Greet(string name)
{
var greeting = $"Hello, {name}!";
console.WriteLine(greeting);
return greeting;
}
}
前面的代码表示 IGreetingService
接口的默认实现。 服务实现需要 IConsole
作为主构造函数参数。 Greet
方法:
- 创建
greeting
,并提供name
。 - 对
IConsole
实例调用WriteLine
方法。 - 将
greeting
返回给调用方。
要创建的最后一项服务是 FarewellService.cs 文件,请在继续操作之前添加以下 C# 代码:
public class FarewellService(IConsole console)
{
public string SayGoodbye(string name)
{
var farewell = $"Goodbye, {name}!";
console.WriteLine(farewell);
return farewell;
}
}
FarewellService
表示具体类型,而不是接口。 它应声明为 public
,以便使用者能够访问它。 与其他声明为 internal
和 sealed
的服务实现类型不同,此代码演示了并非所有服务都需要是接口。 它还表明,服务实现可以是 sealed
来防止继承,可以是 internal
来限制对程序集的访问。
更新 Program
类
打开 Program.cs 文件,将现有代码替换为以下 C# 代码:
using Microsoft.Extensions.DependencyInjection;
// 1. Create the service collection.
var services = new ServiceCollection();
// 2. Register (add and configure) the services.
services.AddSingleton<IConsole>(
implementationFactory: static _ => new DefaultConsole
{
IsEnabled = true
});
services.AddSingleton<IGreetingService, DefaultGreetingService>();
services.AddSingleton<FarewellService>();
// 3. Build the service provider from the service collection.
var serviceProvider = services.BuildServiceProvider();
// 4. Resolve the services that you need.
var greetingService = serviceProvider.GetRequiredService<IGreetingService>();
var farewellService = serviceProvider.GetRequiredService<FarewellService>();
// 5. Use the services
var greeting = greetingService.Greet("David");
var farewell = farewellService.SayGoodbye("David");
前面的更新代码演示了如何:
- 创建新的
ServiceCollection
实例。 - 注册和配置
ServiceCollection
中的服务:IConsole
使用实现工厂重载,返回IsEnabled
设置为“true”的DefaultConsole
类型。IGreetingService
将添加DefaultGreetingService
类型的相应实现类型。FarewellService
添加为具体类型。
- 从
ServiceCollection
生成ServiceProvider
。 - 解析
IGreetingService
和FarewellService
服务。 - 使用解析的服务向名为
David
的人员致以问候并告别。
如果将 DefaultConsole
的 IsEnabled
属性更新为 false
,则 Greet
和 SayGoodbye
方法会忽略将生成的消息写入到控制台。 此类更改有助于证明 IConsole
服务注入到 IGreetingService
中,FarewellService
服务作为影响该应用行为的依赖关系。
所有这些服务都注册为单一实例,尽管对于此示例,如果将其注册为transient 或 scoped 服务,则其工作方式相同。
重要
在这个人为设计的 example 中,服务生存期并不重要,但在实际应用程序中,应仔细考虑每个服务的生存期。
运行示例应用
若要运行示例应用,请在 Visual Studio、Visual Studio Code 中按 F5 或在终端中运行 dotnet run
命令。 应用完成时,会显示以下输出:
Hello, David!
Goodbye, David!
服务描述符
用于将服务添加到 ServiceCollection
的最常用的 API 是生存期命名的泛型扩展方法,例如:
AddSingleton<TService>
AddTransient<TService>
AddScoped<TService>
这些方法是创建 ServiceDescriptor 实例并将其添加到 ServiceCollection
的便利方法。 ServiceDescriptor
是一张简单的类,它描述一种服务及其服务类型、实现类型和生存期。 它还可以描述实现工厂和实例。
对于在 ServiceCollection
中注册的每个服务,可以直接使用 ServiceDescriptor
实例调用 Add
方法。 请开考虑以下示例:
services.Add(ServiceDescriptor.Describe(
serviceType: typeof(IConsole),
implementationFactory: static _ => new DefaultConsole
{
IsEnabled = true
},
lifetime: ServiceLifetime.Singleton));
上述代码等效于在 ServiceCollection
中注册 IConsole
服务的方式。 Add
方法用于添加描述 IConsole
服务的 ServiceDescriptor
实例。 静态方法 ServiceDescriptor.Describe
委派给各种 ServiceDescriptor
构造函数。 请考虑 IGreetingService
服务的等效代码:
services.Add(ServiceDescriptor.Describe(
serviceType: typeof(IGreetingService),
implementationType: typeof(DefaultGreetingService),
lifetime: ServiceLifetime.Singleton));
上述代码描述 IGreetingService
服务及其服务类型、实现类型和生存期。 最后,请考虑 FarewellService
服务的等效代码:
services.Add(ServiceDescriptor.Describe(
serviceType: typeof(FarewellService),
implementationType: typeof(FarewellService),
lifetime: ServiceLifetime.Singleton));
前面的代码将具体的 FarewellService
类型描述为服务和实现类型。 该服务被注册为 singleton 服务。