Aleksei Ananev
Posted on November 21, 2020
Dependency Injection Problem
Dependency injection is an essential part of modern applications that allows getting clean, reusable, testable code. Loosely coupled code and the single responsibility principle are considered best practices. And that's exactly what dependency injection helps to achieve.
Along with the power and benefits of dependency injection, there is at least one problem that can be illustrated as follows.
Assume a service called MyService
depends on ServiceA
and ServiceB
. The number of all nested dependencies is N
for ServiceA
and M
for ServiceB
:
All these services are registered in an IoC container by corresponding interfaces:
container
.RegisterType<IMyService, MyService>()
.RegisterType<IServiceA, ServiceA>()
.RegisterType<IServiceB, ServiceB>()
...
The MyService
service has the following implementation:
public class MyService : IMyService
{
private readonly IServiceA _serviceA;
private readonly IServiceB _serviceB;
public MyService(IServiceA serviceA, IServiceB serviceB)
{
_serviceA = serviceA;
_serviceB = serviceB;
}
public void DoWork(int value)
{
if (value < 42)
{
_serviceA.DoWork();
}
else
{
_serviceB.DoWork();
}
}
}
The DoWork
method uses either ServiceA
or ServiceB
depending on the value
parameter. The important thing is that the method does not simultaneously use both services, but only one of them.
Consider this example of using the MyService
service:
public void MyMethod()
{
// Using the container directly is for clarity only.
var service = container.Resolve<IMyService>();
service.DoWork(1);
}
In this case, only ServiceA
needs to be used. However, when resolving MyService
from the container, both ServiceA
and ServiceB
will be created as well as all other nested dependencies. Thus, instead of instantiating (1 + N)
services, all (2 + N + M)
are created.
The following example illustrates another case where only part of the dependencies is used:
public class MyService : IMyService
{
private readonly IServiceA _serviceA;
private readonly IServiceB _serviceB;
public MyService(IServiceA serviceA, IServiceB serviceB)
{
_serviceA = serviceA;
_serviceB = serviceB;
}
public void DoWorkA()
{
_serviceA.DoWork();
}
public void DoWorkB()
{
_serviceB.DoWork();
}
}
When calling the DoWorkA
method, only ServiceA
is used. ServiceB
is not needed in this case, but it was still created when the MyService
service resolving from the container:
public void MyMethod()
{
// Using the container directly is for clarity only.
var service = container.Resolve<IMyService>();
service.DoWorkA();
}
Of course, the examples given are too simple for real life and are given only for clarity. However, in practice, often, similar situations arise.
To summarize, the problem with dependency injection is that it creates multiple instances that are not even used when the method is called. It leads to:
- Increased consumption of CPU time to create unused service instances. Especially important if for some reason there are slow constructors in those services.
- Increased memory consumption due to allocation for unused service instances.
- Decreased performance due to increased load on the garbage collector.
Solutions
This problem can be solved in several ways.
Singletons
The specified problem becomes negligible if the services are registered in an IoC container as singletons. In this case, the problem only affects the first resolution of the service. However, in many cases, making all services singletons is a rather problematic task, especially for legacy code.
More Dependencies
In some cases, the service can be divided into dependencies by reducing responsibility and methods. For instance, in the second example above, the DoWorkA
method and the DoWorkB
method could be divided into different services. But this approach may not always help, as can be seen from the first example above when the DoWork
method uses a condition by value
.
Fewer Dependencies
Sometimes, combining several services into one can reduce the depth of the dependency tree nesting. It is especially helpful when services are overly segregated. However, this can negatively impact code reusability and testability.
ServiceLocator
Instead of injecting services, some use different service locators. However, this is considered to be an anti-pattern that breaks encapsulation, so ideally you want to avoid it.
Inject Lazy<T> Instead of T
Using Lazy<T>
rather than the T
solves the indicated problem.
The implementation of the first example changes as follows:
public class MyService : IMyService
{
private readonly Lazy<IServiceA> _serviceA;
private readonly Lazy<IServiceB> _serviceB;
public MyService(Lazy<IServiceA> serviceA, Lazy<IServiceB> serviceB)
{
_serviceA = serviceA;
_serviceB = serviceB;
}
public void DoWork(int value)
{
if (value < 42)
{
_serviceA.Value.DoWork();
}
else
{
_serviceB.Value.DoWork();
}
}
}
In this case, two Lazy<T>
instances are created when the MyService
service is resolved. (1 + N)
more services are resolved only when the DoWork
method is executed with the value == 1
(as in the example above). In total, instead of (2 + N + M)
created instances, only (3 + N)
are created.
The difference is more significant, the more dependencies the MyService
service has, and method branches use the more specific services.
The downside of this approach is that it pollutes the code with Lazy<T>
and someServiceLazy.Value
making it less readable.
LazyProxy
LazyProxy solves the above problem without requiring you to change your service code at all.
This library allows you to generate at runtime a type that implements a given interface and proxies all members of this interface to calls through Lazy<T>
.
The following example shows how LazyProxy
works.
Assume there is the following interface:
public interface IMyService
{
void Foo();
}
Then a lazy proxy type can be generated this way:
var proxyType = LazyProxyBuilder.GetType<IMyService>();
The generated type looks like this:
// In reality, the implementation is a little more complicated,
// but the details are omitted for ease of understanding.
public class LazyProxyImpl_IMyService : IMyService
{
private Lazy<IMyService> _service;
public LazyProxyImpl_IMyService(Lazy<IMyService> service)
{
_service = service;
}
public void Foo() => _service.Value.Foo();
}
The generated type hides all of the boilerplate code of using Lazy<T>
. So, LazyProxy
can eliminate the main disadvantage of lazy injection but keep the main advantage.
LazyProxy Registration
Generating a proxy type at runtime is only half the battle. It is necessary to register this type in the container somehow and preserve the ability to resolve a real type when calling _service.Value
. This can be achieved in several ways.
Named registrations
Named registrations can be used to register and resolve a real service. This method can only be used for containers that support named registrations.
Here is a simple example of how a proxy type and a real type can be registered in the Autofac
container:
const string registrationName = "RealService";
// Creating a container builder.
var builder = new ContainerBuilder();
// Registering a type mapping for the real service.
builder
.RegisterType<MyService>()
.Named<IMyService>(registrationName);
// Registering a type mapping for the lazy proxy.
builder
.Register(c =>
{
var context = c.Resolve<IComponentContext>();
return LazyProxyBuilder.CreateInstance(
() => context.ResolveNamed<IMyService>(registrationName));
})
.As<IMyService>();
// Building the container.
using var container = builder.Build();
// Resolving the lazy proxy.
var lazyProxy = container.Resolve<IMyService>();
Marker interface
Another way is generating an additional marker interface at runtime that will implement the main interface and be used to register and resolve a real service.
This method is a little more complicated to implement than named registrations, as it requires a runtime code generation. Simultaneously, this method is universal, as it can work for containers that do not support named registrations.
LazyProxy for IoC Containers
There are already ready-made libraries for some IoC containers that implement the described approach.
LazyProxy.Autofac
LazyProxy.Autofac implements lazy registrations for the Autofac
container.
Here is an example of how to use this library:
var builder = new ContainerBuilder();
builder.RegisterLazy<IFoo, Foo>();
using var container = builder.Build();
var lazyProxy = container.Resolve<IFoo>();
LazyProxy.Unity
There is also an implementation for the Unity
container named LazyProxy.Unity.
The syntax is similar to the previous example:
using var container = new UnityContainer();
container.RegisterLazy<IFoo, Foo>();
var lazyProxy = container.Resolve<IFoo>();
Conclusion
Dependency Injection is a powerful tool and standard for achieving quality code. However, it has the disadvantage that multiple instances are unnecessarily created. It negatively affects performance and memory consumption.
There are several solutions that you should use whenever possible to mitigate the negative impact of dependency injection.
One solution is using the Lazy<T>
injection instead of T
. However, such injection pollutes the code, so this use is not recommended.
Instead, it is suggested to use the LazyProxy
library, which allows you to get lazy injection benefits while still getting clean code. This library can be used for various containers to register lazy dependencies. There are implementations for the Unity
and the Autofac
containers that allow you to simply register lazy dependencies.
GitHub Repositories
Posted on November 21, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.