DEV Community

loading...
Cover image for Lazy Dependency Injection for .NET

Lazy Dependency Injection for .NET

hypercodeplace profile image Aleksei Ananev Updated on ・6 min read

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:

2

All these services are registered in an IoC container by corresponding interfaces:

container
    .RegisterType<IMyService, MyService>()
    .RegisterType<IServiceA, ServiceA>()
    .RegisterType<IServiceB, ServiceB>()
    ...
Enter fullscreen mode Exit fullscreen mode

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();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

Then a lazy proxy type can be generated this way:

var proxyType = LazyProxyBuilder.GetType<IMyService>();
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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>();
Enter fullscreen mode Exit fullscreen mode

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>();
Enter fullscreen mode Exit fullscreen mode

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>();
Enter fullscreen mode Exit fullscreen mode

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

Discussion (7)

pic
Editor guide
Collapse
slavius profile image
Slavius

Hi,

service locator indeed is an antipattern in the following sense:
Imagine seeing source code of a service which depends on a Service Locator (in Microsoft DI world called ServiceProvider).
It is not immediately clear what the service dependencies are because ServiceProvider offers ServiceCollection which is a list of all services registered at startup.
You would have to dig into the source code of that particular service to figure out which services are actually instantiated from the ServiceCollection. If you forget to add required service to the collection your unit tests will happily compile and run, but will crash at runtime when one of the required services cannot be instantiated.
It is also a nightmare to write tests for such class as anytime the developer reponsible for that service adds new service dependency his class signature will not change but your code and tests will probably crash without any obvious reason.

Another modern approach to lazy instantiating dependencies is Factory pattern. Have a look at EF Core (>=5.0.0). They've introduced DBContextFactory which can be injected into DI instead of DBContext. Also Micosoft's Logging framework uses ILoggerFactory which provides CreateLogger() method. Similarly new approach to HttpClient instantiation allows you to inject IHttpClientFactory that produces pooled HttpClient objects on demand.

Collapse
alexyakunin profile image
Alex Yakunin

I think the key difference here is that LazyProxy doesn't require you to change the "consumption API", and e.g. factory pattern does.

Collapse
cmendibl3 profile image
Carlos Mendible

Dear devs, if you want to test LazyProxy with the default ServiceProvider I just pushed the extension methods and tests here: github.com/cmendible/lazy-proxy-se....

@hypercodeplace how would you feel about adding it to the official repos?

Collapse
hypercodeplace profile image
Aleksei Ananev Author

Hi Carlos,

Thank you for your contribution!

I have looked through the repository and definitely liked it. And I'm happy to add this to the official repos.

Also please look at a pull request with some improvements.

Collapse
cmendibl3 profile image
Carlos Mendible

Hi Aleksei, I just merged the PR! Thanks!

Thread Thread
hypercodeplace profile image
Aleksei Ananev Author

Hi @cmendibl3 ,
Here is the article about LazyProxy.ServiceProvider: dev.to/hypercodeplace/lazy-depende...
Please let me know if something needs improvement.
Thanks.

Collapse
gkarnik05 profile image
Guruprasad Karnik

This is great! Thanks for writing this article.