了解 .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 充当所有已注册服务的容器,并用于解析服务。

创建示例服务

并非所有服务都以同等方式创建。 一些服务在每次服务容器获取它们(暂时性)时都需要一个新实例,而其他服务应跨请求(作用域)或在应用的整个生存期(单一实例)内共享。 有关服务生存期的详细信息,请参阅服务生存期

同样,某些服务仅公开具体类型,而另一些服务则表示为接口和实现类型之间的协定。 创建多个服务变体来帮助演示这些概念。

创建名为 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 服务作为影响该应用行为的依赖关系

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

重要

在这个人为设计的示例中,服务生存期并不重要,但在实际应用程序中,应仔细考虑每个服务的生存期。

运行示例应用

若要运行示例应用,请在 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 类型描述为服务和实现类型。 该服务被注册为单一实例服务。

另请参阅