Captive Dependency by Mark Seemann
A Captive Dependency is a dependency with an incorrectly configured lifetime. It's a typical and dangerous DI Container configuration error.
This post is the sixth in a series about Poka-yoke Design.
When you use a Dependency Injection (DI) Container, you should configure it according to the Register Resolve Release pattern. One aspect of configuration is to manage the lifetime of various services. If you're not careful, though, you may misconfigure lifetimes in such a way that a longer-lived service holds a shorter-lived service captive - often with subtle, but disastrous results. You could call this misconfiguration a Captive Dependency.
A major step in applying DI is to compose object graphs, and service lifetimes in object graphs are hierarchical:
This figure illustrates the configured and effective lifetimes of an object graph. Node A1 should have a Transient lifetime, which is certainly possible. A new instance of C1 should be created Per Request (if the object graph is part of a web application), which is also possible, because A1 has a shorter lifetime than Per Request. Similarly, only a single instance of B3 should ever be created, which is also possible, because the various instances of C1 can reuse the same B3 instance.
The A2 node also has a Singleton lifetime, which means that only a single instance should exist of this object. Because A2 holds references to B1 and A3, these two object are also effectively Singletons. It doesn't matter how you'd like the lifetimes of B1 and A3 to be: the fact is that the single instance of A2 holds on to its injected instances of B1 and A3 means that these instances are going to stick around as long as A2. This effect is transitive, so A2 also causes B2 to have an effective Singleton lifetime.
This can be problematic if, for example, B1, A3, or B2 aren't thread-safe.
Commerce example #
This may make more sense if you see this in a more concrete setting than just an object graph with A1, A2, B1, etc. nodes, so consider the introductory example from my book. It has a ProductService, which depends on an IProductRepository interface (actually, in the book, the Repository is an Abstract Base Class):
public class ProductService { private readonly IProductRepository repository; public ProductService(IProductRepository repository) { this.repository = repository; } // Other members go here... }
One implementation of IProductRepository is SqlProductRepository, which itself depends on an Entity Framework context:
public class SqlProductRepository : IProductRepository { private readonly CommerceContext context; public SqlProductRepository(CommerceContext context) { this.context = context; } // IProductRepository members go here... }
The CommerceContext class derives from the Entity Framework DbContext class, which, last time I looked, isn't thread-safe. Thus, when used in a web application, it's very important to create a new instance of the CommerceContext class for every request, because otherwise you may experience errors. What's worse is that these errors will be threading errors, so you'll not discover them when you test your web application on your development machine, but when in production, you'll have multiple concurrent requests, and then the application will crash (or perhaps 'just' lose data, which is even worse).
(As a side note I should point out that I've used neither Entity Framework nor the Repository pattern for years now, but the example explains the problem well, in a context familiar to most people.)
The ProductService class is a stateless service, and therefore thread-safe, so it's an excellent candidate for the Singleton lifestyle. However, as it turns out, that's not going to work.
NInject example #
If you want to configure ProductService and its dependencies using Ninject, you might accidentally do something like this:
var container = new StandardKernel(); container.Bind<ProductService>().ToSelf().InSingletonScope(); container.Bind<IProductRepository>().To<SqlProductRepository>();
With Ninject you don't need to register concrete types, so there's no reason to register the CommerceContext class; it wouldn't be necessary to register the ProductService either, if it wasn't for the fact that you'd like it to have the Singleton lifestyle. Ninject's default lifestyle is Transient, so that's the lifestyle of both SqlProductRepository and CommerceContext.
As you've probably already predicted, the Singleton lifestyle of ProductService captures both the direct dependency IProductRepository, and the indirect dependency CommerceContext:
var actual1 = container.Get<ProductService>(); var actual2 = container.Get<ProductService>(); // You'd want this assertion to pass, but it fails Assert.NotEqual(actual1.Repository, actual2.Repository);
The repositories are the same because actual1
and actual2
are the same instance, so naturally, their constituent components are also the same.
This is problematic because CommerceContext (deriving from DbContext) isn't thread-safe, so if you resolve ProductService from multiple concurrent requests (which you could easily do in a web application), you'll have a problem.
The immediate fix is to make this entire sub-graph Transient:
var container = new StandardKernel(); container.Bind<ProductService>().ToSelf().InTransientScope(); container.Bind<IProductRepository>().To<SqlProductRepository>();
Actually, since Transient is the default, stating the lifetime is redundant, and can be omitted:
var container = new StandardKernel(); container.Bind<ProductService>().ToSelf(); container.Bind<IProductRepository>().To<SqlProductRepository>();
Finally, since you don't have to register concrete types with Ninject, you can completely omit the ProductService registration:
var container = new StandardKernel(); container.Bind<IProductRepository>().To<SqlProductRepository>();
This works:
var actual1 = container.Get<ProductService>(); var actual2 = container.Get<ProductService>(); Assert.NotEqual(actual1.Repository, actual2.Repository);
While the Captive Dependency error is intrinsically tied to using a DI Container, it's by no means particular to Ninject.
Autofac example #
It would be unfair to leave you with the impression that this problem is a problem with Ninject; it's not. All DI Containers I know of have this problem. Autofac is just another example.
Again, you'd like ProductService to have the Singleton lifestyle, because it's thread-safe, and it would be more efficient that way:
var builder = new ContainerBuilder(); builder.RegisterType<ProductService>().SingleInstance(); builder.RegisterType<SqlProductRepository>().As<IProductRepository>(); builder.RegisterType<CommerceContext>(); var container = builder.Build();
Like Ninject, the default lifestyle for Autofac is Transient, so you don't have to explicitly configure the lifetimes of SqlProductRepository or CommerceContext. On the other hand, Autofac requires you to register all services in use, even when they're concrete classes; this is the reason you see a registration statement for CommerceContext as well.
The problem is exactly the same as with Ninject:
var actual1 = container.Resolve<ProductService>(); var actual2 = container.Resolve<ProductService>(); // You'd want this assertion to pass, but it fails Assert.NotEqual(actual1.Repository, actual2.Repository);
The reason is the same as before, as is the solution:
var builder = new ContainerBuilder(); builder.RegisterType<ProductService>(); builder.RegisterType<SqlProductRepository>().As<IProductRepository>(); builder.RegisterType<CommerceContext>(); var container = builder.Build(); var actual1 = container.Resolve<ProductService>(); var actual2 = container.Resolve<ProductService>(); Assert.NotEqual(actual1.Repository, actual2.Repository);
Notice that, because the default lifetime is Transient, you don't have to state it while registering any of the services.
Concluding remarks #
You can re-create this problem with any major DI Container. The problem isn't associated with any particular DI Container, but simply the fact that there are trade-offs associated with using a DI Container, and one of the trade-offs is a reduction in compile-time feedback. The way typical DI Container registration APIs work, they can't easily detect this lifetime configuration mismatch.
It's been a while since I last did a full survey of the .NET DI Container landscape, and back then (when I wrote my book), no containers could detect this problem. Since then, I believe Castle Windsor has got some Captive Dependency detection built in, but I admit that I'm not up to speed; other containers may have this feature as well.
When I wrote my book some years ago, I considered including a description of the Captive Dependency configuration error, but for various reasons, it never made it into the book:
- As far as I recall, it was Krzysztof Koźmic who originally made me aware of this problem. In emails, we debated various ideas for a name, but we couldn't really settle on something catchy. Since I don't like to describe something I can't name, it never really made it into the book.
- One of the major goals of the book was to explain DI as a set of principles and patterns decoupled from DI Containers. The Captive Dependency problem is specifically associated with DI Containers, so it didn't really fit into the book.
In a follow-up post to this, I'll demonstrate why you don't have the same problem when you hand-code your object graphs.
Comments
Simple Injector has built in support for a number of container verifications including lifestyle mismatches (Captive Dependency is a lifestyle mismatch) through its Diagnostic Services.
The configuration for Simple Injector looks like this:
The crucial difference with Simple Injector is that once you have finished configuring the container you make a call to the
Verify()
method to catch misconfigurations such as Captive Dependency.Here's an example test to demonstrate that the container correctly identifies the lifestyle mismatch:
And for completeness we should also mention how to solve the captive dependency problem. From the really awsome SimpleInjector documentation:
For the above example you would probably want to introduce a factory for the DbContexts.