Dependency injection (DI) isn’t a new concept in the .NET space, but the ASP.NET team made it a mainstream feature of ASP.NET Core. ASP.NET Core ships with a default service location mechanism that may behave differently than the previous inversion of control (IoC) products.

In my opinion, one of the most powerful features of leveraging DI is the ability to create processing pipelines. In this post, we’ll see how we can build a straight-forward pipeline within ASP.NET, by registering multiple implementations in our services collection.

Services Collection

We can register our dependencies in the ConfigureServices method in our Startup class for those unfamiliar with ASP.NET. This method allows us to enroll implementations in the services collection and define an implementation’s lifetime scope. Additionally, we may see compound registration methods for frameworks like MVC, Razor Pages, and other technologies.

public void ConfigureServices(IServiceCollection services)
{
    // compound registrations
    services.AddControllers();
    services.AddRazorPages();
    // single registration
    services.AddSingleton<IWidget>(new Widget());
    services.AddScoped<IWidget>((sp) => new Widget());
    services.AddTransient<IWidget>(sp => new Widget());
}

There are three kinds of lifetime scopes: Transient, Scoped, and Singleton. For those interested, read more about service lifetimes at Microsoft docs.

Registering An Interfaced Service

Let’s assume we have an HTTP API that echoes a user value back, enriched with trailing emojis. First, we’ll need an enriching interface.

public interface IEmoji
{
    void Apply(ref string value);
}

Simple enough, we take in a string and mutate its value. Let’s implement a version of the interface that adds a smile (😀) emoji.

public class Smile : IEmoji
{
    public void Apply(ref string value)
    {
        value += "😀";
    }
}

Let’s see how we might register the Smile class into our service collection. We’ll use Singleton as this class is stateless. Our first instinct might be to add the Smile class as itself, hoping that ASP.NET would know what interfaces the class implements. This would be wrong.

// wrong 
services.AddSingleton<Smile>();

We need to inform ASP.NET what we plan on accessing this implementation using the explicit type. In this case, we will access the Smile class utilizing the IEmoji interface.

services.AddSingleton<IEmoji, Smile>();

Now, we can apply our IEmoji interface to any constructor that requires its use.

public HomeController(IEmoji emoji)
{
    this.emoji = emoji;
}

This registration style works great for single implementation dependencies, but what happens when we have more than one?

Registering Multiple Services

We’re in luck! ASP.NET provides a mechanism for registering multiple implementations of a single interface. Let’s implement a few more instances of IEmoji.

public interface IEmoji
{
    void Apply(ref string value);
}

public class Smile : IEmoji
{
    public void Apply(ref string value)
    {
        value += "😀";
    }
}

public class Apple : IEmoji
{
    public void Apply(ref string value)
    {
        value += "🍎";
    }
}

public class ThumbsUp : IEmoji
{
    public void Apply(ref string value)
    {
        value += "👍";
    }
}

Remember how we needed to register our single implementation with the IEmoji interface? Well, we need to do the same with all the new implementations.

services.AddSingleton<IEmoji, Smile>();
services.AddSingleton<IEmoji, Apple>();
services.AddSingleton<IEmoji, ThumbsUp>();

Additionally, our class constructor needs to adapt to allow for multiple instances of IEmoji.

public HomeController(IEnumerable<IEmoji> emojis)
{
    this.emojis = emojis ?? Array.Empty<IEmoji>();
}

Great! Here are some caveats that I found with registering and consuming multiple dependencies of the same interface type:

  • The IEnumerable generic type must be the type the registered type within ConfigureServices.
  • Registration order matters. The order of registration translates to the resolution of implementations. For example, our array order is Smile, Apple, and then ThumbsUp.
  • If no implementations are registered, ASP.NET will pass a null collection. Watch out!.

Let’s see it in action.

[ApiController]
[Route("[controller]")]
public class HomeController : Controller
{
    private readonly IEnumerable<IEmoji> emojis;

    public HomeController(IEnumerable<IEmoji> emojis)
    {
        this.emojis = emojis ?? Array.Empty<IEmoji>();
    }
    
    [Route("")]
    [HttpGet]
    public IActionResult Index(string value)
    {
        foreach (var emoji in emojis) {
            emoji.Apply(ref value);
        }

        return Ok(new {
            value
        });
    }
}

When we run our ASP.NET application with a query of ?value=Hello, we get the following result.

{
	"value": "Hello😀🍎👍"
}

Awesome!

The Possibilities

What advantages do we get from registering multiple implementations of the same interface? The most significant benefit is the ability to build processing pipelines. It’s how ASP.NET works! ASP.NET has many plugin locations, but utilizing a decoupled DI approach can reduce the ASP.NET Pipeline’s complexity in our custom pipelines.

In our simple demo, we were able to enrich our output with emojis. Each interface altered the previous IEmoji value with more information. We could use the same approach to validate inputs, signal events to multiple processors, and log data to numerous targets. Building a custom pipeline also gives us the benefit of decoupling from ASP.NET, in the chance we want to host our solution in a non-web environment.

Conclusion

The ASP.NET DI infrastructure is robust for most common use cases, but some of its more advanced features can be difficult to see immediately. Multiple implementations registration and utilization is a handy feature once we understand how it works. The quirks are easy to follow, but personally wish that ASP.NET wouldn’t inject empty collections. We injected our dependencies into our ASP.NET controller in this post, but our dependencies are accessible anywhere within the ASP.NET pipeline. Access to the DI infrastructure allows us to take advantage of our dependencies in middleware, action filters, and input/output formatters. The sky is the limit!

I hope you enjoyed this post, and please let me know what kind of process pipelines you’re building in your applications.