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 winsThe 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}");
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.