【问题标题】:DI, resolve service implementations from the factoryDI,从工厂解决服务实现
【发布时间】:2019-01-17 19:14:53
【问题描述】:

我有一个接口和它的多个实现:

interface IFoo { bool CanFoo(); }
class Foo1 : IFoo { bool CanFoo() => true; }
class Foo2 : IFoo { bool CanFoo() => false; }

我想在IServiceCollection 中注册所有这些并提供这样的自定义实现工厂:

services
    .AddTransient<IFoo, Foo1>()
    .AddTransient<IFoo, Foo2>();

// register the factory last
services.AddTransient<IFoo>(provider =>
{
    var registrations = provider.GetServices<IFoo>(); // Exception

    return registrations.FirstOrDefault(r => r.CanFoo());
};

所以你可能会看到这个想法是解决第一个可用的实现。然而,这段代码会导致StackOverflowException,因为provider.GetServices&lt;IFoo&gt;() 也尝试解析工厂,即使它没有实现接口,从而导致无限循环。

有几个问题:

  1. 在这种情况下是否期望服务提供商尝试解决工厂问题?
  2. 如果可能,我该如何克服这个问题?

更新

之所以首先涉及工厂,是因为必须在运行时根据输入数据选择正确的服务,这可能会随着每个新的请求而改变。因此,我无法将其缩小到应用程序启动期间的特定服务注册。

【问题讨论】:

  • GetServices 创建服务,它不返回他们的注册。当您调用它时,它将尝试通过实例化任何已注册的类型 工厂来创建服务。如果你在工厂内部调用它,你会得到一个递归调用
  • 这段代码很奇怪。每次您想要一个 IFoo 时,您是否要创建 N 个临时对象但只保留其中一个?看起来您正在尝试在 DI 中实现过滤。您要解决的实际问题是什么?
  • @PanagiotisKanavos 正确,就是这样。我意识到它并不完美,但我需要将处理请求的能力委托给服务。有什么建议吗?
  • 您可以通过services而不是provider获取注册。您可以检查ServiceDescriptor 的每个IFoo 的ServiceType,并在ImplementationTypes 上使用反射来查找它们中的哪些具有特定属性或实现其他“标记”接口。注册具体的类,Foo1Foo2,而不是它们的接口,并使用工厂从服务描述符中选择你想要的
  • 您想要使用的实际标准是什么?为什么要调用具体类的方法?

标签: c# asp.net-core .net-core


【解决方案1】:

如 cmets 中所述,您的代码失败是因为对于依赖注入容器,您的工厂IFoo 实例的有效来源。所以当要求解决IFoo的所有服务时,工厂也会被要求解决。

如果你想让它工作,那么你必须以某种方式分离类型。例如,您可以创建一个标记接口IActualFoo,用于注册您的服务:

services.AddTransient<IActualFoo, Foo1>()
services.AddTransient<IActualFoo, Foo2>();

在您的IFoo 工厂内部,您可以解析IActualFoo 并将它们转换为IFoo。当然,这意味着IActualFoo 将继承自IFoo,并且您的Foo 服务实际上必须实现IActualFoo


另一种方法是将选择正确的IFoo 实现的责任转移到工厂中。正如其他人所指出的,您的方法的缺点是它将为IFoo 的每个实现创建实例,然后才选择正确的实例。太浪费了。

如果工厂能够决定选择正确的IFoo 实现,那就更好了。通常,这看起来像这样:

services.AddTransient<Foo1>();
services.AddTransient<Foo2>();
services.AddTransient<IFoo>(sp =>
{
    if (someMagicCondition)
        return sp.GetService<Foo1>();
    else
        return sp.GetService<Foo2>();
});

因此工厂将根据某些条件逻辑创建正确的实例。虽然这种方法经常被使用,但它需要工厂内的全部责任,并且在设计时需要有限的一组可能的IFoo 实现。因此,如果您想稍后动态添加实现,这将不起作用。

您还可以注册一组IFoo工厂。因此,不要让IFoo 实现来决定它是否是正确的实现,而是将该逻辑移动到每个IFoo 实现的工厂:

services.AddSingleton<IFooFactory, Foo1Factory>();
services.AddSingleton<IFooFactory, Foo2Factory>();
services.AddSingleton<IFoo>(sp =>
{
    var factories = sp.GetServices<IFooFactory>();
    return factories.FirstOrDefault(f => f.CanFoo())?.CreateFoo();
});
public interface IFooFactory
{
    bool CanFoo();
    IFoo CreateFoo();
}

public Foo1Factory : IFooFactory
{
    public bool CanFoo() => true;
    public IFoo CreateFoo() => new Foo1();
}
public Foo2Factory : IFooFactory
{
    public bool CanFoo() => false;
    public IFoo CreateFoo() => new Foo2();
}

当然,除了在工厂中更新 Foo 实现之外,您还可以传递服务提供者并解析它们(如果它们已注册)。

【讨论】:

  • 我喜欢有一组工厂的想法,但它仍然很昂贵。现在我被指出来了,我开始更多地考虑性能。
  • 不,不会那么贵,因为工厂不会一直重建。它们被注册为单例。
【解决方案2】:

看起来实际的问题是如何根据某些标准选择服务实现。抛出异常是因为GetServices 返回注册产生的所有服务,包括工厂方法本身。当工厂调用GetServices 时,它最终会递归调用自身,直到出现 StackOverflowException。

另一个问题是GetServices() 实际上会创建 服务实例但只使用一个。另一个被丢弃。这会导致垃圾对象,并且如果服务本身很昂贵或控制诸如数据库连接之类的资源,则可能会很昂贵。

如果服务是直接注册的,并且一旦工厂方法决定了它想要哪个类型,就可以避免递归::

services
    .AddTransient<Foo1>()
    .AddTransient<Foo2>();

// register the factory last
services.AddTransient<IFoo>(provider =>
{
    var type=PickFooType();

    return provider.GetService(type);
};

现在的诀窍是选择正确的类型。这取决于实际的标准。根据一些配置或易于访问的状态来选择类型是一回事,根据实现类的属性来选择它是另一回事。

在“简单”的情况下,我们假设需要根据标志或配置设置选择正确的类型。选择类型可以是简单的switch

如果该设置在启动时可用,则可以避免出厂设置并简单地注册所需的类型。

但在某些情况下,设置可能会在运行时更改。例如,如果一个远程服务提供商出现故障,我们可能需要故障转移到另一个。在最简单的情况下,可以使用一个简单的开关:

Type PickPaymentProvider()
{
    var activeProvider=LoadActiveProvider();
    switch (activeProvider)
    {
        case 'Paypal':
            return typeof(Foo1);
        case 'Visa' :
            return typeof(Foo2);
        ...
    }
}        

更新 - 基于上下文的依赖解析

从问题的更新看来,问题不在于如何创建工厂。它是如何根据每个单独请求的上下文(参数值、环境变量、数据等)选择服务。这称为基于上下文的依赖关系解析。它在高级 DI 容器中可用,例如 Autofac,但在 .NET Core 的 DI 实现和抽象中不可用。

添加它的正确方法是在每个请求之前添加替换 DI 解析步骤的中间件。快速而肮脏的方法是将工厂函数本身添加为服务并在需要时调用它。这就是 Rahul 在他的回答中所表明的。

这意味着AddTransient 必须注册一个函数,它接受解析所需的参数:

services.AddTransient(provider =>
{
    IFoo resolver(MyParam1 param1,MyParam2 param2)
    {
        var type=PickFooType(param1,param2);    
        return provider.GetService(type);
    }

    return resolver;
};

这会注册一个Func&lt;MyParam1,MyParam2,IFoo&gt;。控制器可以通过构造函数或action injection 请求此函数:

public IActionResult MyAction([FromServices] resolver,int id,MyParam1 param1...)
{
    MyParam2 param2=LoadFromDatabase(id);
    IFoo service=resolver(param1,param2);
    var result=service.DoSomeJob();
    return OK(result);
}

通过注册factory而不是IFoo接口,我们可以再次通过接口注册服务。

services
    .AddTransient<IFoo,Foo1>()
    .AddTransient<IFoo,Foo2>();

如果我们需要调用每个注册的服务并询问它是否可以服务我们的请求,我们可以再次使用GetServices

services.AddTransient(provider =>
{
    IFoo resolver(MyParam1 param1,MyParam2 param2)
    {
        var firstMatch=provider.GetServices<IFoo>()
                               .First(svc=>svc.CanFoo(param1,param2));
        return firstMatch;
    }

    return resolver;
};

【讨论】:

  • 谢谢。我更新了这个问题,因为我意识到缺少某些标准,这让人们感到困惑。
  • @Zabavsky 这又是一个不同的问题。您要求在 .NET Core 的 DI 基础架构中提供基于上下文的依赖关系解析。它在 AutoFac 等其他 DI 容器中可用。正确添加它需要改变请求中间件链的工作方式,这很难。注册一个 function 就像你似乎尝试做的那样,是“快速而肮脏”的方式。
  • 尽管我喜欢 Autofac,但我的项目规模很小,无法涉及这么大的枪。我将采用@poke 的方法来处理多个工厂。感谢您的回答,非常感谢。
【解决方案3】:

你可以像这样注册工厂

        serviceCollection.AddTransient(factory =>
       {
           Func<string, IFoo> mytype = key =>
           {
               switch (key)
               {
                   case "Foo1":
                       return factory.GetService<Foo1>();
                   case "Foo2":
                       return factory.GetService<Foo2>();
                   default:
                       return null;
               }
           };
           return mytype;
       })

无论您在哪里尝试使用可以声明和注入的类型

private Func<string, IFoo> newType;

你可以打电话了

newType("Foo1").CanFoo();

【讨论】:

  • 什么是key,如何使用注册?
  • @Zabavsky you 必须确定该密钥是什么(并将其发布在问题中)。至于注册,只能通过services获得,而不是provider
  • @Zabavsky 根据您的更新,this 看起来是最佳答案
【解决方案4】:

以下代码首先为IFoo 注册Foo1,然后将IFoo 的服务替换为Foo2

services
  .AddTransient<IFoo, Foo1>()
  .AddTransient<IFoo, Foo2>();

对于您的第三个查询,

您不能这样做,因为您尝试使用 IFooIFoo 生成注册,这会导致预期的 StackOverflowException(正如您已经提到的那样)。

你要做的是。先分别注册各个服务,

 services
  .AddTransient<Foo1>()
  .AddTransient<Foo2>();

然后使用工厂方法进行条件注册。

serviceCollection.AddTransient(factory =>
{
    //get the key here. you can use factory.GetService
    // to get another service and extract Key from it.
     switch(key)
     {
         case "Foo1":
              return factory.GetService<Foo1>();
              break;
         case "Foo2":
              return factory.GetService<Foo2>();
              break;
         default:
              return null;
      }
}

【讨论】:

    猜你喜欢
    • 2018-01-24
    • 1970-01-01
    • 2020-06-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多