将依赖实例传递给工厂方法参数,使 Ninject 在解析中使用它

Pass a dependency instance to a factory method parameter to make Ninject use it within the resolution

我有一个抽象工厂,它创建了一些由 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 的内核更多的控制权,并且根本不为工厂创建 class。 并像这样在 Ninject 中使用 Func 绑定:

Bind<Func<IMyService>>().ToMethod(s => CreateService);

通过绑定或不绑定 ILoggerService,您可以集中控制您的服务中是否有记录器。(尝试将其注释掉)

这里是 Bootstrapper 的实现:

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

为工厂实施消费者class:

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

简单 ServiceLogger:

internal class ServiceLogger
        :IServiceLogger
    {
        public ServiceLogger()
        {

        }
    }

    internal interface IServiceLogger
    {
    }

重要更新

虽然我最初的回答给了我一个可行的解决方案,但通过一次意外的 InteliSense 导航,我发现有一个内置工具可以解决这个问题。我只需要使用内置的 TypeMatchingArgumentInheritanceInstanceProvider 来执行此操作,甚至更多,因为由于参数类型匹配,不再需要命名约定。

最好有关于这些选项的更详细的文档,或者可能只有我目前找不到。


原始答案

我尝试了几种方法,最后得到了一种略有不同的基于约定的方法,它利用了 Ninject 的上下文参数继承。

约定用于通过依赖树命名构造函数参数。例如,每当将 IServiceLogger 实例注入服务 class 时,参数应称为 serviceLogger.

考虑到上述约定,我测试了以下方法。首先,我为工厂扩展实现了一个自定义实例提供程序。此自定义提供程序覆盖为上下文创建构造函数参数的机制,让开发人员指定几个应设置为继承的命名参数。这样所有具有指定名称的参数将在获取操作期间通过整个请求图继承。

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 以符合约定。

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

  1. 它很容易出错。如果不遵守命名约定,可能会产生难以跟踪的错误,因为目前如果约定不匹配,它就会默默地失败。这个应该可以改进,以后再考虑。
  2. 它只处理构造函数注入,但这应该不是一个大问题,因为这是建议的技术。例如我几乎从不做其他类型的注射。

我知道很久以前有人问过这个问题,但我自己也想做同样的事情,最后发现你可以使用传递给 Get() 方法的 IParameter 数组指定 ContructorArgument 仅用于当前 Get() 调用。这允许我在创建 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,您可以确保该值沿着解析链传递。所以它被传递给依赖树中使用该参数的任何对象(但仅针对此特定实例)。