.NET 6依赖注入中同一接口多实现如何选择指定类调用
.NET 6 同接口多实现依赖注入指定方案
你当前将ClassA、ClassB都注册为IDestinationFileNaming的实现时,.NET 内置DI容器会按注册顺序存储服务实例,直接注入单个IDestinationFileNaming参数只会拿到最后注册的ClassB。要获取指定实现有以下三种常用方案,你提到的where T : class泛型约束本身仅用于限制泛型参数为引用类型,不具备服务选择能力,但配合泛型接口设计可以实现精准注入,具体见方案3。
方案1:注入全量实现后按类型筛选
这是内置DI原生支持的最简单方案,不需要修改现有注册逻辑,直接在构造函数中注入该接口的所有实现,再通过类型筛选出目标实例即可。
修改ClassC代码如下:
public class ClassC{ private readonly IDestinationFileNaming _destinationFileNaming; public ClassC(IEnumerable<IDestinationFileNaming> allNamingImpls) { // 从所有注册实现中筛选ClassA的实例 _destinationFileNaming = allNamingImpls.OfType<ClassA>().FirstOrDefault() ?? throw new InvalidOperationException("未找到IDestinationFileNaming的ClassA实现"); } public void Run(){ // 直接调用即为ClassA的Generate逻辑 var fileName = _destinationFileNaming.Generate(); } }
- 优点:无需修改注册逻辑,代码改动量极小
- 缺点:需要直接依赖具体实现类
ClassA,耦合度较高,实现数量较多时筛选有额外性能开销
方案2:封装工厂类做服务路由
如果要降低对具体实现的耦合,或者后续需要动态根据业务条件选择不同实现,可以新增工厂类封装选择逻辑。
- 先定义工厂接口与实现:
public interface INamingServiceFactory { IDestinationFileNaming GetImplementation(string implKey); } public class NamingServiceFactory : INamingServiceFactory { private readonly IServiceProvider _serviceProvider; // 维护key与实现类型的映射关系 private readonly Dictionary<string, Type> _implMapping = new() { {"A", typeof(ClassA)}, {"B", typeof(ClassB)} }; public NamingServiceFactory(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } public IDestinationFileNaming GetImplementation(string implKey) { if(!_implMapping.TryGetValue(implKey, out var implType)) throw new ArgumentException($"不支持的实现key:{implKey}"); return (IDestinationFileNaming)_serviceProvider.GetRequiredService(implType); } }
- 注册服务时新增工厂注册:
services.AddScoped<INamingServiceFactory, NamingServiceFactory>(); services.AddScoped<IDestinationFileNaming, ClassA>(); services.AddScoped<IDestinationFileNaming, ClassB>();
- 在
ClassC中注入工厂获取目标实现:
public class ClassC{ private readonly IDestinationFileNaming _destinationFileNaming; public ClassC(INamingServiceFactory namingFactory) { // 传入ClassA对应的key即可获取实例 _destinationFileNaming = namingFactory.GetImplementation("A"); } public void Run(){ var fileName = _destinationFileNaming.Generate(); } }
- 优点:业务层不依赖具体实现类,后续新增实现只需要修改工厂映射,符合开闭原则
- 缺点:需要额外编写工厂类代码,多一层间接调用
方案3:用泛型接口区分不同实现
这个方案就是泛型相关的实现思路:泛型接口不同的类型参数会被DI容器识别为完全独立的服务类型,不需要运行时筛选就能精准注入目标实现。
- 改造接口,新增泛型版本(可以保留原有非泛型接口兼容旧逻辑):
// 原有非泛型接口保留 public interface IDestinationFileNaming { string Generate(); } // 新增泛型接口,where T : class仅限制T为引用类型,不做服务选择 public interface IDestinationFileNaming<T> : IDestinationFileNaming where T : class { }
- 让实现类继承对应泛型接口,原有业务逻辑不需要改动:
public class ClassA : BaseFileNaming, IDestinationFileNaming<ClassA> { public ClassA() : base() { } public override string Generate() { string fileName = string.Empty; try { fileName = BaseName + "AIM_UBW"; } catch (Exception ex) { } return fileName; } } public class ClassB : BaseFileNaming, IDestinationFileNaming<ClassB> { public ClassB() : base() { } public override string Generate() { string fileName = string.Empty; try { fileName = BaseName + "DDS_UBW"; } catch (Exception ex) { } return fileName; } }
- 修改服务注册逻辑:
// 注册泛型接口的服务 services.AddScoped<IDestinationFileNaming<ClassA>, ClassA>(); services.AddScoped<IDestinationFileNaming<ClassB>, ClassB>(); // 如果需要兼容获取所有非泛型实现,可以额外注册非泛型服务 services.AddScoped<IDestinationFileNaming>(sp => sp.GetRequiredService<IDestinationFileNaming<ClassA>>()); services.AddScoped<IDestinationFileNaming>(sp => sp.GetRequiredService<IDestinationFileNaming<ClassB>>());
- 在
ClassC中直接注入对应泛型服务,不需要额外筛选:
public class ClassC{ private readonly IDestinationFileNaming _destinationFileNaming; public ClassC(IDestinationFileNaming<ClassA> classANaming) { _destinationFileNaming = classANaming; } public void Run(){ // 这里拿到的就是确定的ClassA实例 var fileName = _destinationFileNaming.Generate(); } }
- 优点:编译期就能确定注入的服务类型,没有运行时筛选开销,出错概率低
- 缺点:需要修改接口定义和服务注册代码
注意:.NET 7及以上版本新增了键控服务(Keyed Services)能力,可以直接通过
AddKeyedScoped给服务打key,注入时用[FromKeyedServices("keyA")]特性就能直接获取对应实现,不需要自己写工厂或者泛型接口,但.NET 6不支持该特性,只能使用上述三种方案。
内容的提问来源于stack exchange,提问作者K.Z
相关产品推荐
相关产品推荐

