Dependency Injection with .NET : the difference between the Add, TryAdd and TryAddEnumerable methods

Manage the dependencies is one of the most common but complex thing to do in development, and a great opportunity to mess your codebase up if you do it wrong. To help you with this issue .NET has introduced since .NET Core 1.0 a wonderful and powerful built-in IoC container which is now part of the framework.

We will not covering what Dependency Injection is and how the .NET framework implemented it in this article, if you are not familiar with this pattern you must take several minutes to read some documentation about it: Dependency injection in .NET.

The overall idea of this IoC container is that you register the dependencies at the application startup, then you resolve them (you ask and the container do the magic for you) at runtime when you need it.

.NET provides several methods to register a dependency. You can choose one of the three available lifetime (Singleton, Transient and Scoped) but do you know the difference between the Add, TryAdd and TryAddEnumerable methods?
Are you able to explain what will differ in the three implementations below?


builder.Services.AddSingleton<IService, FirstService>();
builder.Services.AddSingleton<IService, SecondService>();

builder.Services.TryAddSingleton<IService, FirstService>();
builder.Services.TryAddSingleton<IService, SecondService>();

var firstDescriptor = new ServiceDescriptor(typeof(IService), typeof(FirstService), ServiceLifetime.Singleton);
var secondDescriptor = new ServiceDescriptor(typeof(IService), typeof(SecondService), ServiceLifetime.Singleton);
builder.Services.TryAddEnumerable(new [] { firstDescriptor, secondDescriptor });

Note: Singleton is not a relevant choice here, the behavior would be exactly the same with the two others service lifetimes.

The context

Assume we have an interface implemented by two services (we don't care about the implementation here, we just want to see how the Dependency Injection container will register and resolve them):


public interface IService
{
}

public class FirstService : IService
{
}

public class SecondService : IService
{
}

Assume now we have a basic controller who has a private member of the interface IService and a method who returns the name of the type of the resolved service:


public class DependencyInjectionInspectionController : ControllerBase
{
        private readonly IService _service;

        public DependencyInjectionInspectionController(IService service)
        {
             _service = service;
        }

        [HttpGet("inspect")]
        public IActionResult Inspect()
        {
             return Ok(_service.GetType().Name);
        }
}

Case one: we use the Add method

Do you know which type will be returned by the API endpoint if we register the services like this:


builder.Services.AddSingleton<IService, FirstService>();
builder.Services.AddSingleton<IService, SecondService>();

The answer is: SecondService.

Why? Because when you register several implementations of the same interface with the Add method to the Dependency Injection container, all the implementations will be registered but it's the last one who will be resolved by default.

You maybe ask yourself now: Why registering all the instance in order to resolving only the last one? Why just not register the last one?

It because the Dependency Injection container resolves what you ask. In our example, if you ask for a IService then the DI container will resolve the last service implementation registered but you also can ask for all the registered implementations.

Assume we now have this code for our controller:


public class DependencyInjectionInspectionController : ControllerBase
{
        private readonly IEnumerable<IService> _services;

        public DependencyInjectionInspectionController(IEnumerable<IService> services)
        {
              _services = services;
        }

        [HttpGet("inspect")]
        public IActionResult Inspect()
        {
              return Ok(services.Select(x=> x.GetType().Name));
        }
}

The response of the inspect endpoint will now be different and returning:
      FirstService
      SecondService

Before using another registration method, just note that we could have use a service descriptor to register our services, the implementation would have been:


var firstDescriptor = new ServiceDescriptor(typeof(IService), typeof(FirstService), ServiceLifetime.Singleton);
builder.Services.Add<IService, firstDescriptor>();

var secondDescriptor = new ServiceDescriptor(typeof(IService), typeof(SecondService), ServiceLifetime.Singleton);
builder.Services.Add();
Another important aspect of this method is that it allows to add several times the same implementation, so the next code is completely valid:

builder.Services.AddSingleton<IService, FirstService>();
builder.Services.AddSingleton<IService, FirstService>();

Not only is this code valid but it will effectively register several times the FirstService implementation for the type IService in the Dependency Injection container so if we call our API inspect method (the one who return a collection) it will return:
      FirstService
      FirstService

Note that this service is not really a singleton anymore because we have several instances of it, we are in a strange case here but it's just to show you the different behaviors of the Dependency injection container methods.

Case two: the TryAdd method

Assume we have our first controller which have a IService private member and this basic service registration:


builder.Services.TryAddSingleton<IService, FirstService>();

What will be the output of our inspect method?

The answer is pretty obvious here, the endpoint will return FirstService.
But now, do you know what will be the output if we add a second registration for the same interface?


builder.Services.TryAddSingleton<IService, FirstService>();
builder.Services.TryAddSingleton<IService, SecondService>();

With the Add method remember that only the last implementation was returned by the DI container, note now that its behavior is different with the TryAdd method: in this case the DI container will only register the first registration for the interface so the output will be the only instance he has for the asked interface, which is FirstService.

We just saw a major difference between the Add and the TryAdd methods which is: the Add method will register all the implementations you want for a service type, unlike the TryAdd method which only will register the first one. When you register an implementation using the TryAdd method, if the DI container already has an implementation registrered for the service's type he will ignore your next requests without throwing any exception, even if you try to register several times the same implementation.

Case three: the TryAddEnumerable method

The TryAddEnumerable method allows you to register multiple implementations in the same call by using service descriptors.

Assume we have this code:


var firstDescriptor = new ServiceDescriptor(typeof(IService), typeof(FirstService), ServiceLifetime.Singleton);
var secondDescriptor = new ServiceDescriptor(typeof(IService), typeof(SecondService), ServiceLifetime.Singleton);

builder.Services.TryAddEnumerable(new [] { firstDescriptor, secondDescriptor });

In our example, the both services will be registered.

This method also has another overloads so it accept a single service as parameter like the Add and the TryAdd methods:


var firstDescriptor = new ServiceDescriptor(typeof(IService), typeof(FirstService), ServiceLifetime.Singleton);
builder.Services.TryAddEnumerable(firstDescriptor);

var secondDescriptor = new ServiceDescriptor(typeof(IService), typeof(SecondService), ServiceLifetime.Singleton);
builder.Services.TryAddEnumerable(secondDescriptor);

In this example, the both services will be registered.

Now assume we have this code:


var firstDescriptor = new ServiceDescriptor(typeof(IService), typeof(FirstService), ServiceLifetime.Singleton);
builder.Services.TryAddEnumerable(firstDescriptor);

var secondDescriptor = new ServiceDescriptor(typeof(IService), typeof(SecondService), ServiceLifetime.Singleton);
builder.Services.TryAddEnumerable(secondDescriptor);
builder.Services.TryAddEnumerable(secondDescriptor);

With this implementation, only the two first services will be registered because the uniqueness of the TryAddEnumerable method is based on the combination of the service type and the implementation type, in opposite to the TryAdd method for which the uniqueness is only based on the service type. So with the TryAddEnumerable method, when in our example we try to register our second service description for the second time, the Dependency Injection container checks if he already has this combination or not, which is the case here, so he just skip this registration.

Note that the uniqueness check is lifetime agnostic, it means that you can't register the same combination of service type and implementation event if the second one has a different lifetime that de first.

Assume we have this code:


var singletonDescriptor = new ServiceDescriptor(typeof(IService), typeof(FirstService), ServiceLifetime.Singleton);
builder.Services.TryAddEnumerable(singletonDescriptor);

var transientDescriptor = new ServiceDescriptor(typeof(IService), typeof(FirstService), ServiceLifetime.Transient);
builder.Services.TryAddEnumerable(transientDescriptor);

In this example, only the first implementation will be registered, the second will not.

Summary

June 06, 2022
  • .NET
  • ASP.NET Core
  • .NET 6
  • .NET 5
  • .NET 7
  • Dependency Injection
  • Inversion Of Control