
[英]How to resolve multiple implementations of the same type in StructureMap DI and MVC 5
[英]DI, resolve service implementations from the factory
我有一个接口及其多种实现:
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>()
也会尝试解析工厂,即使它没有实现该接口,也会导致无限循环。
有几个问题:
更新
之所以将工厂排在第一位,是因为必须在运行时根据输入数据选择正确的服务,该服务可能会随每个新请求而改变。 因此,在应用程序启动期间,我无法将其范围缩小到特定的服务注册。
如注释中所述,您的代码失败,因为对于依赖项注入容器,工厂是 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
将每个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
实现之外,您还可以通过服务提供商并解决它们(如果已注册)。
看起来实际的问题是如何根据某些条件选择服务实现。 抛出异常是因为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>
。 控制器可以通过构造函数或动作注入来请求此功能:
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
我们的请求,则可以再次使用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;
};
您可以像这样注册工厂
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();
下面的代码第一寄存器Foo1
为IFoo
然后替换服务IFoo
与Foo2
。
services
.AddTransient<IFoo, Foo1>()
.AddTransient<IFoo, Foo2>();
对于您的第三个查询,
您无法执行此操作,因为您尝试使用IFoo
为IFoo
生成注册,这会导致出现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;
}
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.