Keyed Services: Resolving the Right Implementation Without Factory Hacks

You have two implementations of the same interface. Before .NET 8, you either registered a Func<string, IService> factory, built a custom resolver, or stuffed a switch into a single implementation. None of it felt right.

.NET 8 added keyed services to the built-in DI container.

The Problem

Two notification senders, one interface. Registering both means the last one wins:

builder.Services.AddSingleton<INotificationSender, EmailSender>();
builder.Services.AddSingleton<INotificationSender, SmsSender>(); // this one wins

The Fix

Register each with a key:

builder.Services.AddKeyedSingleton<INotificationSender, EmailSender>("email");
builder.Services.AddKeyedSingleton<INotificationSender, SmsSender>("sms");

Inject the one you want with [FromKeyedServices]:

app.MapPost("/notify/email", async (
[FromKeyedServices("email")] INotificationSender sender,
NotificationRequest request) =>
{
await sender.SendAsync(request.To, request.Message);
return Results.Ok();
});

It works in constructor injection too:

public class OrderProcessor(
[FromKeyedServices("email")] INotificationSender emailSender,
[FromKeyedServices("sms")] INotificationSender smsSender)
{
// Both are available, resolved by key
}

Dynamic Resolution

When you don’t know the key at compile time, resolve through IServiceProvider:

app.MapPost("/notify/{channel}", async (string channel, IServiceProvider sp) =>
{
var sender = sp.GetKeyedService<INotificationSender>(channel);
if (sender is null) return Results.BadRequest($"Unknown channel: {channel}");
await sender.SendAsync("[email protected]", "Hello!");
return Results.Ok();
});

Keys can be any object, not just strings. Enums work great:

builder.Services.AddKeyedSingleton<INotificationSender, EmailSender>(Channel.Email);
builder.Services.AddKeyedSingleton<INotificationSender, SmsSender>(Channel.Sms);

Key Takeaway

Keyed services replace hand-rolled factories with a first-class DI concept. Register by key, inject with [FromKeyedServices], and stop writing Func<string, IService> workarounds.