Keyed Services in .NET

With the advent of .NET 8, the built-in Dependency Injection (DI) system in .NET got a nifty update – Keyed Services. This feature allows for greater flexibility and control when resolving dependencies, especially in scenarios where multiple implementations of a service interface are required. In this post, we’ll explore what Keyed Services are, how they work and explore a practical use-case.

What are Keyed Services?

Keyed Services in .NET 8 allow you to register multiple implementations of the same interface and distinguish between them using a key. In a consuming class, when requesting an implementation of an interface via DI, you specify this key and .NET will provide the associated implementation for you. This is quite useful in complex applications where different components may need specific implementations of a service based on context or configuration.

Multiple Registrations Without Keyed Registrations

Prior to .NET 8, you could certainly setup multiple implementations of a service in the DI Engine but accessing them was a bit clumsy.

For instance, you could setup two distinct implementations of ISomeService like this:

// Setting up services (usually in Program.cs)
builder.Services.AddScoped<ISomeService, ServiceA>();
builder.Services.AddScoped<ISomeService, ServiceB>();

If you later requested ISomeService via DI, elsewhere in your application, .NET will give you the last registration that was made:

using System.Data;

namespace ExploreKeyedServices;

public class SomeService
{
    private readonly ISomeService service;

    public SomeService(ISomeService service)
    {
        this.service = service; // This is ServiceB
    }
}

Alternatively, you can get all implementations that were registered by requesting it as an IEnumerable:

using System.Data;

namespace ExploreKeyedServices;

public class SomeService
{
    private readonly IEnumerable<ISomeService> services;

    public SomeService(IEnumberable<ISomeService> services)
    {
        this.services = services; // This list has both ServiceA and ServiceB
    }
}

You can potentially inspect the implementation details to distinguish between ServiceA and ServiceB.

Keyed Registrations

In .NET 8, you can provide a key along with each registration and later use the same key to retrieve the specific implementation that you need:

// Keyed Registrations
builder.Services.AddKeyedSingleton<ISomeService, ServiceA>("serviceA");
builder.Services.AddKeyedSingleton<ISomeService, ServiceB>("serviceB");

// later, elsewhere in your app
app.MapGet("/red", ([FromKeyedServices("serviceA")] ISomeService serviceA) => serviceA.DoSomething());
app.MapGet("/blue", ([FromKeyedServices("serviceB")] ISomeService serviceB) => serviceB.DoSomething());

A Practical Use Case

One use-case that I found for keyed-services is connecting to multiple databases from within a single application. I can key the IDbConnection registrations with the names of the particular databases and then request them by name. Here’s a simplified version of what that looks like:

using System.Data;
using System.Data.SqlClient;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddKeyedScoped<IDbConnection, SqlConnection>("red", (_, _) =>
{
    var connection = new SqlConnection("Server=localhost;Database=RedDatabase;User=sa;Password=myPass;");
    return connection;
});

builder.Services.AddKeyedScoped<IDbConnection, SqlConnection>("blue", (_, _) =>
{
    var connection = new SqlConnection("Server=localhost;Database=BlueDatabase;User=sa;Password=otherPass;");
    return connection;
});

builder.Services.AddControllers();

var app = builder.Build();

app.MapGet("/blue", ([FromKeyedServices("blue")] IDbConnection blueDatabase) =>
{
    var message = "I connected to the blue database";
    DoStuff(blueDatabase, message);
    return message;
});
app.MapGet("/red", ([FromKeyedServices("red")] IDbConnection redDatabase) =>
{
    var message = "I connected to the red database";
    DoStuff(redDatabase, message);
    return message;
});

void DoStuff(IDbConnection dbConnection, string message)
{
    if (dbConnection.State != ConnectionState.Open)
        dbConnection.Open();

    using var cmdSQL = dbConnection.CreateCommand();
    cmdSQL.CommandText = "INSERT INTO tests(message) VALUES(@message);";
    cmdSQL.Parameters.Clear();
    cmdSQL.Parameters.Add(new SqlParameter("@message", message));
    cmdSQL.CommandType = CommandType.Text;
    cmdSQL.ExecuteNonQuery();
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

Above, you’ll see two registrations of IDbConnection, keyed as “red” and “blue”, with a connection to the RedDatabse and the BlueDatabase, respectively. There are two API endpoints — /red and /blue and both take a dependency on IDbConnection. However, these dependencies are keyed allowing the red endpoint to connect specifically to the RedDatabase while allowing the blue endpoint to connect to BlueDatabase.

Closing Thoughts

Although not a game-changer, it’s certainly a welcome quality-of-life improvement to the .NET Dependency Injection framework. By enabling the registration and resolution of services using unique keys, this feature simplifies the handling of complex scenarios and enhances the flexibility of your application’s architecture. Whether you’re connecting to multiple databases or developing multi-tenant applications, Keyed Services provides an elegant way to wire up and resolve multiple implementations of the same interface.

Leave a Comment

Your email address will not be published. Required fields are marked *