Lazy Dependency Injection for .NET

hypercodeplace

Aleksei Ananev

Posted on November 21, 2020

Lazy Dependency Injection for .NET

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

💖 💪 🙅 🚩
hypercodeplace
Aleksei Ananev

Posted on November 21, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related