将依赖项实例传递给工厂方法参数,以使 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 内核更多的控制权,并且根本不为工厂创建一个类。并在 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"));

最后,我回顾了参数命名,例如更改了工厂界面中的loggerInstanceserviceLogger以符合约定。

这个解决方案仍然不是最好的解决方案,因为它有几个限制。

  1. 它容易出错。人们可以通过不保留命名约定来制造难以跟踪的错误,因为当前如果约定不匹配,它会静默失败。这可能可以改进,我稍后会考虑。
  2. 它只处理构造函数注入,但这应该不是一个大问题,因为这是建议的技术。例如,我几乎从不做其他类型的注射。

我意识到这是很久以前问过的,但我想自己做同样的事情,最后发现您可以使用传递给 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可以确保该值沿解析链传递。因此,它被传递给依赖树中使用该参数的任何对象(但仅适用于此特定实例化)。