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

Managing dependencies is one of the most common but complex things 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 cover what Dependency Injection is and how the .NET framework implemented it in this article; if you are not familiar with this pattern you should take a few 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 does the magic for you) at runtime when you need them.

.NET provides several methods to register a dependency. You can choose one of the three available lifetimes (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 other 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 that has a private member of the IService interface and a method that 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 in the Dependency Injection container, all the implementations will be registered but it's the last one that will be resolved by default.

You may now ask yourself: why register all the instances in order to resolve only the last one? Why not just register the last one?

It's because the Dependency Injection container resolves what you ask for. In our example, if you ask for an IService then the DI container will resolve the last service implementation registered, but you can also 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 will return:
      FirstService
      SecondService

Before using another registration method, just note that we could have used 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 you to add the same implementation several times, so the following 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 the FirstService implementation several times for the IService type in the Dependency Injection container, so if we call our API inspect method (the one that returns 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 has an 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 the output will be 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 keep the first registration for the interface, so the output will be the only instance it has for the requested 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 will only register the first one. When you register an implementation using the TryAdd method, if the DI container already has an implementation registered for the service type it will ignore your next requests without throwing any exception, even if you try to register the same implementation several times.

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, both services will be registered.

This method also has another overload so that it accepts a single service as a 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, 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 first two services will be registered because the uniqueness of the TryAddEnumerable method is based on the combination of the service type and the implementation type, as opposed 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 whether it already has this combination or not, which is the case here, so it just skips 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 even if the second one has a different lifetime than the 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