HomeNikola Knezevic

In this article

Banner

Keyed Services in .NET Core

12 Feb 2026
4 min

Sponsor Newsletter

Dependency Injection (DI) is a cornerstone of modern .NET applications, keeping our code clean, testable and easy to maintain.

However, until recently, DI in .NET had one common limitation and that is handling multiple implementations of the same interface.

That’s where Keyed Services come in, a DI feature introduced in .NET 8.

Keysed Services

Keyed Services allows you to register multiple implementations of the same interface, each identified by a key.

This means you can easily inject different implementations of the same interface, based on context, without any custom factory logic.

This key can be a string, an enum or any other comparable type.

Getting Started

For this example, here's an simple example:

csharp
public interface INotificationService
{
    void Send(string message);
}

public class EmailNotificationService : INotificationService
{
    public void Send(string message) => Console.WriteLine($"Email: {message}");
}

public class SmsNotificationService : INotificationService
{
    public void Send(string message) => Console.WriteLine($"SMS: {message}");
}

Before Keyed Services

Before .NET 8, the built-in dependency injection system didn’t have a straightforward way to register and resolve multiple implementations of the same interface.

If you tried to register both in the container like this:

csharp
builder.Services.AddTransient<INotificationService, EmailNotificationService>();
builder.Services.AddTransient<INotificationService, SmsNotificationService>();

Only the last registered service (SmsNotificationService) would actually be used, because each registration overwrote the previous one.

To make both available, we had to rely on workarounds, such as resolving them with factories or filtering through IEnumerable<T>:

csharp
public class NotificationProcessor(IEnumerable<INotificationService> services)
{
    public void Process()
    {
        foreach (var service in services)
        {
            service.Send("Processing notification...");
        }
    }
}

In short, it was cumbersome and harder to work with.

With Keyed Services

Now, it's as easy as it can be:

csharp
builder.Services.AddKeyedTransient<INotificationService, EmailNotificationService>("email");
builder.Services.AddKeyedTransient<INotificationService, SmsNotificationService>("sms");

.NET 8 introduced AddKeyedTransient, AddKeyedScoped and AddKeyedSingleton for all three lifetimes.

With implementations you just need to add unique keys which can be strings or enums. I personally prefer enums better.

You can inject a concrete implementation like this:

csharp
public class NotificationProcessor(
    [FromKeyedServices("email")] INotificationService emailService,
    [FromKeyedServices("sms")] INotificationService smsService)
{
    public void Process()
    {
        emailService.Send("Processing email notification...");
        smsService.Send("Processing SMS notification...");
    }
}

By using FromKeyedServices attribute and specificying the key, DI will resolve the service accordingly.

If you need to resolve a service dynamically in runtime you can use GetRequiredKeyedService:

csharp
var emailService = serviceProvider.GetRequiredKeyedService<INotificationService>("email");
emailService.Send("Sending via Email Service");

Conclusion

Keyed Services make the built-in .NET DI container far more flexible when working with multiple implementations of the same interface.

Instead of writing custom factories or manual filtering logic, you can now select the exact implementation you need using a simple, explicit key.

If you want to check out examples I created, you can find the source code here:

Source Code

I hope you enjoyed it, subscribe and get a notification when a new blog is up!

Subscribe

Stay tuned for valuable insights every Thursday morning.