将依赖项实例传递给工厂方法参数,以使 Ninject 在分辨率内使用它
本文关键字:Ninject 以使 分辨率 参数 实例 依赖 方法 工厂 | 更新日期: 2023-09-27 18:35:05
我有一个抽象工厂,它创建了一些由IService
接口表示的服务。在工厂中,我有两种Create
方法,因为在其中一个方法中,我允许使用者传递要由构造的服务树使用的现有IServiceLogger
实例。
public interface IMyServiceFactory {
IMyService Create(IServiceLogger loggerInstance);
IMyService Create();
}
由于IServiceLogger
应在服务树之间共享,因此在将其绑定到具体实现时,我使用该InCallScope
。
如何使用 Ninject 实现此方案?我尝试了以下方法。
1. 手动创建工厂实现
internal class MyServiceFactory : IMyServiceFactory {
private IResolutionRoot _kernel;
public MyServiceFactory
public IMyService Create(IServiceLogger loggerInstance) {
// what should go here? how can I pass the existing instance to Ninject Get method and make Ninject to use it for the whole resolution tree, just as it were created by Ninject and used as InCallScope?
}
// this one is trivial...
pulbic IMyService Create() {
return _kernel.Get<IMyService>();
}
}
更新
实际上,我已经找到了一种混乱且不太安全的方法。我可以通过 GetBindings
获取当前绑定,然后Rebind
IServiceLogger
ToConstant
,然后Get
IMyService
实例,最后使用 AddBinding
恢复原始绑定。我不喜欢它,它感觉很臭,更糟糕的是,它不是线程安全的,因为另一个线程可以在此代码中间请求IMyService
,因此使用本地临时绑定。
2. 使用 Ninject.Extensions.Factory
只需使用 ToFactory
绑定,但这不起作用,因为它只是尝试将参数用作简单的构造函数参数(如果适用),而不是作为整个解析树的对象。
我会给 Ninject 内核更多的控制权,并且根本不为工厂创建一个类。并在 Ninject 中使用 Func 绑定,如下所示:
Bind<Func<IMyService>>().ToMethod(s => CreateService);
通过绑定或不绑定 ILoggerService,您可以集中控制您的服务中是否有记录器。(尝试将其注释掉)
以下是引导程序的实现:
public class Bootstrapper
{
private IKernel _kernel = new StandardKernel();
public Bootstrapper()
{
_kernel.Bind<MyStuff>().ToSelf();
_kernel.Bind<IServiceLogger>().To<ServiceLogger>();
_kernel.Bind<IMyService>().To<MyService>();
_kernel.Bind<Func<IMyService>>().ToMethod(s => CreateService);
}
public IKernel Kernel
{
get
{
return _kernel;
}
set
{
_kernel = value;
}
}
private IMyService CreateService()
{
if(_kernel.GetBindings(typeof(IServiceLogger)).Any())
{
return _kernel.Get<IMyService>(new ConstructorArgument("logger", _kernel.Get<IServiceLogger>()));
}
return _kernel.Get<IMyService>();
}
}
工厂消费类的实现:
internal class MyStuff
{
private readonly Func<IMyService> _myServiceFactory;
public MyStuff(Func<IMyService> myServiceFactory)
{
_myServiceFactory = myServiceFactory;
_myServiceFactory.Invoke();
}
}
MyService的简单实现:
internal class MyService
:IMyService
{
public MyService()
{
Console.WriteLine("with no parameters");
}
public MyService(IServiceLogger logger)
{
Console.WriteLine("with logger parameters");
}
}
简单的服务记录器:
internal class ServiceLogger
:IServiceLogger
{
public ServiceLogger()
{
}
}
internal interface IServiceLogger
{
}
重要更新
虽然我最初的答案给了我一个可行的解决方案,但通过意外的 InteliSense 导航,我刚刚发现有一个内置工具可以解决这个问题。我只需要使用内置TypeMatchingArgumentInheritanceInstanceProvider
来执行此操作,甚至更多,因为由于参数类型匹配,不再需要命名约定。
最好有关于这些选项的更详细的文档,或者也许只是我目前找不到它。
原始答案
我尝试了几种方法,最终得到了一种略有不同的、利用 Ninject 上下文参数继承的基于约定的方法。
该约定用于通过依赖树命名构造函数参数。例如,每当将IServiceLogger
实例注入服务类时,都应调用该参数 serviceLogger
。
考虑到上述约定,我测试了以下方法。首先,我为工厂扩展实现了一个自定义实例提供程序。此自定义提供程序重写为上下文创建构造函数参数的机制,以允许开发人员指定多个应设置为继承的命名参数。这样,在 get 操作期间,具有指定名称的所有参数都将通过整个请求图继承。
public class ParameterInheritingInstanceProvider : StandardInstanceProvider
{
private readonly List<string> _parametersToInherit = new List<string>();
public ParameterInheritingInstanceProvider(params string[] parametersToInherit)
{
_parametersToInherit.AddRange(parametersToInherit);
}
protected override IConstructorArgument[] GetConstructorArguments(MethodInfo methodInfo, object[] arguments)
{
var parameters = methodInfo.GetParameters();
var constructorArgumentArray = new IConstructorArgument[parameters.Length];
for (var i = 0; i < parameters.Length; ++i)
constructorArgumentArray[i] = new ConstructorArgument(parameters[i].Name, arguments[i], _parametersToInherit.Contains(parameters[i].Name));
return constructorArgumentArray;
}
}
然后在绑定配置之后,我只是用相应的参数名称将其扔进去。
kernel.Bind<IMyServiceFactory>().ToFactory(() => new ParameterInheritingInstanceProvider("serviceLogger"));
最后,我回顾了参数命名,例如更改了工厂界面中的loggerInstance
以serviceLogger
以符合约定。
这个解决方案仍然不是最好的解决方案,因为它有几个限制。
- 它容易出错。人们可以通过不保留命名约定来制造难以跟踪的错误,因为当前如果约定不匹配,它会静默失败。这可能可以改进,我稍后会考虑。
- 它只处理构造函数注入,但这应该不是一个大问题,因为这是建议的技术。例如,我几乎从不做其他类型的注射。
我意识到这是很久以前问过的,但我想自己做同样的事情,最后发现您可以使用传递给 Get()
方法的 IParameter
数组来指定仅用于当前Get()
调用的ContructorArgument
。这允许我在创建 Hangfire 作业时使用特定的构造函数值,允许 Hangfire 作业在需要时在每次调用上使用不同的数据库连接。
EnvironmentName forcedEnv = new EnvironmentName() { Name = dbName };
// For this instantiation, set the 'envName' parameter to be the one we've specified for this job
var instance = ResolutionExtensions.Get((IResolutionRoot) _kernel, jobType,
new IParameter[] {new ConstructorArgument("envName", forcedEnv, true)});
return instance;
通过将shouldInherit
值设置为 true
可以确保该值沿解析链传递。因此,它被传递给依赖树中使用该参数的任何对象(但仅适用于此特定实例化)。