将依赖项实例传递给工厂方法参数,以使Ninject在分辨率内使用它

时间:2016-02-17 11:35:51

标签: c# ninject ninject-extensions

我有一个抽象工厂,它创建一些由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绑定,但这不起作用,因为它只是尝试将参数用作简单的构造函数参数(如果适用),而不是整个解析树的对象

3 个答案:

答案 0 :(得分:0)

我会更多地控制Ninject的内核,而不是为工厂创建一个类。 并在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>();
        }
    }

为工厂实施消费者类:

 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
    {
    }

答案 1 :(得分:0)

重要更新

虽然我的原始答案给了我一个可行的解决方案,但是偶然的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以符合约定。

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

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

答案 2 :(得分:0)

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