在.NET中依赖注入(DI)一种常用的软件设计模式,在类与其依赖关系之间实现控制反转(IOC)。
依赖注入解决了以下问题:
- 使用接口或基类将依赖关系实现抽象化。
- 在服务容器中注册依赖关系。 .NET 提供了一个内置的服务容器 IServiceProvider。 服务通常在应用启动时注册,并追加到 IServiceCollection。 添加所有服务后,可以使用 BuildServiceProvider 创建服务容器。
- 将服务注入到使用它的类的构造函数中。 框架负责创建依赖关系的实例,并在不再需要时将其释放。
使用示例如下:
1.将依赖对象抽象成接口。
a.该接口中定义了Write(string message)方法。
public interface IMessageWriter
{
void Write(string message);
}
2.实现依赖对象。
a.实现IMessageWriter接口。
public class MessageWriter: IMessageWriter
{
public void Write(string message)
{
Console.WriteLine($"MessageWriter.Write(message: \"{message}\")");
}
}
3.实现使用依赖对象的对象。
a.通过构造函数的方式注入IMessageWriter实例。
public class Worker(IMessageWriter messageWriter) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
messageWriter.Write($"Worker running at: {DateTimeOffset.Now}");
await Task.Delay(1_000, stoppingToken);
}
}
}
4.注入依赖对象。
a.添加Worker服务。
b.通过AddSingleton方法注册MessageWriter类型到IMessageWriter接口上。
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
//添加Worker服务
builder.Services.AddHostedService<Worker>();
//册使用具体类型 MessageWriter 注册 MessageWriter 服务
// AddSingleton 方法使用单一实例生存期(应用的生存期)注册服务。
builder.Services.AddSingleton<IMessageWriter, MessageWriter>();
using IHost host = builder.Build();
host.Run();
5.新增一个依赖对象。
public class LoggingMessageWriter(ILogger<LoggingMessageWriter> logger) : IMessageWriter
{
public void Write(string message) =>logger.LogInformation("Info: {Msg}", message);
}
6.修改注入对象。
a.该步骤同4。
b.builder.Services.AddSingleton<IMessageWriter, LoggingMessageWriter>()。修改该代码后,注入对象从MessageWriter变成了LoggingMessageWriter。
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<Worker>();
//builder.Services.AddSingleton<IMessageWriter, MessageWriter>();
builder.Services.AddSingleton<IMessageWriter, LoggingMessageWriter>();
using IHost host = builder.Build();
host.Run();
服务生存期
关键字生命周期描述实例创建情况AddTransient暂时每次请求时创建服务,结束时会释放掉此服务。Transient 服务总是不同的,每次检索服务时,都会创建一个新实例AddScoped范围每个客户端请求(连接)创建一次服务,请求(连接)结束时释放。Scoped 服务只会随着新范围而改变,但在一个范围中是相同的实例AddSingleton单例首次请求时创建。应用关闭并释放 ServiceProvider 时释放。Singleton 服务总是相同的,新实例仅被创建一次
服务生命周期使用示例如下:
1.创建报表服务生命周期接口
/// <summary>
/// 报表服务生命周期接口
/// </summary>
public interface IReportServiceLifetime
{
Guid Id { get; }
/// <summary>
/// 生命周期
/// </summary>
ServiceLifetime Lifetime { get; }
}
2.定义三种生命周期接口
public interface IExampleTransientService : IReportServiceLifetime
{
ServiceLifetime IReportServiceLifetime.Lifetime => ServiceLifetime.Transient;
}
public interface IExampleScopedService : IReportServiceLifetime
{
ServiceLifetime IReportServiceLifetime.Lifetime => ServiceLifetime.Scoped;
}
public interface IExampleSingletonService : IReportServiceLifetime
{
ServiceLifetime IReportServiceLifetime.Lifetime => ServiceLifetime.Singleton;
}
3.实现三种生命周期示例
internal sealed class ExampleTransientService : IExampleTransientService
{
Guid IReportServiceLifetime.Id { get; } = Guid.NewGuid();
}
internal sealed class ExampleScopedService : IExampleScopedService
{
Guid IReportServiceLifetime.Id { get; } = Guid.NewGuid();
}
internal sealed class ExampleSingletonService : IExampleSingletonService
{
Guid IReportServiceLifetime.Id { get; } = Guid.NewGuid();
}
4.创建需要DI的服务
/// <summary>
/// 生命周期报告
/// </summary>
/// <param name="transientService"></param>
/// <param name="scopedService"></param>
/// <param name="singletonService"></param>
internal sealed class ServiceLifetimeReporter(
IExampleTransientService transientService,
IExampleScopedService scopedService,
IExampleSingletonService singletonService)
{
public void ReportServiceLifetimeDetails(string lifetimeDetails)
{
Console.WriteLine(lifetimeDetails);
LogService(transientService, "Always different");
LogService(scopedService, "Changes only with lifetime");
LogService(singletonService, "Always the same");
}
private static void LogService<T>(T service, string message)where T : IReportServiceLifetime =>
Console.WriteLine($" {typeof(T).Name}: {service.Id} ({message})");
}
5.注册服务
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
//注册瞬时服务
builder.Services.AddTransient<IExampleTransientService, ExampleTransientService>();
//注册范围服务
builder.Services.AddScoped<IExampleScopedService, ExampleScopedService>();
//注册单例服务
builder.Services.AddSingleton<IExampleSingletonService, ExampleSingletonService>();
builder.Services.AddTransient<ServiceLifetimeReporter>();
using IHost host = builder.Build();
ExemplifyServiceLifetime(host.Services, "Lifetime 1");
ExemplifyServiceLifetime(host.Services, "Lifetime 2");
await host.RunAsync();
static void ExemplifyServiceLifetime(IServiceProvider hostProvider, string lifetime)
{
using IServiceScope serviceScope = hostProvider.CreateScope();
IServiceProvider provider = serviceScope.ServiceProvider;
ServiceLifetimeReporter logger = provider.GetRequiredService<ServiceLifetimeReporter>();
logger.ReportServiceLifetimeDetails(
$"{lifetime}: Call 1 to provider.GetRequiredService<ServiceLifetimeReporter>()");
Console.WriteLine("...");
logger = provider.GetRequiredService<ServiceLifetimeReporter>();
logger.ReportServiceLifetimeDetails(
$"{lifetime}: Call 2 to provider.GetRequiredService<ServiceLifetimeReporter>()");
Console.WriteLine();
}
6.服务生命周期分析
运行结果如图,
其中IExampleTransientService(瞬时服务),在每次被调用的时候GUID都发生了变化,说明访问的不是同一个服务实例,服务每次被调用完后都会释放,并在被调用时新建。
其中IExampleScopedService(范围服务),在Lifetime 1中每次被调用的时候GUID未发生变化,在Lifetime 2中被调用时才发生变化,说明Lifetime 1中访问的是同一个服务实例,在Lifetime 2中访问的是另一个服务实例,范围服务在一次客户端请求结束后会释放,并在下次客户端请求发起时新建。
其中IExampleSingletonService(单例服务)在每次调用时的GUID都未发生变化,说明每次请求都调用的同一个服务示例,该服务会在应用关闭并释放 ServiceProvider 时释放。
键控服务
在.NET8开始支持基于秘钥的服务注册和查找,假设存在实现需要注册多个实现同一个接口的类时,可以通过键控服务来实现,示例如下:
1.使用FromKeyedServices特性来指定秘钥。
public class Worker([FromKeyedServices("M")] IMessageWriter messageWriter) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
messageWriter.Write($"Worker running at: {DateTimeOffset.Now}");
await Task.Delay(1_000, stoppingToken);
}
}
}
2.使用AddKeyedSingleton方法来使用键控服务。
a.FromKeyedServices("M")指向AddKeyedSingleton<T1,T2>("M")
b.FromKeyedServices("L")指向AddKeyedSingleton<T1,T2>("L")
builder.Services.AddHostedService<Worker>();
builder.Services.AddKeyedSingleton<IMessageWriter, MessageWriter>("M");
builder.Services.AddKeyedSingleton<IMessageWriter, LoggingMessageWriter>("L");
DI三大对象
.NET中依赖注入包含三大抽象对象:
IServiceCollection:为服务描述符集合定义协定
IServiceProvider:定义用于检索服务对象的机制
IServiceProvider:描述一种服务,其中包括该服务的类型、实现和生存期
在IServiceCollection中配置和管理服务,注册服务后调用BuildServiceProvider生成IServiceProvider实例,IServiceProvider充当所有已注册服务的容器。
核心实现如下(具体案例请见:依赖关系注入基本信息 - .NET | Microsoft Learn):
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");