教程:在 .NET 中使用依赖注入

本教程介绍如何在 .NET 中使用依赖注入 (DI)。 使用 Microsoft 扩展时,DI 是“一等公民”,其中服务是在 IServiceCollection 中添加和配置的。 IHost 接口会公开 IServiceProvider 实例,它充当所有已注册的服务的容器。

本教程介绍如何执行下列操作:

  • 创建一个使用依赖注入的 .NET 控制台应用
  • 生成和配置通用主机
  • 编写多个接口及相应的实现
  • 为 DI 使用服务生存期和范围设定

先决条件

  • .NET Core 3.1 SDK 或更高版本。
  • 熟悉如何创建新的 .NET 应用程序以及如何安装 NuGet 包。

创建新的控制台应用程序

通过 dotnet new 命令或 IDE 的“新建项目”向导,新建一个名为 ConsoleDI 的 .NET 控制台应用程序 Example 。 将 NuGet 包 Microsoft.Extensions.Hosting 添加到项目。

添加接口

将以下接口添加到项目根目录:

IOperation.cs

namespace ConsoleDI.Example;

public interface IOperation
{
    string OperationId { get; }
}

IOperation 接口会定义一个 OperationId 属性。

IOperation.cs Transient

namespace ConsoleDI.Example;

public interface ITransientOperation : IOperation
{
}

IOperation.cs Scoped

namespace ConsoleDI.Example;

public interface IScopedOperation : IOperation
{
}

IOperation.cs Singleton

namespace ConsoleDI.Example;

public interface ISingletonOperation : IOperation
{
}

IOperation 的所有子接口会会命名其预期服务生存期。 例如,“Transient”或“Singleton”。

添加默认实现

添加以下默认实现来进行各种操作:

DefaultOperation.cs

using static System.Guid;

namespace ConsoleDI.Example;

public record class DefaultOperation :
    ITransientOperation,
    IScopedOperation,
    ISingletonOperation
{
    public string OperationId { get; } = NewGuid().ToString()[^4..];
}

DefaultOperation 会实现所有已命名的标记接口,并将 OperationId 属性初始化为新的全局唯一标识符 (GUID) 的最后 4 个字符。

添加需要 DI 的服务

添加以下操作记录器对象,它作为服务添加到控制台应用:

OperationLogger.cs

namespace ConsoleDI.Example;

public class OperationLogger
{
    private readonly ITransientOperation _transientOperation;
    private readonly IScopedOperation _scopedOperation;
    private readonly ISingletonOperation _singletonOperation;

    public OperationLogger(
        ITransientOperation transientOperation,
        IScopedOperation scopedOperation,
        ISingletonOperation singletonOperation) =>
        (_transientOperation, _scopedOperation, _singletonOperation) =
            (transientOperation, scopedOperation, singletonOperation);

    public void LogOperations(string scope)
    {
        LogOperation(_transientOperation, scope, "Always different");
        LogOperation(_scopedOperation, scope, "Changes only with scope");
        LogOperation(_singletonOperation, scope, "Always the same");
    }


    private static void LogOperation<T>(T operation, string scope, string message)
        where T : IOperation =>
        Console.WriteLine(
            $"{scope}: {typeof(T).Name,-19} [ {operation.OperationId}...{message,-23} ]");
}

OperationLogger 会定义一个构造函数,该函数需要上述每一个标记接口(即 ITransientOperationIScopedOperationISingletonOperation)。 对象会公开一个方法,使用者可通过该方法使用给定的 scope 参数记录操作。 被调用时,LogOperations 方法会使用范围字符串和消息记录每个操作的唯一标识符。

为 DI 注册服务

使用以下代码更新 Program.cs:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ConsoleDI.Example;

using IHost host = Host.CreateDefaultBuilder(args)
    .ConfigureServices((_, services) =>
        services.AddTransient<ITransientOperation, DefaultOperation>()
            .AddScoped<IScopedOperation, DefaultOperation>()
            .AddSingleton<ISingletonOperation, DefaultOperation>()
            .AddTransient<OperationLogger>())
    .Build();

ExemplifyScoping(host.Services, "Scope 1");
ExemplifyScoping(host.Services, "Scope 2");

await host.RunAsync();

static void ExemplifyScoping(IServiceProvider services, string scope)
{
    using IServiceScope serviceScope = services.CreateScope();
    IServiceProvider provider = serviceScope.ServiceProvider;

    OperationLogger logger = provider.GetRequiredService<OperationLogger>();
    logger.LogOperations($"{scope}-Call 1 .GetRequiredService<OperationLogger>()");

    Console.WriteLine("...");

    logger = provider.GetRequiredService<OperationLogger>();
    logger.LogOperations($"{scope}-Call 2 .GetRequiredService<OperationLogger>()");

    Console.WriteLine();
}

每个 services.Add{LIFETIME}<{SERVICE}> 扩展方法添加(并可能配置)服务。 我们建议应用遵循此约定。 将扩展方法置于 Microsoft.Extensions.DependencyInjection 命名空间中以封装服务注册的组。 还包括用于 DI 扩展方法的命名空间部分 Microsoft.Extensions.DependencyInjection

  • 允许在不添加其他 using 块的情况下在 IntelliSense 中显示它们。
  • 在通常会调用这些扩展方法的 ProgramStartup 类中,避免出现过多的 using 语句。

应用会执行以下操作:

结束语

应用会显示如下例所示的输出:

Scope 1-Call 1 .GetRequiredService<OperationLogger>(): ITransientOperation [ 80f4...Always different        ]
Scope 1-Call 1 .GetRequiredService<OperationLogger>(): IScopedOperation    [ c878...Changes only with scope ]
Scope 1-Call 1 .GetRequiredService<OperationLogger>(): ISingletonOperation [ 1586...Always the same         ]
...
Scope 1-Call 2 .GetRequiredService<OperationLogger>(): ITransientOperation [ f3c0...Always different        ]
Scope 1-Call 2 .GetRequiredService<OperationLogger>(): IScopedOperation    [ c878...Changes only with scope ]
Scope 1-Call 2 .GetRequiredService<OperationLogger>(): ISingletonOperation [ 1586...Always the same         ]

Scope 2-Call 1 .GetRequiredService<OperationLogger>(): ITransientOperation [ f9af...Always different        ]
Scope 2-Call 1 .GetRequiredService<OperationLogger>(): IScopedOperation    [ 2bd0...Changes only with scope ]
Scope 2-Call 1 .GetRequiredService<OperationLogger>(): ISingletonOperation [ 1586...Always the same         ]
...
Scope 2-Call 2 .GetRequiredService<OperationLogger>(): ITransientOperation [ fa65...Always different        ]
Scope 2-Call 2 .GetRequiredService<OperationLogger>(): IScopedOperation    [ 2bd0...Changes only with scope ]
Scope 2-Call 2 .GetRequiredService<OperationLogger>(): ISingletonOperation [ 1586...Always the same         ]

在应用输出中,可看到:

  • Transient 操作总是不同,每次检索服务时,都会创建一个新实例。
  • Scoped 仅随着新范围更改,但在一个范围中是相同的实例。
  • Singleton 操作总是相同,新实例仅被创建一次。

另请参阅