.NET 8 Dependency Injection: A Beginner’s Guide to Keyed Services
Learn how to use Keyed Services in .NET 8 to manage multiple implementations of the same interface cleanly. Discover real-world examples, enum-based keys, and best practices for cleaner Dependency Injection.
🌐 Introduction
Dependency Injection (DI) is one of the core building blocks of modern .NET applications. But what happens when you have multiple implementations of the same interface — for example, sending notifications through Email, SMS, and Push channels?
Before .NET 8, developers had to rely on factories, filtering logic, or even service locators to manage this complexity. It worked, but it wasn’t pretty.
Enter Keyed Services, a brand-new feature in .NET 8 that makes it incredibly easy to inject the exact implementation you need — without hacks or boilerplate code.
In this guide, we’ll walk through what Keyed Services are, how to use them, and where they fit best in your .NET projects — with simple, real-world examples anyone can follow.
💡 What Are Keyed Services?
Keyed Services let you register multiple implementations of the same interface, each tagged with a unique key (like a string or enum).
At runtime, you can directly inject or resolve the right one — cleanly, safely, and without extra logic.
Example:
builder.Services.AddKeyedScoped<INotificationService, EmailNotificationService>("email");
builder.Services.AddKeyedScoped<INotificationService, SmsNotificationService>("sms");
Then, you can inject them like this:
public class NotificationHandler(
[FromKeyedServices("email")] INotificationService emailService,
[FromKeyedServices("sms")] INotificationService smsService)
{
public async Task HandleAsync()
{
await emailService.SendAsync("Welcome Email");
await smsService.SendAsync("OTP via SMS");
}
}
It’s clean, concise, and completely in line with DI principles.
🚧 The Old Way (Before .NET 8)
Before Keyed Services, developers had to choose between three less-than-ideal options:
- Inject all implementations using
IEnumerable<T>and filter them manually - Build a custom factory that returned the correct service based on a condition
- Manually resolve services using
IServiceProvider(which breaks DI purity)
All of these worked, but they added unnecessary complexity, made unit testing harder, and cluttered business logic.
⚙️ How to Use Keyed Services
Let’s break it down step by step.
Step 1 – Define an Interface
public interface INotificationService
{
Task SendAsync(string message);
}
Step 2 – Implement It in Different Ways
public class EmailNotificationService : INotificationService
{
public Task SendAsync(string message)
{
Console.WriteLine($"Email sent: {message}");
return Task.CompletedTask;
}
}
public class SmsNotificationService : INotificationService
{
public Task SendAsync(string message)
{
Console.WriteLine($"SMS sent: {message}");
return Task.CompletedTask;
}
}
Step 3 – Register Each with a Key
builder.Services.AddKeyedScoped<INotificationService, EmailNotificationService>("email");
builder.Services.AddKeyedScoped<INotificationService, SmsNotificationService>("sms");
Step 4 – Inject and Use
public class NotificationProcessor(
[FromKeyedServices("email")] INotificationService emailService,
[FromKeyedServices("sms")] INotificationService smsService)
{
public async Task ProcessAsync()
{
await emailService.SendAsync("Welcome Email");
await smsService.SendAsync("OTP via SMS");
}
}
And done — you now have clean, readable DI logic with no extra layers.
⚡ Handling Dynamic Scenarios
What if the channel isn’t known until runtime (say, a user selects it)?
You can still resolve keyed services dynamically:
public class DynamicNotificationHandler(IServiceProvider provider)
{
public async Task SendAsync(string channel, string message)
{
var service = provider.GetKeyedService<INotificationService>(channel);
if (service is null)
throw new InvalidOperationException("Unsupported channel");
await service.SendAsync(message);
}
}
This pattern works beautifully for multi-channel systems or tenant-based architectures.
🧱 Using Enums Instead of Strings (Best Practice)
Using strings like "email" or "sms" is simple, but it can lead to typos or mismatched keys.
Instead, define an enum for strong typing and better maintainability:
public enum NotificationChannel
{
Email,
Sms,
Push
}
Now register and use your services with enum keys:
builder.Services.AddKeyedScoped<INotificationService, EmailNotificationService>(NotificationChannel.Email);
builder.Services.AddKeyedScoped<INotificationService, SmsNotificationService>(NotificationChannel.Sms);
And inject them safely:
public class NotificationHandler(
[FromKeyedServices(NotificationChannel.Email)] INotificationService emailService,
[FromKeyedServices(NotificationChannel.Sms)] INotificationService smsService)
{
public async Task HandleAsync()
{
await emailService.SendAsync("Enum-based Email");
await smsService.SendAsync("Enum-based SMS");
}
}
Using enums gives you type safety, better IntelliSense, and cleaner code in large solutions.
🧩 Works with All Service Lifetimes
Keyed Services can be registered with any lifetime — Singleton, Scoped, or Transient:
builder.Services.AddKeyedSingleton<IMyService, A>("a");
builder.Services.AddKeyedScoped<IMyService, B>("b");
builder.Services.AddKeyedTransient<IMyService, C>("c");
They integrate seamlessly with the existing DI container — no extra setup required.
🚦 When to Use (and When to Avoid)
✅ Use Keyed Services When:
- You have multiple implementations of the same interface
- You choose an implementation at runtime (based on user input or configuration)
- You want to eliminate factory or if-else logic
- You need a cleaner, testable DI setup
🚫 Avoid Keyed Services When:
- You only have one implementation (no need for keys)
- You’re injecting IServiceProvider everywhere (a sign of bad design)
- A simple strategy pattern would work better
Think of Keyed Services as a tool for clarity, not a replacement for good architecture.
🏁 Conclusion
Keyed Services in .NET 8 are a long-awaited improvement for developers who deal with multiple interface implementations.
They remove the need for factories, eliminate redundant logic, and make dependency injection simpler and more intuitive.
Whether you’re sending notifications, handling payments, or managing multi-tenant logic — Keyed Services let you pick the right service cleanly and confidently.
Use them wisely, and your DI setup will stay elegant, testable, and scalable for years to come.
