了解 .NET 中的依赖关系注入基础知识

在本文中,你将创建一个 .NET 控制台应用,该应用手动创建 ServiceCollection 和相应的 ServiceProvider。 了解如何使用依赖关系注入 (DI) 注册服务并解析它们。 本文使用 Microsoft.Extensions.DependencyInjection NuGet 包来演示 .NET 中 DI 的基础知识。

注意

本文不利用泛型主机功能。 有关更全面的指南,请参阅在 .NET 中使用依赖关系注入

开始使用

若要开始,请创建新的名为 DI.Basics 的 .NET 控制台应用程序。 下面的列表中引用了创建控制台项目的一些最常见方法:

需要在项目文件中将包引用添加到 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 包中定义:

在 .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,以便使用者能够访问它。 与其他声明为 internalsealed 的服务实现类型不同,此代码演示了并非所有服务都需要是接口。 它还表明,服务实现可以是 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
  • 解析 IGreetingServiceFarewellService 服务。
  • 使用解析的服务向名为 David 的人员致以问候并告别。

如果将 DefaultConsoleIsEnabled 属性更新为 false,则 GreetSayGoodbye 方法会忽略将生成的消息写入到控制台。 此类更改有助于证明 IConsole 服务注入IGreetingService 中,FarewellService 服务作为影响该应用行为的依赖关系

所有这些服务都注册为单一实例,尽管对于此示例,如果将其注册为transientscoped 服务,则其工作方式相同。

重要

在这个人为设计的 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 服务。

另请参阅