DI,从工厂解决服务实施

时间:2019-01-17 08:55:56

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

我有一个接口及其多种实现:

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<IFoo>()也会尝试解析工厂,即使它没有实现接口也导致无限循环。

有几个问题:

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

更新

之所以将工厂排在第一位,是因为必须在运行时的基础上根据输入数据选择正确的服务,这可能会随每个新请求而变化。因此,在应用程序启动期间,我无法将其范围缩小到特定的服务注册。

4 个答案:

答案 0 :(得分:2)

如注释中所述,您的代码失败,因为对于依赖项注入容器,您的工厂 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实现之外,您还可以通过服务提供商并解决它们(如果已注册)。

答案 1 :(得分:2)

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

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

如果直接注册服务,并且工厂方法决定了所需的类型,则工厂方法会以所需的类型调用GetService,这样可以避免递归::

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);
        ...
    }
}        

更新-基于上下文的依存关系解析

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

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

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

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

    return resolver;
};

这将注册Func<MyParam1,MyParam2,IFoo>。控制器可以通过构造函数或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);
}

通过注册工厂而不是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;
};

答案 2 :(得分:1)

您可以像这样注册工厂

        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();

答案 3 :(得分:0)

以下代码首先为Foo1注册IFoo,然后用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;
      }
}