Introduction
.NET 8 introduced several exciting features that improve the developer experience and expand the flexibility of dependency injection (DI). Among these features is one called Keyed Services, which allows you to register and resolve services based on a key. This feature addresses scenarios where you have multiple implementations of an interface and want to resolve them selectively without relying on IEnumerable or more complex logic.
In this blog, we’ll explore what keyed services are, why they’re useful, and how you can implement them in your .NET applications with practical examples.
Why Keyed Services?
Traditionally in .NET, when you had multiple implementations of a service interface, you would resolve all implementations using IEnumerable. Then, you’d either loop through the services and pick the correct one based on some condition, or use named services with custom factories or resolvers.
For example, you might have an interface like the one below:
public interface IGreetingService
{
string Greet(string name);
}
In this scenario, imagine you have multiple implementations of that interface:
public class EnglishGreetingService : IGreetingService
{
public string Greet(string name)
{
return $"Hello, {name}!";
}
}
public class SpanishGreetingService : IGreetingService
{
public string Greet(string name)
{
return $"Hola, {name}!";
}
}
Each of these services is registered in the Startup.cs or Program.cs as a transient service for the interface:
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<IGreetingService, EnglishGreetingService>();
services.AddTransient<IGreetingService, SpanishGreetingService>();
}
To be able to inject these services, you’d need to inject an IEnumerable<IGreetingService> like this:
[ApiController]
[Route("api/[controller]")]
public class GreetingController : ControllerBase
{
private readonly IEnumerable<IGreetingService> _greetingServices;
public GreetingController(IEnumerable<IGreetingService> greetingServices)
{
_greetingServices = greetingServices;
}
[HttpGet("greet")]
public IActionResult Greet(string language, string name)
{
// Select the appropriate service based on the language condition
IGreetingService selectedService = _greetingServices.FirstOrDefault(service =>
{
return (language == "English" && service is EnglishGreetingService) ||
(language == "Spanish" && service is SpanishGreetingService);
});
// Use the selected service to generate the greeting
var greeting = selectedService?.Greet(name) ?? "Language not supported.";
return Ok(greeting);
}
}
As you can see, this is quite complex. Keyed services simplify this pattern by allowing you to assign keys to service registrations, and later resolve the correct implementation using that key. This removes the need for custom logic and makes your DI container setup cleaner and easier to maintain.
Keyed Services in Action
Let’s dive into an example using keyed services.
Suppose you’re building an application where you need different strategies for calculating discounts, depending on the user’s membership level. You might have the following discount strategies:
• BasicMemberDiscount
• PremiumMemberDiscount
• VIPMemberDiscount
All these classes implement a common interface IDiscountService.
public interface IDiscountService
{
decimal CalculateDiscount(decimal totalAmount);
}
public class BasicMemberDiscount : IDiscountService
{
public decimal CalculateDiscount(decimal totalAmount) => totalAmount * 0.05m;
}
public class PremiumMemberDiscount : IDiscountService
{
public decimal CalculateDiscount(decimal totalAmount) => totalAmount * 0.10m;
}
public class VIPMemberDiscount : IDiscountService
{
public decimal CalculateDiscount(decimal totalAmount) => totalAmount * 0.20m;
}
Registering Keyed Services
With keyed services, you can register these implementations with specific string keys.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddKeyedSingleton<IDiscountService, BasicMemberDiscount>("Basic");
builder.Services.AddKeyedSingleton<IDiscountService, PremiumMemberDiscount>("Premium");
builder.Services.AddKeyedSingleton<IDiscountService, VIPMemberDiscount>("VIP");
var app = builder.Build();
In this example, we use AddKeyedSingleton, which is one of the new methods for registering keyed services. You can also use AddKeyedScoped or AddKeyedTransient depending on the service’s lifetime requirements.
Resolving Keyed Services
Once the services are registered, you can resolve them by their key. For instance, you might have a minimal API endpoint where you resolve the discount service based on the user’s membership level.
app.MapGet("/calculate-discount/{membershipLevel}", (string membershipLevel, [FromServices] IServiceProvider serviceProvider, decimal totalAmount) =>
{
var discountService = serviceProvider.GetRequiredKeyedService<IDiscountService>(membershipLevel);
var discount = discountService.CalculateDiscount(totalAmount);
return Results.Ok(new { DiscountedAmount = totalAmount - discount });
});
app.Run();
In this code, instead of injecting IDiscountService directly, we inject a IServiceProvider and use its GetRequiredKeyedService method to resolve the service based on the membershipLevel key. The correct discount strategy is chosen based on the user’s membership level passed in the route.
Keyed Service Factory
.NET 8 also allows you to create factories for keyed services, providing even more flexibility. This can be useful if you need more complex logic for resolving services or if the key is dynamic.
Here’s an example of how to implement a factory for keyed services:
public class DiscountServiceFactory
{
private readonly IServiceProvider _serviceProvider;
public DiscountServiceFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public IDiscountService GetDiscountService(string membershipLevel)
{
return _serviceProvider.GetRequiredKeyedService<IDiscountService>(membershipLevel);
}
}
You can then inject the DiscountServiceFactory into your controllers or services to resolve the appropriate keyed service as needed.
Conclusion
Keyed services in .NET 8 add a powerful new tool to your DI toolkit. They make it easier to manage multiple implementations of an interface without the need for complex resolution logic or cumbersome named service patterns. By leveraging keyed services, you can keep your codebase clean, maintainable, and flexible.
If you’re already using .NET’s built-in dependency injection, incorporating keyed services is a natural evolution that can simplify your service registration and resolution patterns. Whether you’re building a microservice architecture or a large monolithic application, keyed services offer a clear path to cleaner code and better service management.
Whether you’re looking to streamline your dependency injection setup with keyed services or need expert guidance on custom software architecture, Trailhead has the skills and experience to help your team succeed. Contact us today to discuss how we can enhance your project with tailored solutions that fit your unique needs.


