通过名称解析依赖注入
Posted
技术标签:
【中文标题】通过名称解析依赖注入【英文标题】:Dependency injection resolving by name 【发布时间】:2016-08-22 05:37:02 【问题描述】:如何为特定类注入不同的对象实现?
例如在Unity中,我可以定义IRepository
的两个实现
container.RegisterType<IRepository, TestSuiteRepositor("TestSuiteRepository");
container.RegisterType<IRepository, BaseRepository>();
并调用所需的实现
public BaselineManager([Dependency("TestSuiteRepository")]IRepository repository)
【问题讨论】:
您不应该在单元测试中需要或使用 IoC(表明您做的事情非常错误)。对于集成测试,您应该使用多个启动类,例如 radu-matei 说的 这不是单元测试,它是业务逻辑的一部分 =) TestSuite 是业务实体 我在这里使用强类型方法发布了一个类似问题的答案:***.com/questions/39174989/… 这能回答你的问题吗? How to register multiple implementations of the same interface in Asp.Net Core? 【参考方案1】:正如@Tseng 所指出的,命名绑定没有内置解决方案。但是,使用工厂方法可能对您的情况有所帮助。示例应如下所示:
创建存储库解析器:
public interface IRepositoryResolver
IRepository GetRepositoryByName(string name);
public class RepositoryResolver : IRepositoryResolver
private readonly IServiceProvider _serviceProvider;
public RepositoryResolver(IServiceProvider serviceProvider)
_serviceProvider = serviceProvider;
public IRepository GetRepositoryByName(string name)
if(name == "TestSuiteRepository")
return _serviceProvider.GetService<TestSuiteRepositor>();
//... other condition
else
return _serviceProvider.GetService<BaseRepositor>();
在ConfigureServices.cs
注册需要的服务
services.AddSingleton<IRepositoryResolver, RepositoryResolver>();
services.AddTransient<TestSuiteRepository>();
services.AddTransient<BaseRepository>();
最终在任何类中使用它:
public class BaselineManager
private readonly IRepository _repository;
public BaselineManager(IRepositoryResolver repositoryResolver)
_repository = repositoryResolver.GetRepositoryByName("TestSuiteRepository");
【讨论】:
我试试你说的,但是在GetRepositoryByName()
方法中,我得到了这个错误:he non-generic method 'IServiceProvider.GetService(Type)' cannot be used with type arguments
!
你需要 Microsoft.Extensions.DependencyInjection 或 _serviceProvider.GetService(typeof(TestSuiteRepository))
这个解决方案对我有用,因为我需要一个 DbContextFactory 对象。
这是 service locator anti-pattern 在引擎盖下。看看this good explanation of service locator vs abstract factory。您“应该”永远不要在业务层中引用 DI 容器。而是传递一个带有容器所需参数的委托,以获取您想要的实例。 Here 就是一个例子
@gfache - “永远不应该在你的业务层引用 DI 容器”:你所说的“DI 容器”有多宽泛?我同意注入/依赖IServiceProvider
显然是一种服务定位器模式,但我已经编写了业务逻辑以在IServiceCollection
上包含辅助扩展方法,以使注册逻辑保持在定义附近。您是否也会考虑这种不良做法?【参考方案2】:
除了@adem-caglin 的回答,我想在这里发布一些我为基于名称的注册创建的可重用代码。
更新现在可以使用nuget package。
为了注册您的服务,您需要将以下代码添加到您的 Startup
类:
services.AddTransient<ServiceA>();
services.AddTransient<ServiceB>();
services.AddTransient<ServiceC>();
services.AddByName<IService>()
.Add<ServiceA>("key1")
.Add<ServiceB>("key2")
.Add<ServiceC>("key3")
.Build();
然后就可以通过IServiceByNameFactory
接口使用了:
public AccountController(IServiceByNameFactory<IService> factory)
_service = factory.GetByName("key2");
或者你可以使用工厂注册来保持客户端代码干净(我更喜欢)
_container.AddScoped<AccountController>(s => new AccountController(s.GetByName<IService>("key2")));
扩展的完整代码在github。
【讨论】:
看起来这违背了依赖注入的目的。现在你的常规类依赖于依赖框架。 作为替代方案,IoC 容器可以决定注入什么实例。尽管它需要更复杂的基于名称的注册规则,这可能是一个很好的解决方案,无需显式依赖。 @mac10688 最后我添加了这种可能性github.com/yuriy-nelipovich/DependencyInjection.Extensions 有趣的是,默认情况下,ASP.NET Core 的默认 IOC 实现会为您在 Startup 中定义的所有实现实例创建一个 IEnumerable您不能使用内置的 ASP.NET Core IoC 容器。
这是设计。内置容器有意保持简单且易于扩展,因此如果您需要更多功能,可以插入第三方容器。
您必须使用第三方容器来执行此操作,例如 Autofac(请参阅 docs)。
public BaselineManager([WithKey("TestSuiteRepository")]IRepository repository)
【讨论】:
【参考方案4】:看了the official documentation for dependency injection之后,我觉得你不能这样。
但我的问题是:您是否同时需要这两个实现?因为如果你不这样做,你可以create multiple environments through environment variables 和specific functionality in the Startup
class based on the current environment,甚至创建多个StartupEnvironmentName
类。
当 ASP.NET Core 应用程序启动时,
Startup
类用于引导应用程序、加载其配置设置等(了解有关 ASP.NET 启动的更多信息)。但是,如果存在名为StartupEnvironmentName
的类(例如StartupDevelopment
),并且ASPNETCORE_ENVIRONMENT
环境变量与该名称匹配,则将使用该Startup
类。因此,您可以配置Startup
用于开发,但有一个单独的StartupProduction
将在应用程序在生产中运行时使用。反之亦然。
我也是wrote an article about injecting dependencies from a JSON file,因此您不必每次想要在实现之间切换时都重新编译整个应用程序。基本上,您保留一个 JSON 数组,其中包含如下服务:
"services": [
"serviceType": "ITest",
"implementationType": "Test",
"lifetime": "Transient"
]
然后您可以在这个文件中修改所需的实现,而不必重新编译或更改环境变量。
希望这会有所帮助!
【讨论】:
如果我们同时需要两个实现该怎么办? 堆叠一个接口的多个实现似乎是一个常见的用例,以至于试图弄清楚为什么他们忽略了这一点让我很头疼。但这就是你的微软。【参考方案5】:首先,这可能仍然是个坏主意。您要实现的是将依赖项的使用方式和定义方式分开。但是你想使用依赖注入框架,而不是反对它。避免服务定位器反模式的发现能力差。为什么不以类似于ILogger<T>
/ IOptions<T>
的方式使用泛型?
public BaselineManager(RepositoryMapping<BaselineManager> repository)
_repository = repository.Repository;
public class RepositoryMapping<T>
private IServiceProvider _provider;
private Type _implementationType;
public RepositoryMapping(IServiceProvider provider, Type implementationType)
_provider = provider;
_implementationType = implementationType;
public IRepository Repository => (IRepository)_provider.GetService(_implementationType);
public static IServiceCollection MapRepository<T,R>(this IServiceCollection services) where R : IRepository =>
services.AddTransient(p => new RepositoryMapping<T>(p, typeof(R)));
services.AddScoped<BaselineManager>();
services.MapRepository<BaselineManager, BaseRepository>();
自 .net core 3 起,如果您未能定义映射,则应引发验证错误。
【讨论】:
以上是关于通过名称解析依赖注入的主要内容,如果未能解决你的问题,请参考以下文章