This is a follow up to my previous post. I deliberately didn’t discuss the issues that arise when using container hierarchies to get some feedback on usage first.
So what’s the problem?
Consider trivial scenario:
We have two components, where foo depends on bar. The dependency is optional, which means that foo can be constructed even when bar is not available.
Then we have the following container hierarchy:
We have parent container where foo is registered and two child containers. In one of them we register bar. Both foo and bar are singletons (for sake of simplicity – the example would work for any non-transient lifestyle).
Now we do this
var fooA = childA.Resolve<Foo>();
var fooB = childB.Resolve<Foo>();
What should happen?
var fooC = parent.Resolve<Foo>();
fooA.ShouldNotBeSameAs( fooB );
fooA.ShouldNotBeSameAs( fooC );
fooB.ShouldBeSameAs( fooC );
Effectively, scope the component to lowest container in the hierarchy required to satisfy the dependencies.
If child containers are kept, they should be created and managed by the parent container though, not managed externally to the parent container as they currently are.
They are singletons in App scope, so this would not work. Probably upon resolving foo via the childA route, bar would stay with foo.
IMO it’s a world of hurt to have dependencies flow more than 1 direction. what happens when we have a child of a child of a child of a child which each have different dependencies, some optional, some not?
or worse, what happens if we have multiple children, should then be able to resolve dependencies through a common parent?
Good illustration of an inherent problem with child containers.
However, I think the root cause of the problem is not so much with child containers as it is with the ambiguity of optional dependencies.
However, the problem is really about scope. Would you consider yanking support for lifestyle management just because it’s technically possible to inject a Transient into a Singleton?
I admit that the decision algorithm in the case of child containers is less obvious, but considering your example, I would argue that it’s a degenerate case when the first thing that happens is childA.Resolve<Foo>().
In the general case Foo could be resolved by any container, and it would be resolved without ‘bar’. I would therefore argue that resolving Foo from childA ought to resolve Foo without ‘bar’ as well.
I would imagine that the outcome of this would be surprising to most developers. What about, as Neal suggested, making the Singleton Lifestyle container scoped? However I would suggested that:
* Singleton remains Singleton 😉 – for anybody "I’m brave enough"/"I know what I’m doing"
* SingletonPerContainer – which would be Singleton but scoped to the resolution context of a container.
The problem would remain if it wasn’t an optional dependency. Just the outcome would be different (throw instead of null). Real question is – which of possible behaviours should be applied here.
More on that in tomorrow’s post.
I would expect any container to throw if the dependency can’t be satisfied, and as far as I recall, that’s exactly what currently happens.
IIRC this fits our scenario very well. We have services defined in the parent container where their dependencies are only satisfied in child containers. The point is that each child container supplies different instances of those dependencies.
I would even go so far as to state (IMHO) that a component in a parent container relying on child containers to provide its dependencies would be a bit of a code smell.
I agree with Mark and think the natural (and safe) way to go here would be that foo should always be resolved without bar in the above example.
This approach is not a code smell – it is effectively a container driven strategy pattern, where the parent container acts much like the abstract base class for all of the strategies and each child container acts much like the concrete strategy.
Consider a scenario where you have some application level services that need to vary their implementation based on some aspect of the context ( a common scenario would be database / account for multi-tenant scenarios ). By using child containers you can effectively set up 1 per tenant and add the equivalent of ISessionFactory for the tenant specific database to each – a very obvious solution for any maintenance developers as opposed to trying to understand how variations of ISessionFactory are resolved when they are all registered in one container.
Child containers become even more useful when you have multiple parent components, all with multiple variations of some dependency as understanding what dependencies the tenant get is as simple as looking in their child container.