4

Generally I would always answer "yes" to this question, because at this point, your DI container has become a service locator, and you're losing the benefits of DI.

However, I wonder if it is acceptable in the following specific scenario.

  • I have an application with a DI container.
  • The DI container is responsible for constructing instances of components.
  • Upon component construction, the container resolves the component's constructor arguments (Reflection) and injects any applicable dependencies – either from its cached instances, or constructing them anew
  • Constructed instance returned to caller, and cached in the container – next time an instance of this component is requested, the currently existing one will be returned.

So really the container has two roles: enforcement of singleton pattern, and dependency management.

But this isn't really relevant to the question; I'm comfortable with the container operation, it does its job well and I'm comfortable using it. It's only directly interacted with during application bootstrap to create instances of core components with their dependencies, and I feel this is the correct way to be doing things.

So you won't see $this -> DIContainer anywhere else in the application except higher-level orchestrator components.


Excuse the preamble, but I want to make things clear. There is one exception to the above sentence.

The application supports plugins, and naturally, we cannot anticipate the dependencies of plugins ahead of time. We have no idea where this plugin came from or what components it needs to do its job.

Initially this isn't a problem – the DI container was built to do this job! We just call $this -> DIContainer -> instance("MyPlugin") and it's problem solved.

But it's more tricky. It turns out plugins can only be built by a special plugin factory, which takes care of a lot of boilerplate operations around getting our plugin instance – loading its definition from the filesystem, checking it's valid for our API version, ensuring it's permitted to be enabled, getting static configuration data to inject into the plugin etc.

So the factory should be the entrypoint for any consumer wanting to get an instantiated plugin.

Only problem is, if the factory is to instantiate the plugin, it needs direct access to the DI container to call $this -> DIContainer -> ("MyPlugin").

Which means constructing a PluginFactory instance requires passing in a DIContainer instance, which on some level feels wrong.


Now, I'm not entirely against this approach. Here's what I see as the advantages/disadvantages.

Good

  • It's obvious where to go to get a plugin instance, the PluginFactory, and you don't need to go to two places
  • The plugin factory could be considered a higher-level component
  • It's not entirely against DI, but I might be pushing it a bit... we cannot possibly anticipate the dependencies of the plugins, so the only way the plugin factory can do its job is if it can access every possible dependency – in turn, the DI container itself is the only component which can fulfil that requirement.

Bad

  • The DI container has been passed around, and is no longer kept secure in the application root component.
  • It could be said the plugin factory is doing too much; perhaps consumers should call the factory to load the plugin from filesystem/do the boilerplate stuff, then defer to DI container to get an instance afterwards. Like requesting the factory make something, and going to the store to get it.
  • Actually getting the DI container into the plugin factory as a dependency is itself potentially troublesome – if we assume the plugin factory is constructed through DI itself (it has other dependencies besides the container), then the DI container is probably going to be injected into itself, so it can be resolved back out when it comes to parsing the factory's DIContainer argument – which seems extremely fragile to me.

Like so often, writing all this out has already helped me along the way, an I'm looking into further consideration of my point above – perhaps the plugin factory is doing too much. Maybe the factory only needs to get the pieces in place, so higher-level components can get plugin instances from DI.

Nonetheless I'm interested to hear thoughts in. This feels like a time when it just might be OK to make the DI container available to a non-top-level component, but there are clearly still concerns.

4
  • How does the plugin know what interfaces are available for injection?
    – Ewan
    Nov 12, 2018 at 15:58
  • @Ewan I'm not sure how that is relevant really. Assume there's a documented set of interfaces which the plugin (or any other resource being constructed through DI) can expect to be able to access. But that doesn't alter how the plugin itself gets constructed..
    – user192491
    Nov 12, 2018 at 16:40
  • 1
    Reading your questions, it feels more like a limitation of DI containers in general: their generic approach forbids specialized instantiation logic, which can be easily implemented in factories. Maybe you will better off and stick to pure DI
    – Doc Brown
    Nov 12, 2018 at 16:40
  • If there is a set of possibly required by plugins interfaces then the factory can just require those interfaces and pass them on as required without using the DI container directly
    – Ewan
    Nov 12, 2018 at 16:42

2 Answers 2

1

So, its definitely an anti-pattern to pass the DI container around. That doesn't mean theres never a case where you do it out of expediency. Its just a warning to say: "If you are doing this you are not following the DI pattern"

Now in the case of a 3rd party plugin I don't think I would want to register or resolve the plugin through DI at all.

I want the plugin to run in my sandbox. If I give it access to the DI its could do something like:

public class MyBadPlugin 
{
    public MyBadPlugin(IContainer c)
    {
        c.DeRegister<CoreComponent>();
        c.Register<EvilCoreComponent>();
    }
}

Or at least break out of my sandbox.

Plus, the good plugin developers are going to expect some sort of documented plugin scope.

Sure its great if I give them god mode access, but I also have to document all of my internal code and worry about what happens when its called unexpectedly by plugin X

That sounds like a recipie for disaster with plugins likely to break each other and my app.

So in summary:

Sure pass it in, IF you are only allowing internally developed plugins where they are fully trusted and the developers know the codebase. You understand the DI pattern well enough that you can allow yourself to break the rule.

Don't pass it in IF you are allowing 3rd party plugins. Its a security hole and stability issue.

6
  • 1
    If I got the OP right, they were not suggesting to give the plugin itself access to the DI container. So the scenario you scetched does not really fit to the question.
    – Doc Brown
    Nov 12, 2018 at 16:35
  • If you resolve the plugin from the container and it requires the container, then the container will inject itself
    – Ewan
    Nov 12, 2018 at 16:37
  • 1
    As mentioned by @DocBrown this isn't what I'm asking I'm afraid.
    – user192491
    Nov 12, 2018 at 16:39
  • @Ewan: not the plugin needs the container, the factory does, which is code under the OP's control, opposed to the plugin code.
    – Doc Brown
    Nov 12, 2018 at 16:41
  • The factory resolves the plugin from the di container right?
    – Ewan
    Nov 12, 2018 at 16:44
1

In my opinion it is "okay" to inject the container itself into a factory. If you want to justify it in your mind, don't call it a factory-- call it a "container extension." It's all part of the subsystem that is responsible for instantiation and lifetime scope-- there's no rule that says a DI container must be exactly one class or one object.

That being said, if you wish to lock the container away from the factory, you can inject a factory method instead, passing the container in a closure. The method would accept a plugin name and return an instance.

//Composition root
container.RegisterInstance<Func<string,IPlugin>>
(  
    pluginName => container.instance(pluginName) 
);

//Your factory
class PluginFactory
{
    protected readonly Func<string,IPlugin> _getter;

    public PluginFactory(Func<string,IPlugin> getter)
    {
        _getter = getter;
    }

    public IPlugin GetPlugin(string name)
    {
        return _getter(name);
    }
}

That offers somewhat better separation of concerns because it keeps the container logic mostly private and only exposes exactly what the factory needs. It will also be easier for a unit test to substitute a mock or stub for the factory method when instantiating the factory.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.