.NET Core DI多接口注册与实例共享问题咨询
.NET Core DI 相关问题与解决方案
背景信息
现有服务及接口定义如下:
public class ServiceA: IServiceA {} public class ServiceB: IServiceB {} public class ServiceC: IServiceC {}
这些服务都包含相同的重置方法:
public Task ResetAsync(CancellationToken cancellationToken = default) { // 重置相关逻辑 }
已为该方法定义统一的重置能力接口:
public interface IResettable { Task ResetAsync(CancellationToken cancellationToken = default); }
当前服务已通过以下方式注册为单例:
services.AddSingleton<IServiceA, ServiceA>(); services.AddSingleton<IServiceB, ServiceB>(); services.AddSingleton<IServiceC, ServiceC>();
有两种接口继承方案可选:
方案一
让业务接口继承重置接口:
IServiceA : IResettable IServiceB : IResettable IServiceC : IResettable
方案二
让服务类直接实现业务接口和重置接口:
ServiceA : IServiceA, IResettable ServiceB : IServiceB, IResettable ServiceC : IServiceC, IResettable
我们需要通过定时任务调用所有服务的ResetAsync方法,已实现ResetService:
public class ResetService( IEnumerable<IResettable> services) : IResetService { private readonly IEnumerable<IResettable> _services = services; public async Task DoWorkAsync(CancellationToken cancellationToken = default) { foreach (var service in _services) { await service.ResetAsync(cancellationToken); } } }
但如果按以下方式注册,会生成新的实例,而非复用已注册的IServiceA/B/C实例:
services.AddTransient<IResettable, ServiceA>(); services.AddTransient<IResettable, ServiceB>(); services.AddTransient<IResettable, ServiceC>(); services.TryAddSingleton<IResetService, ResetService>();
问题与解答
1. 以下两种单例注册方式的核心区别是什么?
services.AddSingleton<IServiceA, ServiceA>(); services.AddSingleton<ServiceA>();
解答:
- 第一种
AddSingleton<IServiceA, ServiceA>():注册的是「业务接口IServiceA对应实现类ServiceA」,DI容器会维护一个ServiceA的单例实例,只有请求IServiceA时才会返回这个实例;如果直接请求ServiceA类型,容器会创建新实例(除非单独注册过ServiceA)。 - 第二种
AddSingleton<ServiceA>():注册的是「服务类型和实现类型都是ServiceA」,DI容器维护一个ServiceA的单例实例,只有请求ServiceA本身时才会返回这个单例;请求IServiceA的话,容器会创建新实例(除非单独注册过IServiceA)。
本质是两种注册的服务键不同,容器会分别管理这两个注册项的实例。
2. 应选择哪种继承方式?原因是什么?
// 选项1:服务类直接实现两个接口 ServiceA : IServiceA, IResettable // 选项2:业务接口继承重置接口 ServiceA : IServiceA IServiceA : IResettable
解答:
优先选选项1(服务类直接实现IResettable),原因如下:
- 符合单一职责:
IServiceA是业务接口,专注定义业务能力;IResettable是通用重置能力,属于横切关注点。让业务接口继承重置接口会把两个不相关的职责耦合,违背单一职责原则。 - 灵活性更高:后续如果某个服务不需要重置能力,直接不实现
IResettable即可;如果业务接口继承了IResettable,所有实现该业务接口的服务都必须实现ResetAsync,哪怕不需要。 - 语义更清晰:从类的定义就能直接看出它具备重置能力,无需通过业务接口的继承关系间接推断。
3. 核心问题:如何在.NET Core DI中注册同一ServiceA实例为IServiceA和IResettable,既可以在其他服务中使用IServiceA,又能在ResetService中通过IEnumerable调用所有服务的ResetAsync?
解答:
可以通过以下几种方式实现复用同一实例:
方式一:手动关联实例注册
先注册实现类的单例,再将业务接口和重置接口映射到同一个实例:
// 注册ServiceA单例,容器维护这个实例 services.AddSingleton<ServiceA>(); // 将IServiceA映射到已注册的ServiceA实例 services.AddSingleton<IServiceA>(sp => sp.GetRequiredService<ServiceA>()); // 将IResettable也映射到同一个ServiceA实例 services.AddSingleton<IResettable>(sp => sp.GetRequiredService<ServiceA>()); // ServiceB、ServiceC执行相同操作 services.AddSingleton<ServiceB>(); services.AddSingleton<IServiceB>(sp => sp.GetRequiredService<ServiceB>()); services.AddSingleton<IResettable>(sp => sp.GetRequiredService<ServiceB>()); services.AddSingleton<ServiceC>(); services.AddSingleton<IServiceC>(sp => sp.GetRequiredService<ServiceC>()); services.AddSingleton<IResettable>(sp => sp.GetRequiredService<ServiceC>()); // 注册ResetService services.AddSingleton<IResetService, ResetService>();
方式二:链式注册(.NET Core 3.0+)
利用DI容器的实例跟踪能力,链式注册实现接口复用:
// 链式注册,确保所有接口指向同一个ServiceA实例 services.AddSingleton<ServiceA>() .AddSingleton<IServiceA>(sp => sp.GetRequiredService<ServiceA>()) .AddSingleton<IResettable>(sp => sp.GetRequiredService<ServiceA>()); // ServiceB同理 services.AddSingleton<ServiceB>() .AddSingleton<IServiceB>(sp => sp.GetRequiredService<ServiceB>()) .AddSingleton<IResettable>(sp => sp.GetRequiredService<ServiceB>()); // ServiceC同理 services.AddSingleton<ServiceC>() .AddSingleton<IServiceC>(sp => sp.GetRequiredService<ServiceC>()) .AddSingleton<IResettable>(sp => sp.GetRequiredService<ServiceC>()); services.AddSingleton<IResetService, ResetService>();
方式三:批量反射注册(适合服务较多的场景)
通过扫描程序集,批量注册符合条件的服务:
// 扫描当前程序集中所有实现了IResettable和业务接口(命名为IServiceXXX)的类 var resettableServices = typeof(ServiceA).Assembly.GetTypes() .Where(t => !t.IsAbstract && !t.IsInterface && t.GetInterfaces().Contains(typeof(IResettable)) && t.GetInterfaces().Any(i => i.Name.StartsWith("IService"))); foreach (var serviceType in resettableServices) { // 获取对应的业务接口(如ServiceA对应IServiceA) var serviceInterface = serviceType.GetInterfaces().First(i => i.Name == $"I{serviceType.Name}"); // 注册服务类单例 services.AddSingleton(serviceType); // 注册业务接口到服务类实例 services.AddSingleton(serviceInterface, sp => sp.GetRequiredService(serviceType)); // 注册IResettable到服务类实例 services.AddSingleton(typeof(IResettable), sp => sp.GetRequiredService(serviceType)); } services.AddSingleton<IResetService, ResetService>();
以上三种方式都能保证IServiceA和IResettable指向同一个ServiceA实例,ResetService注入IEnumerable<IResettable>时,会拿到所有已注册的服务实例,且和业务中使用的IServiceA/B/C是同一个对象。
内容的提问来源于stack exchange,提问作者Alex Nevajniy
相关产品推荐
相关产品推荐

