Keyed Services in .NET 8 – A Clean Way to Handle Multiple Implementations of an Interface

If you’ve ever worked on a real-world .NET application, you’ve likely run into this situation:
You define an interface, say IPaymentService. Soon you realize you need multiple implementations — one for Stripe, another for PayPal, maybe even Apple Pay.
So far so good. But then comes the real question: when your controller or service asks for IPaymentService, which one should .NET inject?
Until .NET 7, there was no built-in, elegant way to solve this. Developers usually relied on:
Injecting
IEnumerable<T>and writing custom selection logicOr creating factory classes to map providers to services
Both approaches worked, but they introduced unnecessary boilerplate and made the code harder to maintain.
That’s exactly what Keyed Services in .NET 8 solve. Now, you can register multiple implementations of the same interface and resolve them using a simple key — no extra factories, no plumbing code.
In this blog, we’ll cover:
Why this feature is a game-changer
How developers used to handle it before .NET 8
How Keyed Services work (with hands-on code)
Real-world use cases like payments, messaging, and storage
Pros and cons to keep in mind
By the end, you’ll see how Keyed Services can make your applications cleaner, more modular, and much easier to extend.
The Problem
Let’s say we define a simple payment service interface:
public interface IPaymentService
{
string Provider { get; }
string ProcessPayment(decimal amount);
}
Now imagine you have multiple implementations — for example, StripePaymentService and PaypalPaymentService.
So far, so good. But the real challenge comes when your controller needs the right implementation at runtime based on the provider name (e.g., "Stripe" or "PayPal").
In .NET 7, the common solution was to inject all implementations and then build a factory:
public class OldWayPaymentFactory
{
private readonly IEnumerable<IPaymentService> _paymentServices;
public OldWayPaymentFactory(IEnumerable<IPaymentService> paymentServices)
{
_paymentServices = paymentServices;
}
public IPaymentService GetService(string provider)
{
return _paymentServices.FirstOrDefault(s =>
s.Provider.Equals(provider, StringComparison.OrdinalIgnoreCase))
?? throw new Exception("No service found for provider " + provider);
}
}
And then use it in a controller:
[HttpPost("oldway")]
public IActionResult PayOldWay(PaymentRequest request)
{
var service = _oldFactory.GetService(request.Provider);
var result = service.ProcessPayment(request.Amount);
return Ok(result);
}
This works, but notice the extra boilerplate:
Factory class
String matching logic
Manual error handling
All of this just to tell .NET which implementation to inject.
Introducing Keyed Services in .NET 8
.NET 8 finally solves this cleanly with Keyed Services.
We can now register multiple implementations with unique keys:
builder.Services.AddKeyedScoped<IPaymentService, StripePaymentService>("Stripe");
builder.Services.AddKeyedScoped<IPaymentService, PaypalPaymentService>("PayPal");
And resolve them directly by key:
[HttpPost("keyed")]
public IActionResult PayKeyed(PaymentRequest request, [FromServices] IServiceProvider provider)
{
var keyedService = provider.GetRequiredKeyedService<IPaymentService>(request.Provider);
var result = keyedService.ProcessPayment(request.Amount);
return Ok(result);
}
No factory needed
No manual loops
Cleaner, built-in support
How It Works Under the Hood
When we call AddKeyedScoped, the DI container stores the implementation under a key → service mapping.
At runtime, GetRequiredKeyedService<T>(key) looks up that key and returns the right service.
This is built directly into the Microsoft DI container — no Autofac, no custom hacks.
Hands-on Example
Enough theory — let’s see some code. I built a small demo project with two payment providers (Stripe & PayPal) to show both the old and new approaches side by side.
At the core, we have an interface IPaymentService:
public interface IPaymentService
{
string Provider { get; }
string ProcessPayment(decimal amount);
}
Stripe Implementation
public class StripePaymentService : IPaymentService
{
public string Provider => "Stripe";
public string ProcessPayment(decimal amount)
{
return $"Processed {amount:C} via Stripe";
}
}
PayPal Implementation
public class PaypalPaymentService : IPaymentService
{
public string Provider => "PayPal";
public string ProcessPayment(decimal amount)
{
return $"Processed {amount:C} via PayPal";
}
}
Old Way (before .NET 8)
We used a factory to pick the correct implementation:
public class OldWayPaymentFactory
{
private readonly IEnumerable<IPaymentService> _paymentServices;
public OldWayPaymentFactory(IEnumerable<IPaymentService> paymentServices)
{
_paymentServices = paymentServices;
}
public IPaymentService GetService(string provider)
{
return _paymentServices.FirstOrDefault(s =>
s.Provider.Equals(provider, StringComparison.OrdinalIgnoreCase))
?? throw new Exception("No service found for provider " + provider);
}
}
[HttpPost("oldway")]
public IActionResult PayOldWay(PaymentRequest request)
{
var service = _oldFactory.GetService(request.Provider);
var result = service.ProcessPayment(request.Amount);
return Ok(result);
}

New Way (with Keyed Services in .NET 8)
Now, we register services with keys:
builder.Services.AddKeyedScoped<IPaymentService, StripePaymentService>("Stripe");
builder.Services.AddKeyedScoped<IPaymentService, PaypalPaymentService>("PayPal");
And resolve them directly:
[HttpPost("keyed")]
public IActionResult PayKeyed(PaymentRequest request, [FromServices] IServiceProvider provider)
{
var keyedService = provider.GetRequiredKeyedService<IPaymentService>(request.Provider);
var result = keyedService.ProcessPayment(request.Amount);
return Ok(result);
}

Real-world Use Cases
Keyed Services shine when we need multiple interchangeable implementations:
Payments → Stripe, PayPal, Apple Pay: Choose payment provider at runtime without factories.
Messaging → Email, SMS, WhatsApp: Send notifications through the right channel.
Storage → SQL, MongoDB, Redis: Switch between different storage backends easily.
Instead of factories or giant switch statements, we just register and resolve by key.
Pros & Cons
Pros:
Built-in, no external libraries
Minimal boilerplate
Cleaner, more maintainable DI setup
Cons:
- Keys can become “magic strings” — better to use constants or enums
Conclusion
This might look like a small DI improvement, but in real projects it eliminates a lot of unnecessary plumbing code. For teams building modular, extensible apps, Keyed Services are a huge win.
You can explore the full demo code on GitHub.



