Dependency Injection (DI) and IoC
Dependency Injection (DI) is one of the most important concepts in software engineering. We are no strangers to DI in the .NET world. Historically, the .NET framework had support for many IoC containers, such as AutoFac, Castle Windsor, Structure Map, Unity, etc. With the evolution of .NET Core, now ASP.NET Core comes up with a built-in IoC Container.
Before I proceed further, here is a quick recap of different lifetime services which comes with ASP.NET Core Dependency Injection:
Transient lifetime services are created each time they’re requested from the service container. This lifetime works best for lightweight, stateless services.
Scoped lifetime services (AddScoped) are created once per client request (connection).
Singleton lifetime services (AddSingleton) are created the first time they’re requested. Every subsequent request uses the same instance.
I understand DI but what is a captive dependency?
The captive dependency issue perhaps is as old as Dependency Injection. Mark Seemann has defined captive dependency as follows:
A Captive Dependency is a dependency with an incorrectly configured lifetime. It’s a typical and dangerous DI Container configuration error.
Let us consider a simple example:
- Consider a class ScopedDependency with a scoped lifetime.
- Consider a class SingletonDependency with singleton lifetime.
As per definition, SingletonDependency instance is created only once whereas a new ScopedDependency instance would be created for each request. What would happen if SingletonDependency takes a dependency on ScopedDependency?
|public class SingletonDependency|
|private readonly ScopedDependency scopedDepdendency;|
|// Should this blow up??|
|public SingletonDependency(ScopedDependency transitiveDependecy)|
|this.scopedDepdendency = transitiveDependecy;|
|public int GetNextCounter()|
As you can see in code, for every new request a when ScopedDependency is instantiated, you would expect the counter to increment by 1. Unfortunately, when you instantiate SingletonDepedency, it would hold on to a stale instance ScopedDependency which was created for the first-ever request.
In a complex architecture, captive dependency can lead to notorious runtime bugs which can very hard to identify and debug.
Even the IoC containers such as Autofac do not prevent developers from creating captive dependency and leave the responsibility to the developers. From the documentation:
Autofac does not necessarily prevent you from creating captive dependencies. You may find times when you get a resolution exception because of the way a captive is set up, but you won’t always. Stopping captive dependencies is the responsibility of the developer.
ASP.NET Core tries to solve this problem (but just partially)
ASP.NET Core default service container provides a way to deduct the captive dependency. However, it is an opt-in feature. To opt-in to this feature, you would need to set property ValidateScopes in Program.cs as shown below.
When you try to run your application now, you would receive an InvalidOperationException.
An important thing to note here is that validating scope at the Startup is a performance intensive operation. Hence, you may want only to set this property during development.
Also, this solution is not a silver bullet. As the name suggests, it only validates “Scoped” dependencies. This property does not work when you have Transient lifetime services.
A real-world example
One of the real-world examples where this issue can bite you hard is when you try to use Typed HttpClient by registering your service through AddHttpClient. A Typed HttpClient has a transient lifetime. From the documentation
A Typed Client is, effectively, a transient object, meaning that a new instance is created each time one is needed and it will receive a new
HttpClientinstance each time it’s constructed. However, the
HttpMessageHandlerobjects in the pool are the objects that are reused by multiple
Since, Typed Client is a transient object, using it in singleton service could lead to unexpected issues.
Of course, you easily fix this creating a ClientFactory.
The problem, however, is that it is “one more thing” that you need to remember. There are no guard rails to stop a developer from using it wrongly.
Please be extra careful when defining the lifetimes of your services. At this point in time, the ASP.NET Core default IoC container does not do a great job in preventing captive dependency issues. The captive dependency issues are hard to deduct and could lead to runtime errors. I hope that I’m able to highlight pitfalls of not configuring the lifetimes correctly.