通过名称解析依赖注入

Posted

技术标签:

【中文标题】通过名称解析依赖注入【英文标题】:Dependency injection resolving by name 【发布时间】:2016-12-28 13:26:53 【问题描述】:

如何为特定类注入不同的对象实现?

例如在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 就是一个例子【参考方案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 顺便说一句,图书馆的荣誉。我能够很容易地插入它。我喜欢它不需要任何复杂的接线这一事实。我发现最快的对象实例化发生在我仅针对启动时默认创建的 IEnumerable 运行一个简单的 LINQ 查询时。 Ninject 提供了一个命名实现,我不明白为什么微软不只是移植类似的东西。我看不出它是如何违反国际奥委会的原则的。干得好!【参考方案3】:

您不能使用内置的 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&lt;T&gt; / IOptions&lt;T&gt; 的方式使用泛型?

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 起,如果您未能定义映射,则应引发验证错误。

【讨论】:

以上是关于通过名称解析依赖注入的主要内容,如果未能解决你的问题,请参考以下文章

解析-依赖注入

iOS之深入解析依赖注入的原理与应用

.NET Core WebAPI 依赖注入解析 null

Spring源码解读---依赖注入源码解析

Spring源码解读---依赖注入源码解析

Spring源码解读---依赖注入源码解析