TryAddEnumerable to configure dependency injection

How TryAddEnumerable operates differently to the other DI methods

Home DailyDrop

Daily Knowledge Drop

When using the C# dependency injection container, the:

  • Add methods (AddSingleton, AddScoped, AddTransient) will always add the interface and implementation to the container, even if it results in duplicate registrations
  • TryAdd methods (TryAddSingleton, TryAddScoped, TryAddTransient) will only add the interface and implementation to the container if the interface has not already been registered

Today we are looking specifically at TryAddEnumerable:

  • TryAddEnumerable will only add the interface and implementation to the container if the combination of the interface and implementation has not already been registered

Endpoint example

In the three examples below, we have a simple setup, with an interface and a couple of implementations of the interface:

public interface IServiceInterface { }

public class ServiceImplementationOne: IServiceInterface { }

public class ServiceImplementationTwo : IServiceInterface { }

And then a minimal endpoint which has IEnumerable<IServiceInterface> injected, and returns all implementations:

app.MapGet("/endpoint", (IEnumerable<IServiceInterface> service) =>
{
    return service.Select(s => s.GetType().Name);
});

Add methods

First, lets look at the behavior of the Add methods. These methods will always add the interface and implementation, even if its already been added.

In the dependency injection configuration, in this example, a duplicate implementation of ServiceImplementationTwo is added:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddTransient(typeof(IServiceInterface), 
    typeof(ServiceImplementationOne));
builder.Services.AddTransient(typeof(IServiceInterface), 
    typeof(ServiceImplementationTwo));
builder.Services.AddTransient(typeof(IServiceInterface), 
    typeof(ServiceImplementationTwo));

var app = builder.Build();

The endpoint will return the following - ServiceImplementationTwo has been added to the dependency injection container twice, and is therefor the duplicate was injected into and returned from the endpoint:

[
  "ServiceImplementationOne",
  "ServiceImplementationTwo",
  "ServiceImplementationTwo"
]

TryAdd methods

The TryAdd methods operate differently, and will ensure that only one implementation of the interface is added to the container.

In the dependency injection configuration, in this example, we use the same configuration as in the first example, but with the TryAddTransient method instead of the AddTransient method:

var builder = WebApplication.CreateBuilder(args);

builder.Services.TryAddTransient(typeof(IServiceInterface), 
    typeof(ServiceImplementationOne));
builder.Services.TryAddTransient(typeof(IServiceInterface), 
    typeof(ServiceImplementationTwo));
builder.Services.TryAddTransient(typeof(IServiceInterface), 
    typeof(ServiceImplementationTwo));

var app = builder.Build();

The endpoint will return the following - only the first implementation of IServiceInterface added to the dependency injection container is registered.

[
  "ServiceImplementationOne"
]

Neither of the two registrations of ServiceImplementationTwo are actioned, because an implementation of IServiceInterface (ServiceImplementationOne) was already added.


TryAddEnumerable method

The TryAddEnumerable method is used slightly differently - this method takes a ServiceDescriptor instance, and not a interface and implementation directly.

In the dependency injection configuration, in this example, we use the the same configuration as in the above example, with a slightly different setup. We define three instances of ServiceDescriptor, two of them are describing implementations of ServiceImplementationTwo:

var builder = WebApplication.CreateBuilder(args);

var descriptorOne = new ServiceDescriptor(typeof(IServiceInterface), 
    typeof(ServiceImplementationOne), ServiceLifetime.Transient);
var descriptorTwo = new ServiceDescriptor(typeof(IServiceInterface), 
    typeof(ServiceImplementationTwo), ServiceLifetime.Transient);
var descriptorThree = new ServiceDescriptor(typeof(IServiceInterface), 
    typeof(ServiceImplementationTwo), ServiceLifetime.Transient);

builder.Services.TryAddEnumerable(new[] { descriptorOne, descriptorTwo, descriptorThree });

var app = builder.Build();

The endpoint will return the following - only the first implementation of each unique interface + implementation combination:

[
  "ServiceImplementationOne",
  "ServiceImplementationTwo"
]

Notes

The TryAddEnumerable method is incredible useful, especially as a library author where your library might need to add items to the dependency injection container, but without knowing if another library has already added it. TryAddEnumerable can be used to ensure that the dependency injection container is not "polluted" with duplicate identical implementations, which could potentially cause issues.


References

The .NET dependency injection methods you are not using

Daily Drop 100: 21-06-2022

At the start of 2022 I set myself the goal of learning one new coding related piece of knowledge a day.
It could be anything - some.NET / C# functionality I wasn't aware of, a design practice, a cool new coding technique, or just something I find interesting. It could be something I knew at one point but had forgotten, or something completely new, which I may or may never actually use.

The Daily Drop is a record of these pieces of knowledge - writing about and summarizing them helps re-enforce the information for myself, as well as potentially helps others learn something new as well.
c# .net dependencyinjection tryaddenumerable