在 .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
C#
namespace ConsoleDI.Example;
public interface IOperation
{
string OperationId { get; }
}
IOperation 接口会定义一个 OperationId 属性。
IOperation.cs Transient
C#
namespace ConsoleDI.Example;
public interface ITransientOperation : IOperation
{
}
IOperation.cs Scoped
C#
namespace ConsoleDI.Example;
public interface IScopedOperation : IOperation
{
}
IOperation.cs Singleton
C#
namespace ConsoleDI.Example;
public interface ISingletonOperation : IOperation
{
}
IOperation 的所有子接口会会命名其预期服务生存期。 例如,“Transient”或“Singleton”。
添加默认实现
添加以下默认实现来进行各种操作:
DefaultOperation.cs
C#
using static System.Guid;
namespace ConsoleDI.Example;
public class DefaultOperation :
ITransientOperation,
IScopedOperation,
ISingletonOperation
{
public string OperationId { get; } = NewGuid().ToString()[^4..];
}
DefaultOperation 会实现所有已命名的标记接口,并将 OperationId 属性初始化为新的全局唯一标识符 (GUID) 的最后 4 个字符。
添加需要 DI 的服务
添加以下操作记录器对象,它作为服务添加到控制台应用:
OperationLogger.cs
C#
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 会定义一个构造函数,该函数需要上述每一个标记接口(即 ITransientOperation、IScopedOperation 和 ISingletonOperation)。 对象会公开一个方法,使用者可通过该方法使用给定的 scope 参数记录操作。 被调用时,LogOperations 方法会使用范围字符串和消息记录每个操作的唯一标识符。
为 DI 注册服务
使用以下代码更新 Program.cs:
C#
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{SERVICE_NAME} 扩展方法添加(并可能配置)服务。 我们建议应用遵循此约定。 将扩展方法置于
Microsoft.Extensions.DependencyInjection 命名空间中以封装服务注册的组。 还包括用于 DI 扩展方法的命名空间部分 Microsoft.Extensions.DependencyInjection:
- 允许在不添加其他 using 块的情况下在 IntelliSense 中显示它们。
- 在通常会调用这些扩展方法的 Program 或 Startup 类中,避免出现过多的 using 语句。
应用会执行以下操作:
- 使用默认活页夹设置创建一个 IHostBuilder 实例。
- 配置服务并对其添加相应的服务生存期。
- 调用 Build() 并分配 IHost 的实例。
- 调用 ExemplifyScoping,传入 IHost.Services。
结束语
应用会显示如下例所示的输出:
控制台
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 操作总是相同,新实例仅被创建一次。
ActivatorUtilities
无法自动注入的类想使用DI提供的类时可使用此类创建对应的实例
var testTask = ActivatorUtilities.CreateInstance<TestTask>(serProvider, "test"); testTask.Execute(new CancellationToken());
public class TestTask : ITask
{
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly IMyHostService myHostService;
private readonly string _name;
public TestTask(IServiceScopeFactory serviceScopeFactory,
IMyHostService myHostService,
string name)
{
_serviceScopeFactory = serviceScopeFactory;
this.myHostService = myHostService;
_name = name;
}
}