ChatGPT解决这个技术问题 Extra ChatGPT

如何在 Asp.Net Core 中注册同一接口的多个实现?

我有从相同接口派生的服务。

public interface IService { }
public class ServiceA : IService { }
public class ServiceB : IService { } 
public class ServiceC : IService { }

通常,其他 IoC 容器(如 Unity)允许您通过一些区分它们的 Key 注册具体实现。

在 ASP.NET Core 中,如何注册这些服务并在运行时根据某些键解析它们?

我没有看到任何采用 keyname 参数的 Add 服务方法,这些参数通常用于区分具体实现。

    public void ConfigureServices(IServiceCollection services)
    {            
         // How do I register services of the same interface?            
    }


    public MyController:Controller
    {
       public void DoSomething(string key)
       { 
          // How do I resolve the service by key?
       }
    }

工厂模式是这里唯一的选择吗?

Update1
我已经阅读了文章 here,该文章展示了当我们有多个具体实现时如何使用工厂模式来获取服务实例。但是,它仍然不是一个完整的解决方案。当我调用 _serviceProvider.GetService() 方法时,我无法将数据注入构造函数。

例如考虑这个:

public class ServiceA : IService
{
     private string _efConnectionString;
     ServiceA(string efconnectionString)
     {
       _efConnecttionString = efConnectionString;
     } 
}

public class ServiceB : IService
{    
   private string _mongoConnectionString;
   public ServiceB(string mongoConnectionString)
   {
      _mongoConnectionString = mongoConnectionString;
   }
}

public class ServiceC : IService
{    
    private string _someOtherConnectionString
    public ServiceC(string someOtherConnectionString)
    {
      _someOtherConnectionString = someOtherConnectionString;
    }
}

_serviceProvider.GetService() 如何注入适当的连接字符串?在 Unity 或任何其他 IoC 库中,我们可以在类型注册时做到这一点。但是,我可以使用 IOption,这需要我注入所有设置。我无法将特定的连接字符串注入服务。

另请注意,我试图避免使用其他容器(包括 Unity),因为我还必须使用新容器注册其他所有内容(例如,控制器)。

此外,使用工厂模式创建服务实例是违反 DIP 的,因为它会增加客户端的依赖项数量 details here

所以,我认为 ASP.NET Core 中的默认 DI 缺少两件事:

使用键注册实例的能力 在注册期间将静态数据注入构造函数的能力

可以将 Update1 移到不同的问题,因为在构造函数中注入东西与确定要构造的对象有很大不同
未来的读者可能希望在此处查看我的答案(stackoverflow.com/questions/42402064/…)以避免..我想说的是..将服务定位器引入组合中。只是给了另一个选择。
这里的问题是密钥的要求。如果我们去掉密钥的概念,我们就可以拥有我们的工厂并吃掉它。这里的问题是我们用标准工厂模式的想法强制实施的业务逻辑(强制所有东西都有一个键)。波动性在于业务逻辑,而不是实现。如果我们认为这是需要抽象的易变事物,那么需要密钥就消失了。请在下面查看我的答案以了解实施细节。 AMA。
一个相关但更有针对性的问题是 here。有任何想法吗?

I
Ian Kemp

当我发现自己处于这种情况时,我使用 Func 做了一个简单的解决方法。

首先声明一个共享委托:

public delegate IService ServiceResolver(string key);

然后在您的 Startup.cs 中,设置多个具体注册和这些类型的手动映射:

services.AddTransient<ServiceA>();
services.AddTransient<ServiceB>();
services.AddTransient<ServiceC>();

services.AddTransient<ServiceResolver>(serviceProvider => key =>
{
    switch (key)
    {
        case "A":
            return serviceProvider.GetService<ServiceA>();
        case "B":
            return serviceProvider.GetService<ServiceB>();
        case "C":
            return serviceProvider.GetService<ServiceC>();
        default:
            throw new KeyNotFoundException(); // or maybe return null, up to you
    }
});

并从使用 DI 注册的任何类中使用它:

public class Consumer
{
    private readonly IService _aService;

    public Consumer(ServiceResolver serviceAccessor)
    {
        _aService = serviceAccessor("A");
    }

    public void UseServiceA()
    {
        _aService.DoTheThing();
    }
}

请记住,在这个例子中,解析的关键是一个字符串,为了简单起见,因为 OP 特别要求这种情况。

但是您可以使用任何自定义分辨率类型作为键,因为您通常不希望一个巨大的 n-case 开关腐烂您的代码。取决于您的应用程序的扩展方式。


@MatthewStevenMonkan 用一个例子更新了我的答案
使用这样的工厂模式是最好的方法。感谢分享!
+1 非常整洁,因为当我们使用其他双容器时,我们必须在需要解决依赖关系时包含它们的包,例如。 AutoFac 中的 ILifetimeScope。
@AnupamSingh 在我看来,大多数在 .NET Core 上运行的中小型应用程序不需要任何 DI 框架,只是增加了复杂性和不需要的依赖项,内置 DI 的美观和简单已经绰绰有余,它可以也可以轻松扩展。
否决票解释 - 它非常有趣,但我目前正在重构一个庞大的代码库以删除几年前某人所做的所有这些 Func 魔法(在 MS DI 革命之前),问题在于它极大地增加了属性的 connascence 复杂性可能会进一步导致复杂的 DI 分辨率。例如,我在一个 Windows 服务处理程序上工作,有超过 1.6k 行代码与 Func 相关,在执行完推荐的 DI 方法后,我将其减少到 0.2k 行。 OK-Lines of code 没有任何意义......除了现在更容易阅读和重用......
r
rnrneverdies

另一种选择是使用来自 Microsoft.Extensions.DependencyInjection 的扩展方法 GetServices

将您的服务注册为:

services.AddSingleton<IService, ServiceA>();
services.AddSingleton<IService, ServiceB>();
services.AddSingleton<IService, ServiceC>();

然后用一点 Linq 解决:

var services = serviceProvider.GetServices<IService>();
var serviceB = services.First(o => o.GetType() == typeof(ServiceB));

或者

var serviceZ = services.First(o => o.Name.Equals("Z"));

(假设 IService 有一个名为“Name”的字符串属性)

确保有 using Microsoft.Extensions.DependencyInjection;

更新

AspNet 2.1 来源:GetServices


不确定,但我认为这不是确定性的。你今天得到的任何结果明天可能会改变,这似乎不是一个好习惯。
支持 GetServices 的链接,它向我展示了您可以通过请求 IEnumerable<IService> 来请求依赖服务的服务列表
serviceProvider.GetServices() 将分别实例化 ServiceA、ServiceB 和 ServiceC。您只想调用一项服务的构造函数 - 您实际需要的服务。如果实现不是轻量级的,或者您有许多 IService 实现(例如,您有每个模型的自动生成的 IRepository 实现),这将是一个大问题。
我同意@Uros。这不是一个好的解决方案。想象一下,如果您注册了 10 个 IService 实现,而您实际需要的实例是最后一个,会发生什么情况。在这种情况下,DI 实际创建了 9 个实例,这些实例从未使用过。
坏主意:多个未使用的实例、服务定位器反模式和与实际实现的直接耦合(typeof)。
C
Community

工厂方法当然是可行的。另一种方法是使用继承来创建从 IService 继承的单个接口,在 IService 实现中实现继承的接口,并注册继承的接口而不是基接口。添加继承层次结构或工厂是否是“正确”模式完全取决于您与谁交谈。在使用泛型(例如 IRepository<T>)作为数据访问基础的同一应用程序中处理多个数据库提供程序时,我经常不得不使用这种模式。

示例接口和实现:

public interface IService 
{
}

public interface IServiceA: IService
{}

public interface IServiceB: IService
{}

public interface IServiceC: IService
{}

public class ServiceA: IServiceA 
{}

public class ServiceB: IServiceB
{}

public class ServiceC: IServiceC
{}

容器:

container.Register<IServiceA, ServiceA>();
container.Register<IServiceB, ServiceB>();
container.Register<IServiceC, ServiceC>();

这导致我看到一个空接口(IServiceA、IServiceB、IServiceC),它们通常被认为是代码异味。这种特殊情况可以接受吗?
当我需要注入最通用的类型(IService)时,这无助于注入 - 但不同用例的不同实现
T
T Brown

我只是简单地注入一个 IEnumerable

Startup.cs 中的配置服务

Assembly.GetEntryAssembly().GetTypesAssignableFrom<IService>().ForEach((t)=>
                {
                    services.AddScoped(typeof(IService), t);
                });

服务文件夹

public interface IService
{
    string Name { get; set; }
}

public class ServiceA : IService
{
    public string Name { get { return "A"; } }
}

public class ServiceB : IService
{    
    public string Name { get { return "B"; } }
}

public class ServiceC : IService
{    
    public string Name { get { return "C"; } }
}

我的控制器.cs

public class MyController
{
    private readonly IEnumerable<IService> _services;
    public MyController(IEnumerable<IService> services)
    {
        _services = services;
    }
    public void DoSomething()
    {
        var service = _services.Where(s => s.Name == "A").Single();
    }
...
}

扩展.cs

    public static List<Type> GetTypesAssignableFrom<T>(this Assembly assembly)
    {
        return assembly.GetTypesAssignableFrom(typeof(T));
    }
    public static List<Type> GetTypesAssignableFrom(this Assembly assembly, Type compareType)
    {
        List<Type> ret = new List<Type>();
        foreach (var type in assembly.DefinedTypes)
        {
            if (compareType.IsAssignableFrom(type) && compareType != type)
            {
                ret.Add(type);
            }
        }
        return ret;
    }

在 Controller 的 DoSomething() 方法中,您可以使用 typeof 来解析您想要的服务: var service = _services.FirstOrDefault(t => t.GetType() == typeof(ServiceA));
我真的尝试了一切,这是唯一对我有用的解决方案。谢谢!
@Skatz1990 试试我在另一篇文章中创建的解决方案。我认为它更清洁,更易于使用。
这很好——我试图注入一个列表,但它没有用。我必须是一个可枚举的。
感谢您存储该变体,非常适合我的情况,而且我更喜欢下面的变体,尤其是对于 Assembly.GetEntryAssembly().GetTypesAssignableFrom<IService>().ForEach((t)=> { services.AddScoped(typeof(IService), t); });
O
Ole Haugset

这个聚会有点晚了,但这是我的解决方案:...

Startup.cs 或 Program.cs 如果通用处理程序...

services.AddTransient<IMyInterface<CustomerSavedConsumer>, CustomerSavedConsumer>();
services.AddTransient<IMyInterface<ManagerSavedConsumer>, ManagerSavedConsumer>();

T接口设置的IMyInterface

public interface IMyInterface<T> where T : class, IMyInterface<T>
{
    Task Consume();
}

T的IMyInterface的具体实现

public class CustomerSavedConsumer: IMyInterface<CustomerSavedConsumer>
{
    public async Task Consume();
}

public class ManagerSavedConsumer: IMyInterface<ManagerSavedConsumer>
{
    public async Task Consume();
}

访问控制器中的服务

public class MyController
{
    private readonly IMyInterface<CustomerSavedConsumer> _customerSavedConsumer;
    private readonly IMyInterface<ManagerSavedConsumer> _managerSavedConsumer;

    public MyController(IMyInterface<CustomerSavedConsumer> customerSavedConsumer, IMyInterface<ManagerSavedConsumer> managerSavedConsumer)
    {
        _customerSavedConsumer = customerSavedConsumer;
        _managerSavedConsumer = managerSavedConsumer;
    }
}

希望如果这样做有任何问题,有人会指出为什么这是错误的方法。


IMyInterface<CustomerSavedConsumer>IMyInterface<ManagerSavedConsumer>不同 服务类型 - 这根本不能回答 OP 的问题。
OP 想要一种在 Asp.net 核心中注册同一接口的多个实现的方法。如果我没有这样做,请解释如何(确切地)。
虽然你是对的,但这种模式允许操作人员想要的效果。至少当我自己尝试这样做时,我偶然发现了这篇文章,我的解决方案最适合我的情况。
我希望问题更多的是为单个接口注册多个实现(使用 MS DI)不允许容器将一个实现与另一个实现区分开来。在其他 DI 中,您可以键入它们,以便容器知道选择哪个。在 MS 中,您必须使用委托并手动选择。您的解决方案没有解决这种情况,因为您的接口不同,因此容器选择正确的实现没有问题。虽然您的示例显然有效,但它不是解决问题的方法。
@Gray 即使您的帖子受到了一些不好的报道,我还是感谢您提出这个解决方案。它为读者提供了另一种选择来克服 .net cores DI 的限制。尽管它可能无法直接回答 OP 的问题,但它提供了一个完美的替代解决方案,这就是 SO 的全部意义所在,对吧?
R
Rico Suter

这里的大多数答案都违反了单一责任原则(服务类本身不应解决依赖关系)和/或使用服务定位器反模式。

避免这些问题的另一个选择是:

在接口上使用额外的泛型类型参数或实现非泛型接口的新接口,

实现一个适配器/拦截器类来添加标记类型,然后

使用泛型类型作为“名称”

我写了一篇更详细的文章:Dependency Injection in .NET: A way to work around missing named registrations


公认的答案紫罗兰单一责任原则如何?
请参阅 stackoverflow.com/a/52066039/876814 的评论,并且在接受的答案中,服务是懒惰地解决的,即您只知道它是否在运行时失败,并且在容器构建后无法在启动时静态检查这一点(类似于评论中的答案) . SRP,因为服务不仅负责其业务逻辑,还负责依赖解析
@RicoSuter 我真的很喜欢您博客中的解决方案,但对您在 Startup 课程中的 DI 感到困惑。具体来说,我不理解 MessagePublisher("MyOrderCreatedQueue") 行,因为我没有看到具有该签名的构造函数。 services.AddSingleton>(new MessagePublisher(new MessagePublisher("MyOrderCreatedQueue")));
谢谢,更新了文章并使用 MyMessagePublisher 作为 IMessagePublisher 的示例实现
G
Gerardo Grignoli

Microsoft.Extensions.DependencyInjection 不支持它。

但是您可以插入另一种依赖注入机制,例如 StructureMap See it's Home page,它是 GitHub Project

一点都不难:

在 project.json 中添加对 StructureMap 的依赖项:“Structuremap.Microsoft.DependencyInjection”:“1.0.1”,将其注入到 ConfigureServices 内的 ASP.NET 管道中并注册您的类(请参阅文档) public IServiceProvider ConfigureServices(IServiceCollection services) // 返回 IServiceProvider ! { // 添加框架服务。服务.AddMvc(); services.AddWhatever(); //使用结构映射; var 容器 = 新容器(); container.Configure(config => { // 使用 StructureMap API 在容器中注册内容... config.For().Add(new Cat("CatA")).Named("A"); config .For().Add(new Cat("CatB")).Named("B"); config.For().Use("A"); // 可选择设置默认配置。填充(服务);});返回容器.GetInstance();然后,要获取命名实例,您需要请求 IContainer public class HomeController : Controller { public HomeController(IContainer injectionContainer) { var myPet = injectionContainer.GetInstance("B");字符串名称 = myPet.Name; // 返回 "CatB"

而已。

要构建示例,您需要

    public interface IPet
    {
        string Name { get; set; }
    }

    public class Cat : IPet
    {
        public Cat(string name)
        {
            Name = name;
        }

        public string Name {get; set; }
    }

我已经尝试过这种方法,但我的控制器上出现运行时错误,因为在构建计划中找不到 IContainer。有什么我必须要求 IContainer 自动注入的吗?
顺便说一句,我使用的是 StructureMap.Micorosoft.DependencyInjection 1.3.0。
您是否在 ConfigureServices 中返回新容器?
它对我有用,感谢 GerardoGrignoli。 @mohrtan 如果您仍在研究此示例代码,请在此处。 github.com/Yawarmurtaza/AspNetCoreStructureMap
将容器注入控制器会破坏 IoC imho 的全部目的。
S
Sock

你是对的,内置的 ASP.NET Core 容器没有注册多个服务然后检索特定服务的概念,正如你所建议的,在这种情况下,工厂是唯一真正的解决方案。

或者,您可以切换到第三方容器,例如 Unity 或 StructureMap,它们确实提供了您需要的解决方案(在此处记录:https://docs.asp.net/en/latest/fundamentals/dependency-injection.html?#replacing-the-default-services-container)。


我认为 Jason Roberts 在他的帖子 Injecting a Factory Service in ASP.NET Core 中提出的方式在这种情况下可能是工厂方法的一个很好的扩展 - 请参阅帖子中的 ServiceCollectionExtensions.AddFactory 示例。
O
Oleg Suprun

为什么不使用继承?这样,我们可以拥有任意数量的接口副本,并且可以为每个副本选择合适的名称。而且我们有类型安全的好处

public interface IReportGenerator
public interface IExcelReportGenerator : IReportGenerator
public interface IPdfReportGenerator : IReportGenerator

具体类:

public class ExcelReportGenerator : IExcelReportGenerator
public class PdfReportGenerator : IPdfReportGenerator

登记:

代替

services.AddScoped<IReportGenerator, PdfReportGenerator>();
services.AddScoped<IReportGenerator, ExcelReportGenerator>();

我们有 :

services.AddScoped<IPdfReportGenerator, PdfReportGenerator>();
services.AddScoped<IExcelReportGenerator, ExcelReportGenerator>();

客户:

public class ReportManager : IReportManager
{
    private readonly IExcelReportGenerator excelReportGenerator;
    private readonly IPdfReportGenerator pdfReportGenerator;

    public ReportManager(IExcelReportGenerator excelReportGenerator, 
                         IPdfReportGenerator pdfReportGenerator)
    {
        this.excelReportGenerator = excelReportGenerator;
        this.pdfReportGenerator = pdfReportGenerator;
    }

这种方法还允许使用虱子耦合代码,因为我们可以将 IReportGenerator 移至应用程序的核心,并拥有将在更高级别声明的子接口。


我经常使用这种方法,但是当您想从外部程序集或类似程序中注册插件时,您将拥有多个相同的接口。 +1 用于您自己的代码库,-1 用于外部库;)
n
neleus

我遇到了同样的问题,想分享一下我是如何解决的以及为什么。

正如你提到的,有两个问题。首先:

在 Asp.Net Core 中,我如何注册这些服务并在运行时根据某些键解决它?

那么我们有哪些选择呢?网友们建议两个:

使用自定义工厂(如 _myFactory.GetServiceByKey(key))

使用另一个 DI 引擎(如 _unityContainer.Resolve(key))

工厂模式是这里唯一的选择吗?

事实上,这两个选项都是工厂,因为每个 IoC 容器也是工厂(尽管高度可配置且复杂)。在我看来,其他选项也是工厂模式的变体。

那么什么选择更好呢?在这里,我同意@Sock 的建议,他建议使用自定义工厂,这就是原因。

首先,我总是尽量避免在不需要时添加新的依赖项。所以我同意你的观点。此外,使用两个 DI 框架比创建自定义工厂抽象更糟糕。在第二种情况下,您必须添加新的包依赖项(如 Unity),但在这里依赖新的工厂接口并不那么邪恶。我相信 ASP.NET Core DI 的主要思想是简单。它在 KISS principle 之后维护了一组最少的功能。如果您需要一些额外的功能,那么 DIY 或使用实现所需功能的相应 Plungin(开放封闭原则)。

其次,通常我们需要为单个服务注入许多命名依赖项。对于 Unity,您可能必须为构造函数参数指定名称(使用 InjectionConstructor)。此注册使用反射和一些智能逻辑来猜测构造函数的参数。如果注册与构造函数参数不匹配,这也可能导致运行时错误。另一方面,在使用自己的工厂时,您可以完全控制如何提供构造函数参数。它更具可读性,并在编译时解决。再次KISS principle

第二个问题:

_serviceProvider.GetService() 如何注入适当的连接字符串?

首先,我同意您的观点,即依赖于 IOptions 之类的新事物(因此依赖于包 Microsoft.Extensions.Options.ConfigurationExtensions)并不是一个好主意。我看到一些关于 IOptions 的讨论,对于它的好处有不同的看法。同样,我尽量避免在不需要时添加新的依赖项。真的需要吗?我想不是。否则,每个实现都必须依赖它,而该实现没有任何明确的需求(对我来说,这看起来像是违反了 ISP,我也同意你的观点)。取决于工厂也是如此,但在这种情况下可以避免。

ASP.NET Core DI 为此提供了一个非常好的重载:

var mongoConnection = //...
var efConnection = //...
var otherConnection = //...
services.AddTransient<IMyFactory>(
             s => new MyFactoryImpl(
                 mongoConnection, efConnection, otherConnection, 
                 s.GetService<ISomeDependency1>(), s.GetService<ISomeDependency2>())));

嗨,对不起我的愚蠢问题,但我是关于 Microsoft.Extensions.DependencyInjection 的新手......你认为创建 3 个扩展 Iservice 的接口,如“公共接口 IServiceA:IService”而不是“公共类 ServiceA:IServiceA” ...可能是一个很好的实践选择?
@emiliano-magliocca 通常,您不应依赖于您不使用的接口(ISP),IServiceA 在您的情况下。由于您只使用来自 IService 的方法,因此您应该只依赖于 IService
@cagatay-kalan 如果遇到 OP 的问题,他可以使用 ASP.NET Core DI 轻松实现他的目标。不需要其他 DI 框架。
@EmilianoMagliocca 可以通过这种方式轻松解决:services.AddTransient<MyFirstClass>( s => new MyFirstClass(s.GetService<Escpos>())); 用于第一类,services.AddTransient<MySecondClass>( s => new MySecondClass(s.GetService<Usbpos>())); 用于第二类。
@EmilianoMagliocca 在我的示例中,“MyFirstClass”和“MySecondClass”都具有 Escpos 和 Usbpos 都实现的接口类型的相同 ctor 参数。所以上面的代码只指示 IoC 容器如何实例化“MyFirstClass”和“MySecondClass”。而已。因此,除此之外,您可能需要将一些其他接口映射到“MyFirstClass”和“MySecondClass”。这取决于您的需求,我没有在我的示例中介绍它。
S
Stefan Steiger

死灵术。我认为这里的人们正在重新发明轮子——而且很糟糕,如果我可以这么说的话......如果你想按键注册一个组件,只需使用字典:

System.Collections.Generic.Dictionary<string, IConnectionFactory> dict = 
    new System.Collections.Generic.Dictionary<string, IConnectionFactory>(
        System.StringComparer.OrdinalIgnoreCase);

dict.Add("ReadDB", new ConnectionFactory("connectionString1"));
dict.Add("WriteDB", new ConnectionFactory("connectionString2"));
dict.Add("TestDB", new ConnectionFactory("connectionString3"));
dict.Add("Analytics", new ConnectionFactory("connectionString4"));
dict.Add("LogDB", new ConnectionFactory("connectionString5"));

然后使用服务集合注册字典:

services.AddSingleton<System.Collections.Generic.Dictionary<string, IConnectionFactory>>(dict);

如果您不愿意获取字典并通过密钥访问它,您可以通过向服务集合添加额外的 key-lookup-method 来隐藏字典:(委托/闭包的使用应该给潜在的维护者一个机会了解发生了什么 - 箭头符号有点神秘)

services.AddTransient<Func<string, IConnectionFactory>>(
    delegate (IServiceProvider sp)
    {
        return
            delegate (string key)
            {
                System.Collections.Generic.Dictionary<string, IConnectionFactory> dbs = Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService
 <System.Collections.Generic.Dictionary<string, IConnectionFactory>>(sp);

                if (dbs.ContainsKey(key))
                    return dbs[key];

                throw new System.Collections.Generic.KeyNotFoundException(key); // or maybe return null, up to you
            };
    });

现在您可以使用以下任一方式访问您的类型

IConnectionFactory logDB = Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService<Func<string, IConnectionFactory>>(serviceProvider)("LogDB");
logDB.Connection

或者

System.Collections.Generic.Dictionary<string, IConnectionFactory> dbs = Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService<System.Collections.Generic.Dictionary<string, IConnectionFactory>>(serviceProvider);
dbs["logDB"].Connection

正如我们所看到的,第一个完全是多余的,因为您也可以使用字典完全做到这一点,而无需闭包和 AddTransient(如果您使用 VB,甚至大括号也不会有所不同):

IConnectionFactory logDB = Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService<System.Collections.Generic.Dictionary<string, IConnectionFactory>>(serviceProvider)["logDB"];
logDB.Connection

(越简单越好——不过你可能想用它作为扩展方法)

当然,如果你不喜欢字典,你也可以为你的界面设置一个属性 Name (或其他),然后通过键查找:

services.AddSingleton<IConnectionFactory>(new ConnectionFactory("ReadDB"));
services.AddSingleton<IConnectionFactory>(new ConnectionFactory("WriteDB"));
services.AddSingleton<IConnectionFactory>(new ConnectionFactory("TestDB"));
services.AddSingleton<IConnectionFactory>(new ConnectionFactory("Analytics"));
services.AddSingleton<IConnectionFactory>(new ConnectionFactory("LogDB"));



// https://stackoverflow.com/questions/39174989/how-to-register-multiple-implementations-of-the-same-interface-in-asp-net-core
services.AddTransient<Func<string, IConnectionFactory>>(
    delegate(IServiceProvider sp)
    {
        return
            delegate(string key)
            {
                System.Collections.Generic.IEnumerable<IConnectionFactory> svs = 
                    sp.GetServices<IConnectionFactory>();
                
                foreach (IConnectionFactory thisService in svs)
                {
                    if (key.Equals(thisService.Name, StringComparison.OrdinalIgnoreCase))
                        return thisService;
                }
    
                return null;
            };
    });

但这需要更改您的界面以适应该属性,并且遍历大量元素应该比关联数组查找(字典)慢得多。不过,很高兴知道它可以在没有字典的情况下完成。

这些只是我的 0.05 美元


如果服务已实现 IDispose,谁负责处置该服务?您已将字典注册为 Singleton
@LP13:您还可以使用委托注册字典作为值,然后您可以在 itransient 中注册它,并创建一个新实例,例如。 GetRequiredService()["logDB"]()
我用字典试过你的东西,问题是:它只为所有人打开一个连接。这就像一个静态的,任何想要执行的查询都将使用相同的连接。并且连接可能已经在使用中。
解决方案是 Dictionary> 我将把我的答案放在这篇文章的底部
T
T Brown

自从我上面的帖子以来,我已经搬到了一个通用工厂类

用法

 services.AddFactory<IProcessor, string>()
         .Add<ProcessorA>("A")
         .Add<ProcessorB>("B");

 public MyClass(IFactory<IProcessor, string> processorFactory)
 {
       var x = "A"; //some runtime variable to select which object to create
       var processor = processorFactory.Create(x);
 }

执行

public class FactoryBuilder<I, P> where I : class
{
    private readonly IServiceCollection _services;
    private readonly FactoryTypes<I, P> _factoryTypes;
    public FactoryBuilder(IServiceCollection services)
    {
        _services = services;
        _factoryTypes = new FactoryTypes<I, P>();
    }
    public FactoryBuilder<I, P> Add<T>(P p)
        where T : class, I
    {
        _factoryTypes.ServiceList.Add(p, typeof(T));

        _services.AddSingleton(_factoryTypes);
        _services.AddTransient<T>();
        return this;
    }
}
public class FactoryTypes<I, P> where I : class
{
    public Dictionary<P, Type> ServiceList { get; set; } = new Dictionary<P, Type>();
}

public interface IFactory<I, P>
{
    I Create(P p);
}

public class Factory<I, P> : IFactory<I, P> where I : class
{
    private readonly IServiceProvider _serviceProvider;
    private readonly FactoryTypes<I, P> _factoryTypes;
    public Factory(IServiceProvider serviceProvider, FactoryTypes<I, P> factoryTypes)
    {
        _serviceProvider = serviceProvider;
        _factoryTypes = factoryTypes;
    }

    public I Create(P p)
    {
        return (I)_serviceProvider.GetService(_factoryTypes.ServiceList[p]);
    }
}

扩大

namespace Microsoft.Extensions.DependencyInjection
{
    public static class DependencyExtensions
    {
        public static FactoryBuilder<I, P> AddFactory<I, P>(this IServiceCollection services)
            where I : class
        {
            services.AddTransient<IFactory<I, P>, Factory<I, P>>();
            return new FactoryBuilder<I, P>(services);
        }
    }
}

你能提供 .AddFactory() 方法扩展吗?
抱歉刚刚看到这个...添加
AddFactory 扩展需要一个委托。您的用法不起作用,因为没有。
_services.AddSingleton(_factoryTypes);我感觉这行应该在FactoryBuilder的构造函数中,否则每次调用add都会调用它。
你是对的。不知道那是从哪里来的。我已经更新了代码。
A
ArcadeRenegade

显然,您可以只注入服务接口的 IEnumerable !然后使用 LINQ 找到您想要的实例。

我的示例是针对 AWS SNS 服务的,但您实际上可以对任何注入服务执行相同操作。

启动

foreach (string snsRegion in Configuration["SNSRegions"].Split(',', StringSplitOptions.RemoveEmptyEntries))
{
    services.AddAWSService<IAmazonSimpleNotificationService>(
        string.IsNullOrEmpty(snsRegion) ? null :
        new AWSOptions()
        {
            Region = RegionEndpoint.GetBySystemName(snsRegion)
        }
    );
}

services.AddSingleton<ISNSFactory, SNSFactory>();

services.Configure<SNSConfig>(Configuration);

SNSConfig

public class SNSConfig
{
    public string SNSDefaultRegion { get; set; }
    public string SNSSMSRegion { get; set; }
}

应用设置.json

  "SNSRegions": "ap-south-1,us-west-2",
  "SNSDefaultRegion": "ap-south-1",
  "SNSSMSRegion": "us-west-2",

SNS工厂

public class SNSFactory : ISNSFactory
{
    private readonly SNSConfig _snsConfig;
    private readonly IEnumerable<IAmazonSimpleNotificationService> _snsServices;

    public SNSFactory(
        IOptions<SNSConfig> snsConfig,
        IEnumerable<IAmazonSimpleNotificationService> snsServices
        )
    {
        _snsConfig = snsConfig.Value;
        _snsServices = snsServices;
    }

    public IAmazonSimpleNotificationService ForDefault()
    {
        return GetSNS(_snsConfig.SNSDefaultRegion);
    }

    public IAmazonSimpleNotificationService ForSMS()
    {
        return GetSNS(_snsConfig.SNSSMSRegion);
    }

    private IAmazonSimpleNotificationService GetSNS(string region)
    {
        return GetSNS(RegionEndpoint.GetBySystemName(region));
    }

    private IAmazonSimpleNotificationService GetSNS(RegionEndpoint region)
    {
        IAmazonSimpleNotificationService service = _snsServices.FirstOrDefault(sns => sns.Config.RegionEndpoint == region);

        if (service == null)
        {
            throw new Exception($"No SNS service registered for region: {region}");
        }

        return service;
    }
}

public interface ISNSFactory
{
    IAmazonSimpleNotificationService ForDefault();

    IAmazonSimpleNotificationService ForSMS();
}

现在您可以在自定义服务或控制器中获取所需区域的 SNS 服务

public class SmsSender : ISmsSender
{
    private readonly IAmazonSimpleNotificationService _sns;

    public SmsSender(ISNSFactory snsFactory)
    {
        _sns = snsFactory.ForSMS();
    }

    .......
 }

public class DeviceController : Controller
{
    private readonly IAmazonSimpleNotificationService _sns;

    public DeviceController(ISNSFactory snsFactory)
    {
        _sns = snsFactory.ForDefault();
    }

     .........
}

l
littgle

我的价值解决方案......考虑切换到温莎城堡,因为不能说我喜欢上面的任何解决方案。对不起!!

public interface IStage<out T> : IStage { }

public interface IStage {
      void DoSomething();
}

创建您的各种实现

public class YourClassA : IStage<YouClassA> { 
    public void DoSomething() 
    {
        ...TODO
    }
}

public class YourClassB : IStage<YourClassB> { .....etc. }

登记

services.AddTransient<IStage<YourClassA>, YourClassA>()
services.AddTransient<IStage<YourClassB>, YourClassB>()

构造函数和实例使用...

public class Whatever
{
   private IStage ClassA { get; }

   public Whatever(IStage<YourClassA> yourClassA)
   {
         ClassA = yourClassA;
   }

   public void SomeWhateverMethod()
   {
        ClassA.DoSomething();
        .....
   }

如果你的 What class,OP 要求控制器,你会怎么做,所以我假设这是控制器,需要 15 种不同的服务?您想将它们添加到构造函数中吗?
Z
Zach Hutchins

这是一个关于如何创建依赖关系解析器的示例,它允许您指定一个通用参数来解析您的依赖关系。

var serviceProvider = new ServiceCollection()
    .AddSingleton<IPerson, Larry>()
    .AddSingleton<IPerson, Phil>()
    .AddSingleton<IDependencyResolver<IPerson, string>, PersonDependecyResolver>()
    .BuildServiceProvider();

var persons = serviceProvider.GetService<IDependencyResolver<IPerson, string>>();
Console.WriteLine(persons.GetDependency("Phil").GetName());

public interface IDependencyResolver<out TResolve, in TArg>
{
    TResolve GetDependency(TArg arg);
}

public class PersonDependecyResolver : IDependencyResolver<IPerson, string>
{
    private readonly IEnumerable<IPerson> people;

    public PersonDependecyResolver(IEnumerable<IPerson> people)
    {
        this.people = people;
    }
        
    public IPerson GetDependency(string arg)
    {
        return arg switch
        {
            "Larry" => this.people.FirstOrDefault(p => p.GetType() == typeof(Larry)),
            "Phil" => this.people.FirstOrDefault(p => p.GetType() == typeof(Phil)),
            _ => throw new Exception("Unable to resolve dependency")
        }
     
        ?? throw new Exception($"No type was found for argument {arg}");
    }
}

这是最干净的解决方案
J
Jacob Roberts

我没有时间通读它们,但似乎每个人都在为原本不应该存在的问题提供解决方案。

如果您需要所有已注册的 IService 实现,那么您就需要它们。但不要将它们全部注入 IEnumerable,然后使用逻辑根据某种类型的键选择一个。这样做的问题是您需要一个密钥,如果密钥更改,则逻辑不需要更改,即; IService 的不同实现,因此 typeof 不再起作用。

真正的问题

这里有应该在引擎服务中的业务逻辑。需要像 IServiceDecisionEngine 这样的东西。 IServiceDecisionEngine 的实现仅从 DI 获得所需的 IService 实现。喜欢

public class ServiceDecisionEngine<SomeData>: IServiceDecisionEngine<T> 
{
    public ServiceDecisionEngine(IService serviceA, IService serviceB) { }

    public IService ResolveService(SomeData dataNeededForLogic)
    {
        if (dataNeededForLogic.someValue == true) 
        { 
            return serviceA;
        } 
        return serviceB;
    }
}

现在在您的 DI 中,您可以执行 .AddScoped<IServiceDecisionEngine<SomeData>, new ServiceDecisionEngine(new ServiceA(), new ServiceB()),需要 IService 的 managerService 将通过注入和使用 IServiceDecisionEngine 来获得它。


J
Javier

我认为下面文章“Resolución dinámica de tipos en tiempo de ejecución en el contenedor de IoC de .NET Core”中描述的解决方案更简单,不需要工厂。

您可以使用通用接口

public interface IService<T> where T : class {}

然后在 IoC 容器上注册所需的类型:

services.AddTransient<IService<ServiceA>, ServiceA>();
services.AddTransient<IService<ServiceB>, ServiceB>();

之后,您必须按如下方式声明依赖项:

private readonly IService<ServiceA> _serviceA;
private readonly IService<ServiceB> _serviceB;

public WindowManager(IService<ServiceA> serviceA, IService<ServiceB> serviceB)
{
    this._serviceA = serviceA ?? throw new ArgumentNullException(nameof(serviceA));
    this._serviceB = serviceB ?? throw new ArgumentNullException(nameof(ServiceB));
}

这是完美的解决方案
E
Elias Bobadilla

我遇到了同样的问题,我使用 <T> 解决了

我的界面:

public interface IProvider<T>
{
    Task<string> GetTotalSearchResults(string searchValue);
}

我的服务配置:

var host = Host.CreateDefaultBuilder()
                .ConfigureServices((_, services) =>
                {
                    services.AddSingleton(googleSettings);
                    services.AddSingleton(bingSettings);
                    services.AddSingleton<IProvider<BingProvider>, BingProvider>();
                    services.AddSingleton<IProvider<GoogleProvider>, GoogleProvider>();
                    services.AddSingleton<ISearchManager, SearchManager>();
                });

您可以在课堂上使用它:

public class SearchManager : ISearchManager
    {
        private readonly IProvider<BingProvider> _bing;
        private readonly IProvider<GoogleProvider> _google;

        public SearchManager(IProvider<BingProvider> bing, IProvider<GoogleProvider> google)
        {
            _bing = bing;
            _google = google;
        }

缺点是这需要在您需要的任何地方指定具体类型,而不是在一个地方。
A
Assil

虽然@Miguel A. Arilla 似乎已经明确指出了这一点并且我投票支持他,但我在他有用的解决方案之上创建了另一个看起来很整洁但需要更多工作的解决方案。

这绝对取决于上述解决方案。所以基本上我创建了类似于 Func<string, IService>> 的东西,并将其称为 IServiceAccessor 作为接口,然后我必须向 IServiceCollection 添加一些扩展,如下所示:

public static IServiceCollection AddSingleton<TService, TImplementation, TServiceAccessor>(
            this IServiceCollection services,
            string instanceName
        )
            where TService : class
            where TImplementation : class, TService
            where TServiceAccessor : class, IServiceAccessor<TService>
        {
            services.AddSingleton<TService, TImplementation>();
            services.AddSingleton<TServiceAccessor>();
            var provider = services.BuildServiceProvider();
            var implementationInstance = provider.GetServices<TService>().Last();
            var accessor = provider.GetServices<TServiceAccessor>().First();

            var serviceDescriptors = services.Where(d => d.ServiceType == typeof(TServiceAccessor));
            while (serviceDescriptors.Any())
            {
                services.Remove(serviceDescriptors.First());
            }

            accessor.SetService(implementationInstance, instanceName);
            services.AddSingleton<TServiceAccessor>(prvd => accessor);
            return services;
        }

服务访问器看起来像:

 public interface IServiceAccessor<TService>
    {
         void Register(TService service,string name);
         TService Resolve(string name);

    }

最终结果,您将能够像我们过去对其他容器一样使用名称或命名实例注册服务..例如:

    services.AddSingleton<IEncryptionService, SymmetricEncryptionService, EncyptionServiceAccessor>("Symmetric");
    services.AddSingleton<IEncryptionService, AsymmetricEncryptionService, EncyptionServiceAccessor>("Asymmetric");

现在这已经足够了,但是为了使您的工作完整,最好添加更多的扩展方法,以覆盖所有类型的注册,遵循相同的方法。

stackoverflow 上有另一篇文章,但我找不到,发帖者详细解释了为什么不支持此功能以及如何解决它,与@Miguel 所说的基本相似。尽管我不同意每一点,但这篇文章还是不错的,因为我认为在某些情况下您确实需要命名实例。再次找到该链接后,我将在此处发布该链接。

事实上,您不需要传递该 Selector 或 Accessor:

我在我的项目中使用以下代码,到目前为止它运行良好。

 /// <summary>
    /// Adds the singleton.
    /// </summary>
    /// <typeparam name="TService">The type of the t service.</typeparam>
    /// <typeparam name="TImplementation">The type of the t implementation.</typeparam>
    /// <param name="services">The services.</param>
    /// <param name="instanceName">Name of the instance.</param>
    /// <returns>IServiceCollection.</returns>
    public static IServiceCollection AddSingleton<TService, TImplementation>(
        this IServiceCollection services,
        string instanceName
    )
        where TService : class
        where TImplementation : class, TService
    {
        var provider = services.BuildServiceProvider();
        var implementationInstance = provider.GetServices<TService>().LastOrDefault();
        if (implementationInstance.IsNull())
        {
            services.AddSingleton<TService, TImplementation>();
            provider = services.BuildServiceProvider();
            implementationInstance = provider.GetServices<TService>().Single();
        }
        return services.RegisterInternal(instanceName, provider, implementationInstance);
    }

    private static IServiceCollection RegisterInternal<TService>(this IServiceCollection services,
        string instanceName, ServiceProvider provider, TService implementationInstance)
        where TService : class
    {
        var accessor = provider.GetServices<IServiceAccessor<TService>>().LastOrDefault();
        if (accessor.IsNull())
        {
            services.AddSingleton<ServiceAccessor<TService>>();
            provider = services.BuildServiceProvider();
            accessor = provider.GetServices<ServiceAccessor<TService>>().Single();
        }
        else
        {
            var serviceDescriptors = services.Where(d => d.ServiceType == typeof(IServiceAccessor<TService>));
            while (serviceDescriptors.Any())
            {
                services.Remove(serviceDescriptors.First());
            }
        }
        accessor.Register(implementationInstance, instanceName);
        services.AddSingleton<TService>(prvd => implementationInstance);
        services.AddSingleton<IServiceAccessor<TService>>(prvd => accessor);
        return services;
    }

    //
    // Summary:
    //     Adds a singleton service of the type specified in TService with an instance specified
    //     in implementationInstance to the specified Microsoft.Extensions.DependencyInjection.IServiceCollection.
    //
    // Parameters:
    //   services:
    //     The Microsoft.Extensions.DependencyInjection.IServiceCollection to add the service
    //     to.
    //   implementationInstance:
    //     The instance of the service.
    //   instanceName:
    //     The name of the instance.
    //
    // Returns:
    //     A reference to this instance after the operation has completed.
    public static IServiceCollection AddSingleton<TService>(
        this IServiceCollection services,
        TService implementationInstance,
        string instanceName) where TService : class
    {
        var provider = services.BuildServiceProvider();
        return RegisterInternal(services, instanceName, provider, implementationInstance);
    }

    /// <summary>
    /// Registers an interface for a class
    /// </summary>
    /// <typeparam name="TInterface">The type of the t interface.</typeparam>
    /// <param name="services">The services.</param>
    /// <returns>IServiceCollection.</returns>
    public static IServiceCollection As<TInterface>(this IServiceCollection services)
         where TInterface : class
    {
        var descriptor = services.Where(d => d.ServiceType.GetInterface(typeof(TInterface).Name) != null).FirstOrDefault();
        if (descriptor.IsNotNull())
        {
            var provider = services.BuildServiceProvider();
            var implementationInstance = (TInterface)provider?.GetServices(descriptor?.ServiceType)?.Last();
            services?.AddSingleton(implementationInstance);
        }
        return services;
    }

这有助于解决我在服务访问器中丢失类型注册的问题。诀窍是删除服务访问器的所有绑定,然后再次添加它!
M
Martijn Pieters

我为此创建了一个库,它实现了一些不错的功能。代码可在 GitHub 上找到:https://github.com/dazinator/Dazinator.Extensions.DependencyInjection NuGet:https://www.nuget.org/packages/Dazinator.Extensions.DependencyInjection/

用法很简单:

将 Dazinator.Extensions.DependencyInjection nuget 包添加到您的项目中。添加您的命名服务注册。

    var services = new ServiceCollection();
    services.AddNamed<AnimalService>(names =>
    {
        names.AddSingleton("A"); // will resolve to a singleton instance of AnimalService
        names.AddSingleton<BearService>("B"); // will resolve to a singleton instance of BearService (which derives from AnimalService)
        names.AddSingleton("C", new BearService()); will resolve to singleton instance provided yourself.
        names.AddSingleton("D", new DisposableTigerService(), registrationOwnsInstance = true); // will resolve to singleton instance provided yourself, but will be disposed for you (if it implements IDisposable) when this registry is disposed (also a singleton).

        names.AddTransient("E"); // new AnimalService() every time..
        names.AddTransient<LionService>("F"); // new LionService() every time..

        names.AddScoped("G");  // scoped AnimalService
        names.AddScoped<DisposableTigerService>("H");  scoped DisposableTigerService and as it implements IDisposable, will be disposed of when scope is disposed of.

    });


在上面的示例中,请注意,对于每个命名注册,您还指定了生命周期或 Singleton、Scoped 或 Transient。

您可以通过以下两种方式之一来解析服务,具体取决于您是否愿意让您的服务依赖此包,而不是:

public MyController(Func<string, AnimalService> namedServices)
{
   AnimalService serviceA = namedServices("A");
   AnimalService serviceB = namedServices("B"); // BearService derives from AnimalService
}

或者

public MyController(NamedServiceResolver<AnimalService> namedServices)
{
   AnimalService serviceA = namedServices["A"];
   AnimalService serviceB = namedServices["B"]; // instance of BearService returned derives from AnimalService
}

我专门设计了这个库来很好地与 Microsoft.Extensions.DependencyInjection 配合使用 - 例如:

当您注册命名服务时,您注册的任何类型都可以具有带参数的构造函数——它们将通过 DI 得到满足,就像 AddTransient<>、AddScoped<> 和 AddSingleton<> 方法通常工作一样。对于瞬态和作用域命名服务,注册中心构建一个 ObjectFactory,以便它可以在需要时非常快速地激活该类型的新实例。这比其他方法快得多,并且符合 Microsoft.Extensions.DependencyInjection 的处理方式。


b
birdamongmen

我知道这篇文章已经有几年的历史了,但我一直遇到这个问题,我对服务定位器模式不满意。

另外,我知道 OP 正在寻找一种实现,它允许您基于字符串选择具体的实现。我也意识到 OP 专门要求实现相同的接口。我将要描述的解决方案依赖于向你的接口添加一个泛型类型参数。问题是除了服务集合绑定之外,类型参数没有任何实际用途。我将尝试描述可能需要这样的情况。

想象一下 appsettings.json 中这种场景的配置,它可能看起来像这样(这只是为了演示,只要您有更正的配置提供程序,您的配置就可以来自您想要的任何地方):

{
  "sqlDataSource": {
    "connectionString": "Data Source=localhost; Initial catalog=Foo; Connection Timeout=5; Encrypt=True;",
    "username": "foo",
    "password": "this normally comes from a secure source, but putting here for demonstration purposes"
  },
  "mongoDataSource": {
    "hostName": "uw1-mngo01-cl08.company.net",
    "port": 27026,
    "collection": "foo"
  }
}

您确实需要一个代表每个配置选项的类型:

public class SqlDataSource
{
  public string ConnectionString { get;set; }
  public string Username { get;set; }
  public string Password { get;set; }
}

public class MongoDataSource
{
  public string HostName { get;set; }
  public string Port { get;set; }
  public string Collection { get;set; }
}

现在,我知道拥有相同接口的两个实现似乎有点做作,但我肯定在不止一种情况下见过它。我经常遇到的有:

从一个数据存储迁移到另一个数据存储时,能够使用相同的接口实现相同的逻辑操作非常有用,这样您就无需更改调用代码。这还允许您添加在运行时在不同实现之间交换的配置(这对于回滚很有用)。使用装饰器模式时。您可能使用该模式的原因是您希望在不更改接口的情况下添加功能并在某些情况下回退到现有功能(我在将缓存添加到存储库类时使用它,因为我想要围绕连接的类似断路器的逻辑到回退到基本存储库的缓存——这给了我在缓存可用时的最佳行为,但在缓存不可用时仍然有效的行为)。

无论如何,您可以通过向服务接口添加类型参数来引用它们,以便您可以实现不同的实现:

public interface IService<T> {
  void DoServiceOperation();
}

public class MongoService : IService<MongoDataSource> {
  private readonly MongoDataSource _options;

  public FooService(IOptionsMonitor<MongoDataSource> serviceOptions){
    _options = serviceOptions.CurrentValue
  }

  void DoServiceOperation(){
    //do something with your mongo data source options (connect to database)
    throw new NotImplementedException();
  }
}

public class SqlService : IService<SqlDataSource> {
  private readonly SqlDataSource_options;

  public SqlService (IOptionsMonitor<SqlDataSource> serviceOptions){
    _options = serviceOptions.CurrentValue
  }

  void DoServiceOperation(){
    //do something with your sql data source options (connect to database)
    throw new NotImplementedException();
  }
}

在启动时,您将使用以下代码注册这些:

services.Configure<SqlDataSource>(configurationSection.GetSection("sqlDataSource"));
services.Configure<MongoDataSource>(configurationSection.GetSection("mongoDataSource"));

services.AddTransient<IService<SqlDataSource>, SqlService>();
services.AddTransient<IService<MongoDataSource>, MongoService>();

最后,在依赖于具有不同连接的 Service 的类中,您只需依赖所需的服务,DI 框架将处理其余的:

[Route("api/v1)]
[ApiController]
public class ControllerWhichNeedsMongoService {  
  private readonly IService<MongoDataSource> _mongoService;
  private readonly IService<SqlDataSource> _sqlService ;

  public class ControllerWhichNeedsMongoService(
    IService<MongoDataSource> mongoService, 
    IService<SqlDataSource> sqlService
  )
  {
    _mongoService = mongoService;
    _sqlService = sqlService;
  }

  [HttpGet]
  [Route("demo")]
  public async Task GetStuff()
  {
    if(useMongo)
    {
       await _mongoService.DoServiceOperation();
    }
    await _sqlService.DoServiceOperation();
  }
}

这些实现甚至可以相互依赖。另一个很大的好处是您可以获得编译时绑定,因此任何重构工具都可以正常工作。

希望这对将来的某人有所帮助。


J
Jake Walden

模块化扩展类解决方案

答案很晚,但这是我这样做的方式,与这个问题的其他一些解决方案相比,它具有一些优势。

优点:

每个服务实现注册只需 1 行代码,注册方法不需要额外的逻辑

密钥服务不需要在同一时间和/或地点全部注册。如果需要,甚至可以在不同的项目中进行注册,只要密钥是唯一的。这允许完全模块化地添加新的实现。

服务实例化是惰性的(+ 线程安全),因此当只使用一个或几个时,不会不必要地激活所有实现。

不依赖代码中的任何外部委托或类型,默认情况下,服务作为普通 Func 注入,但如果您愿意,可以轻松注册自定义委托或类型

在工厂的瞬态、单例或范围注册之间轻松选择

使用您喜欢的任何键类型(我强烈建议您只使用具有内置有效相等比较的简单类型,例如 int、string、enum 或 bool,因为为什么要让生活变得比需要的更复杂)

配置示例:

public IServiceProvider ConfigureServices(IServiceCollection services)
{
    // default instantiation:
    services.AddKeyedService<IService, ImplementationA, string>("A", ServiceLifetime.Scoped);

    // using an implementation factory to pass a connection string to the constructor:
    services.AddKeyedService<IService, ImplementationB, string>("B", x => {
        var connectionString = ConfigurationManager.ConnectionStrings["mongo"].ConnectionString;
        return new ImplementationB(connectionString);
    }, ServiceLifetime.Scoped);

    // using a custom delegate instead of Func<TKey, TService>
    services.AddKeyedService<IService, ImplementationC, string, StringKeyedService>(
        "C", (_, x) => new StringKeyedService(x), ServiceLifetime.Singleton);

    return services.BuildServiceProvider();
}

public delegate IService StringKeyedService(string key);

使用示例:

public ExampleClass(Func<string, IService> keyedServiceFactory, StringKeyedService<IService> keyedServiceDelegate)
{
    var serviceKey = Configuration.GetValue<string>("IService.Key");
    var service = keyedServiceFactory(serviceKey);
    var serviceC = keyedServiceDelegate("C");
}

执行:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Microsoft.Extensions.DependencyInjection;

public static class KeyedServiceExtensions
{
    // Use this to register TImplementation as TService, injectable as Func<TKey, TService>.
    // Uses default instance activator.
    public static IServiceCollection AddKeyedService<TService, TImplementation, TKey>(this IServiceCollection services, TKey key, ServiceLifetime serviceLifetime)
        where TService : class
        where TImplementation : class, TService
    {
        services.AddTransient<TImplementation>();

        var keyedServiceBuilder = services.CreateOrUpdateKeyedServiceBuilder<TKey, TService, Func<TKey, TService>>(
            DefaultImplementationFactory<TKey, TService>, serviceLifetime);
        keyedServiceBuilder.Add<TImplementation>(key);

        return services;
    }

    // Use this to register TImplementation as TService, injectable as Func<TKey, TService>.
    // Uses implementationFactory to create instances
    public static IServiceCollection AddKeyedService<TService, TImplementation, TKey>(this IServiceCollection services, TKey key,
        Func<IServiceProvider, TImplementation> implementationFactory, ServiceLifetime serviceLifetime)
        where TService : class
        where TImplementation : class, TService
    {
        services.AddTransient(implementationFactory);

        var keyedServiceBuilder = services.CreateOrUpdateKeyedServiceBuilder<TKey, TService, Func<TKey, TService>>(
            DefaultImplementationFactory<TKey, TService>, serviceLifetime);
        keyedServiceBuilder.Add<TImplementation>(key);

        return services;
    }

    // Use this to register TImplementation as TService, injectable as TInjection.
    // Uses default instance activator.
    public static IServiceCollection AddKeyedService<TService, TImplementation, TKey, TInjection>(this IServiceCollection services, TKey key,
        Func<IServiceProvider, Func<TKey, TService>, TInjection> serviceFactory, ServiceLifetime serviceLifetime)
        where TService : class
        where TImplementation : class, TService
        where TInjection : class
    {
        services.AddTransient<TImplementation>();

        var keyedServiceBuilder = services.CreateOrUpdateKeyedServiceBuilder<TKey, TService, TInjection>(
            x => serviceFactory(x, DefaultImplementationFactory<TKey, TService>(x)), serviceLifetime);
        keyedServiceBuilder.Add<TImplementation>(key);

        return services;
    }

    // Use this to register TImplementation as TService, injectable as TInjection.
    // Uses implementationFactory to create instances
    public static IServiceCollection AddKeyedService<TService, TImplementation, TKey, TInjection>(this IServiceCollection services, TKey key,
        Func<IServiceProvider, TImplementation> implementationFactory, Func<IServiceProvider, Func<TKey, TService>, TInjection> serviceFactory, ServiceLifetime serviceLifetime)
        where TService : class
        where TImplementation : class, TService
        where TInjection : class
    {
        services.AddTransient(implementationFactory);

        var keyedServiceBuilder = services.CreateOrUpdateKeyedServiceBuilder<TKey, TService, TInjection>(
            x => serviceFactory(x, DefaultImplementationFactory<TKey, TService>(x)), serviceLifetime);
        keyedServiceBuilder.Add<TImplementation>(key);

        return services;
    }

    private static KeyedServiceBuilder<TKey, TService> CreateOrUpdateKeyedServiceBuilder<TKey, TService, TInjection>(this IServiceCollection services,
        Func<IServiceProvider, TInjection> serviceFactory, ServiceLifetime serviceLifetime)
        where TService : class
        where TInjection : class
    {
        var builderServiceDescription = services.SingleOrDefault(x => x.ServiceType == typeof(KeyedServiceBuilder<TKey, TService>));
        KeyedServiceBuilder<TKey, TService> keyedServiceBuilder;
        if (builderServiceDescription is null)
        {
            keyedServiceBuilder = new KeyedServiceBuilder<TKey, TService>();
            services.AddSingleton(keyedServiceBuilder);

            switch (serviceLifetime)
            {
                case ServiceLifetime.Singleton:
                    services.AddSingleton(serviceFactory);
                    break;
                case ServiceLifetime.Scoped:
                    services.AddScoped(serviceFactory);
                    break;
                case ServiceLifetime.Transient:
                    services.AddTransient(serviceFactory);
                    break;
                default:
                    throw new ArgumentOutOfRangeException(nameof(serviceLifetime), serviceLifetime, "Invalid value for " + nameof(serviceLifetime));
            }
        }
        else
        {
            CheckLifetime<KeyedServiceBuilder<TKey, TService>>(builderServiceDescription.Lifetime, ServiceLifetime.Singleton);

            var factoryServiceDescriptor = services.SingleOrDefault(x => x.ServiceType == typeof(TInjection));
            CheckLifetime<TInjection>(factoryServiceDescriptor.Lifetime, serviceLifetime);

            keyedServiceBuilder = (KeyedServiceBuilder<TKey, TService>)builderServiceDescription.ImplementationInstance;
        }

        return keyedServiceBuilder;

        static void CheckLifetime<T>(ServiceLifetime actual, ServiceLifetime expected)
        {
            if (actual != expected)
                throw new ApplicationException($"{typeof(T).FullName} is already registered with a different ServiceLifetime. Expected: '{expected}', Actual: '{actual}'");
        }
    }

    private static Func<TKey, TService> DefaultImplementationFactory<TKey, TService>(IServiceProvider x) where TService : class
        => x.GetRequiredService<KeyedServiceBuilder<TKey, TService>>().Build(x);

    private sealed class KeyedServiceBuilder<TKey, TService>
    {
        private readonly Dictionary<TKey, Type> _serviceImplementationTypes = new Dictionary<TKey, Type>();

        internal void Add<TImplementation>(TKey key) where TImplementation : class, TService
        {
            if (_serviceImplementationTypes.TryGetValue(key, out var type) && type == typeof(TImplementation))
                return; //this type is already registered under this key

            _serviceImplementationTypes[key] = typeof(TImplementation);
        }

        internal Func<TKey, TService> Build(IServiceProvider serviceProvider)
        {
            var serviceTypeDictionary = _serviceImplementationTypes.Values.Distinct()
                .ToDictionary(
                    type => type,
                    type => new Lazy<TService>(
                        () => (TService)serviceProvider.GetRequiredService(type),
                        LazyThreadSafetyMode.ExecutionAndPublication
                    )
                );
            var serviceDictionary = _serviceImplementationTypes
                .ToDictionary(kvp => kvp.Key, kvp => serviceTypeDictionary[kvp.Value]);

            return key => serviceDictionary[key].Value;
        }
    }
}

也可以在此之上制作一个流畅的界面,如果有兴趣请告诉我。

流体使用示例:

var keyedService = services.KeyedSingleton<IService, ServiceKey>()
    .As<ICustomKeyedService<TKey, IService>>((_, x) => new CustomKeyedServiceInterface<ServiceKey, IService>(x));
keyedService.Key(ServiceKey.A).Add<ServiceA>();
keyedService.Key(ServiceKey.B).Add(x => {
    x.GetService<ILogger>.LogDebug("Instantiating ServiceB");
    return new ServiceB();
});

A
Ash

使用 IEnumerable<Interface> 的任何技术方式都有效地违背了 DI 的全部目的,因为您需要选择需要解决的实现,并且可能指向糟糕的设计。

对我有用的这个问题的解决方法是分开使用并创建单独的接口,像这样

public interface ICRUDService<T> where T : class
{
    void CreateAndSetId(T item);
    void Delete(int id);
    ActionResult<List<T>> GetAll();
    ActionResult<T> GetById(int id);
    void Update(int id, T item);
}

然后是各个接口

public interface ITodoService : ICRUDService<Todo> {}
public interface IValuesService : ICRUDService<Values> {}

以及他们的实现

public class TodoService : ITodoService { ... }
public class ValuesService : IValuesService { ... }

启动.ConfigureServices

services.AddScoped<ITodoService, TodoService>();
services.AddScoped<IValuesService, ValuesService>();

用法

public class UsageClass {
 public UsageClass(ITodoService todoService, IValuesService valuesService) {}
}

如果您仍然对解决多个实现感兴趣,THIS 是 Microsoft 的建议。只是在这里链接它,因为这不是我推荐的。


A
Arthur Zennig

我发现的用于多种实现的最佳文档/教程来自以下来源:.NET Core Dependency Injection - One Interface, Multiple Implementations, (Authored by Akshay Patel)

教程中提到的示例遵循 Controller/Service/Repository 约定,带有 Func 在 Startup.cs 的 ConfigurationService() 中实现以实例化正确/需要的接口实现;教程是我发现澄清这个问题的最佳配方。

下面,对上述文章的粗略复制/粘贴:(示例处理购物车接口的 3 种不同实现,一种使用缓存解决方案的方法,另一种使用 API 和其他使用 DB 的实现。)接口是多个实现... .

namespace MultipleImplementation  
{  
    public interface IShoppingCart  
    {  
        object GetCart();  
    }  
}  

实施A

namespace MultipleImplementation  
{  
    public class ShoppingCartCache : IShoppingCart  
    {  
        public object GetCart()  
        {  
            return "Cart loaded from cache.";  
        }  
    }  
}  

实施 B

namespace MultipleImplementation  
{  
    public class ShoppingCartDB : IShoppingCart  
    {  
        public object GetCart()  
        {  
            return "Cart loaded from DB";  
        }  
    }  
}  

实施 C

namespace MultipleImplementation  
{  
    public class ShoppingCartAPI : IShoppingCart  
    {  
        public object GetCart()  
        {  
            return "Cart loaded through API.";  
        }  
    }  
}  

将使用存储库中的接口声明来选择 A,B,C....

namespace MultipleImplementation  
{  
    public interface IShoppingCartRepository  
    {  
        object GetCart();  
    }  
}

枚举以选择将使用哪个实现...

namespace MultipleImplementation  
{  
    public class Constants  
    {  
    }  
  
    public enum CartSource  
    {  
        Cache=1,  
        DB=2,  
        API=3  
    }  
}  

声明的存储库接口的实现(谁将选择哪个实现......)

using System;  
  
namespace MultipleImplementation  
{  
    public class ShoppingCartRepository : IShoppingCartRepository  
    {  
        private readonly Func<string, IShoppingCart> shoppingCart;  
        public ShoppingCartRepository(Func<string, IShoppingCart> shoppingCart)  
        {  
            this.shoppingCart = shoppingCart;  
        }  
  
        public object GetCart()  
        {  
            return shoppingCart(CartSource.DB.ToString()).GetCart();  
        }  
    }  
}  

最后,将所有内容打包到 startup.cs 文件中,在 ConfigureService 方法中

public void ConfigureServices(IServiceCollection services)  
        {  
  
            services.AddScoped<IShoppingCartRepository, ShoppingCartRepository>();  
  
            services.AddSingleton<ShoppingCartCache>();  
            services.AddSingleton<ShoppingCartDB>();  
            services.AddSingleton<ShoppingCartAPI>();  
  
            services.AddTransient<Func<string, IShoppingCart>>(serviceProvider => key =>  
            {  
                switch (key)  
                {  
                    case "API":  
                        return serviceProvider.GetService<ShoppingCartAPI>();  
                    case "DB":  
                        return serviceProvider.GetService<ShoppingCartDB>();  
                    default:  
                        return serviceProvider.GetService<ShoppingCartCache>();  
                }  
            });  
  
            services.AddMvc();  
        }  

在那里,我强调,6 分钟的阅读将帮助您在一个界面中解决多个实现。祝你好运!


v
vrluckyin

扩展@rneverdies 的解决方案。除了 ToString(),还可以使用以下选项 - 1) 使用公共属性实现,2) @Craig Brunetti 建议的服务服务。

public interface IService { }
public class ServiceA : IService
{
    public override string ToString()
    {
        return "A";
    }
}

public class ServiceB : IService
{
    public override string ToString()
    {
        return "B";
    }

}

/// <summary>
/// extension method that compares with ToString value of an object and returns an object if found
/// </summary>
public static class ServiceProviderServiceExtensions
{
    public static T GetService<T>(this IServiceProvider provider, string identifier)
    {
        var services = provider.GetServices<T>();
        var service = services.FirstOrDefault(o => o.ToString() == identifier);
        return service;
    }
}

public void ConfigureServices(IServiceCollection services)
{
    //Initials configurations....

    services.AddSingleton<IService, ServiceA>();
    services.AddSingleton<IService, ServiceB>();
    services.AddSingleton<IService, ServiceC>();

    var sp = services.BuildServiceProvider();
    var a = sp.GetService<IService>("A"); //returns instance of ServiceA
    var b = sp.GetService<IService>("B"); //returns instance of ServiceB

    //Remaining configurations....
}

C
Ciarán Bruen

在阅读了这里的答案和其他地方的文章之后,我能够让它在没有字符串的情况下工作。当您有同一个接口的多个实现时,DI 会将它们添加到一个集合中,因此可以使用 typeof 从集合中检索您想要的版本。

// In Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped(IService, ServiceA);
    services.AddScoped(IService, ServiceB);
    services.AddScoped(IService, ServiceC);
}

// Any class that uses the service(s)
public class Consumer
{
    private readonly IEnumerable<IService> _myServices;

    public Consumer(IEnumerable<IService> myServices)
    {
        _myServices = myServices;
    }

    public UseServiceA()
    {
        var serviceA = _myServices.FirstOrDefault(t => t.GetType() == typeof(ServiceA));
        serviceA.DoTheThing();
    }

    public UseServiceB()
    {
        var serviceB = _myServices.FirstOrDefault(t => t.GetType() == typeof(ServiceB));
        serviceB.DoTheThing();
    }

    public UseServiceC()
    {
        var serviceC = _myServices.FirstOrDefault(t => t.GetType() == typeof(ServiceC));
        serviceC.DoTheThing();
    }
}

违背了 IoC 的目的。您不妨只写:var serviceA = new ServiceA();
@JamesCurran 如果 ServiceA 具有依赖关系,或者您想对类进行单元测试,则不会。
当您处理单例或想要获取范围实例时,这很有用。
如果您有 150 多个服务怎么办?
m
marc_s

我在 IServiceCollection 使用的 WithName 扩展上创建了自己的扩展:

public static IServiceCollection AddScopedWithName<TService, TImplementation>(this IServiceCollection services, string serviceName)
        where TService : class
        where TImplementation : class, TService
    {
        Type serviceType = typeof(TService);
        Type implementationServiceType = typeof(TImplementation);
        ServiceCollectionTypeMapper.Instance.AddDefinition(serviceType.Name, serviceName, implementationServiceType.AssemblyQualifiedName);
        services.AddScoped<TImplementation>();
        return services;
    }

ServiceCollectionTypeMapper 是映射 IService > 的单例实例NameOfService > Implementation 一个接口可以有许多不同名称的实现,这允许注册类型而不是我们在需要时可以解析的类型,并且是与解析多个服务以选择我们想要的内容不同的方法。

 /// <summary>
/// Allows to set the service register mapping.
/// </summary>
public class ServiceCollectionTypeMapper
{
    private ServiceCollectionTypeMapper()
    {
        this.ServiceRegister = new Dictionary<string, Dictionary<string, string>>();
    }

    /// <summary>
    /// Gets the instance of mapper.
    /// </summary>
    public static ServiceCollectionTypeMapper Instance { get; } = new ServiceCollectionTypeMapper();

    private Dictionary<string, Dictionary<string, string>> ServiceRegister { get; set; }

    /// <summary>
    /// Adds new service definition.
    /// </summary>
    /// <param name="typeName">The name of the TService.</param>
    /// <param name="serviceName">The TImplementation name.</param>
    /// <param name="namespaceFullName">The TImplementation AssemblyQualifiedName.</param>
    public void AddDefinition(string typeName, string serviceName, string namespaceFullName)
    {
        if (this.ServiceRegister.TryGetValue(typeName, out Dictionary<string, string> services))
        {
            if (services.TryGetValue(serviceName, out _))
            {
                throw new InvalidOperationException($"Exists an implementation with the same name [{serviceName}] to the type [{typeName}].");
            }
            else
            {
                services.Add(serviceName, namespaceFullName);
            }
        }
        else
        {
            Dictionary<string, string> serviceCollection = new Dictionary<string, string>
            {
                { serviceName, namespaceFullName },
            };
            this.ServiceRegister.Add(typeName, serviceCollection);
        }
    }

    /// <summary>
    /// Get AssemblyQualifiedName of implementation.
    /// </summary>
    /// <typeparam name="TService">The type of the service implementation.</typeparam>
    /// <param name="serviceName">The name of the service.</param>
    /// <returns>The AssemblyQualifiedName of the inplementation service.</returns>
    public string GetService<TService>(string serviceName)
    {
        Type serviceType = typeof(TService);

        if (this.ServiceRegister.TryGetValue(serviceType.Name, out Dictionary<string, string> services))
        {
            if (services.TryGetValue(serviceName, out string serviceImplementation))
            {
                return serviceImplementation;
            }
            else
            {
                return null;
            }
        }
        else
        {
            return null;
        }
    }

要注册新服务:

services.AddScopedWithName<IService, MyService>("Name");

要解析服务,我们需要像这样对 IServiceProvider 进行扩展。

/// <summary>
    /// Gets the implementation of service by name.
    /// </summary>
    /// <typeparam name="T">The type of service.</typeparam>
    /// <param name="serviceProvider">The service provider.</param>
    /// <param name="serviceName">The service name.</param>
    /// <returns>The implementation of service.</returns>
    public static T GetService<T>(this IServiceProvider serviceProvider, string serviceName)
    {
        string fullnameImplementation = ServiceCollectionTypeMapper.Instance.GetService<T>(serviceName);
        if (fullnameImplementation == null)
        {
            throw new InvalidOperationException($"Unable to resolve service of type [{typeof(T)}] with name [{serviceName}]");
        }
        else
        {
            return (T)serviceProvider.GetService(Type.GetType(fullnameImplementation));
        }
    }

解决时:

serviceProvider.GetService<IWithdrawalHandler>(serviceName);

请记住,serviceProvider 可以作为 IServiceProvider 注入到我们应用程序的构造函数中。

我希望这有帮助。


G
G Clovs

好的,这是使用字典的清晰易读的答案

使用您的数据库键名创建一个枚举

public enum Database
    {
        Red,
        Blue
    }

在 Startup.cs 中,创建一个打开新 SqlConnection 的函数字典,然后将依赖字典注入为 Singleton

Dictionary<Database, Func<IDbConnection>> connectionFactory = new()
   {
      { Database.Red, () => new SqlConnection(Configuration.GetConnectionString("RedDatabase")) },
      { Database.Blue, () => new SqlConnection(Configuration.GetConnectionString("BlueDatabase")) }
   };
services.AddSingleton(connectionFactory);

在您可以像这样获得实例 od 对对象构造函数的依赖之后:

public class ObjectQueries
{
   private readonly IDbConnection _redConnection;
   private readonly IDbConnection _blueConnection;

   public ObjectQueries(Dictionary<Database, Func<IDbConnection>> connectionFactory)
   {
      _redConnection = connectionFactory[Database.Red]();
      _blueConnection = connectionFactory[Database.Blue]();
   }
}

感谢@Stefan Steiger 的想法;)


A
Andrew Stakhov

虽然开箱即用的实现不提供它,但这里有一个示例项目,允许您注册命名实例,然后将 INamedServiceFactory 注入代码并按名称提取实例。与此处的其他工厂解决方案不同,它将允许您注册相同实现但配置不同的多个实例

https://github.com/macsux/DotNetDINamedInstances


C
Craig Brunetti

服务的服务怎么样?

如果我们有一个 INamedService 接口(带有 .Name 属性),我们可以为 .GetService(string name) 编写一个 IServiceCollection 扩展,其中扩展将采用该字符串参数,并对自身执行一个 .GetServices(),并在每个返回实例,查找其 INamedService.Name 与给定名称匹配的实例。

像这样:

public interface INamedService
{
    string Name { get; }
}

public static T GetService<T>(this IServiceProvider provider, string serviceName)
    where T : INamedService
{
    var candidates = provider.GetServices<T>();
    return candidates.FirstOrDefault(s => s.Name == serviceName);
}

因此,您的 IMyService 必须实现 INamedService,但您将获得所需的基于键的解析,对吗?

公平地说,甚至必须有这个 INamedService 接口似乎很难看,但如果你想走得更远,让事情变得更优雅,那么可以通过这里的代码找到实现/类上的 [NamedServiceAttribute("A")]扩展名,它也可以正常工作。更公平地说,反射很慢,因此可能需要进行优化,但老实说,这是 DI 引擎应该一直在帮助的。速度和简单性都是 TCO 的重要贡献者。

总而言之,不需要显式工厂,因为“找到命名服务”是一个可重用的概念,工厂类不能作为解决方案进行扩展。 Func<> 看起来不错,但是 switch 块太麻烦了,再说一次,你写 Funcs 的频率和你写工厂的频率一样高。从简单的、可重用的、使用更少的代码开始,如果结果不适合你,那就变得复杂。


这被称为服务定位器模式,并且通常不是最好的路线,除非您绝对必须这样做
@JoePhillips您对为什么它不是一个好的解决方案有一些意见吗?我喜欢它的优雅。我能想到的唯一缺点是每次你得到一个时我都会创建一个实例。
@Peter 主要原因是因为它非常难以使用。如果您将 serviceLocator 对象传递给一个类,那么该类使用什么依赖关系并不明显,因为它都是从一个神奇的“上帝”对象中获取它们的。想象一下,必须找到要更改的类型的引用。当您通过服务定位器对象获取所有内容时,该功能基本上消失了。构造函数注入更加清晰可靠
我不知道。显而易见性对我来说并不是一个缺点......因为如果我关心跟踪我的组件如何利用它们的依赖项,我会为此进行单元测试......测试不仅引用每个依赖项,而且帮助我们理解如何需要每个依赖项。通过阅读构造函数,您还能如何意识到这一点?!?