- Home
- .NET tutorials
- Use keyed services for multiple implementations of a service
Use keyed services for multiple implementations of a service
Published: Monday 7 July 2025
Keyed services allows you to add multiple implementations of the same service.
How to add keyed services in ASP.NET Core
We've set up the following services:
public class CategorySingletonService
{
public DateTime UtcTime { get; }
public CategorySingletonService()
{
UtcTime = DateTime.UtcNow;
}
}
public class CategoryScopedService
{
public DateTime UtcTime { get; }
public CategoryScopedService([FromKeyedServices("categorysingleton1")] CategorySingletonService categorySingletonService)
{
UtcTime = DateTime.UtcNow;
}
}
public class CategoryTransientService
{
public DateTime UtcTime { get; }
public CategoryTransientService()
{
UtcTime = DateTime.UtcNow;
}
}
public interface IProductSingletonService
{
DateTime UtcTime { get; }
}
public interface IProductScopedService
{
DateTime UtcTime { get; }
}
public interface IProductTransientService
{
DateTime UtcTime { get; }
}
public class ProductSingletonService : IProductSingletonService
{
public DateTime UtcTime { get; }
public ProductSingletonService()
{
UtcTime = DateTime.UtcNow;
}
}
public class ProductScopedService : IProductScopedService
{
public DateTime UtcTime { get; }
public ProductScopedService([FromKeyedServices("productsingleton1")] IProductSingletonService productSingletonService)
{
UtcTime = DateTime.UtcNow;
}
}
public class ProductTransientService : IProductTransientService
{
public DateTime UtcTime { get; }
public ProductTransientService()
{
UtcTime = DateTime.UtcNow;
}
}
Like with adding a service, there are a number of IServiceCollection
extension methods in Program.cs
. However, you add Keyed
to the extension method so they become keyed services. So you would choose either AddKeyedSingleton
, AddKeyedScoped
or AddKeyedTransient
depending on the service lifetime you wish to register the service.
What makes these extension methods different is that you have to add a key. The key is used to distinguish between multiple registrations of the same service type. It's an object
type so it could be either a string, integer or a boolean to name a few.
You can also use the TryAddKeyedSingleton
, TryAddKeyedScoped
and TryAddKeyedTransient
extension methods to register keyed services. Like with adding services, the difference is that the Try
extension methods will only add the keyed service if one has not already been registered with the same type and key.
These are the different IServiceCollection
extension methods available.
Add service type as a parameter
This is where you specify the service type as a parameter.
// Program.cs
builder.Services.AddKeyedSingleton(typeof(CategorySingletonService), (object)"categorysingleton1");
builder.Services.AddKeyedSingleton(typeof(CategorySingletonService), (object)"categorysingleton2");
builder.Services.AddKeyedScoped(typeof(CategoryScopedService), "categoryscoped1");
builder.Services.AddKeyedScoped(typeof(CategoryScopedService), "categoryscoped2");
builder.Services.AddKeyedTransient(typeof(CategoryTransientService), "categorytransient1");
builder.Services.AddKeyedTransient(typeof(CategoryTransientService), "categorytransient2");
builder.Services.TryAddKeyedSingleton(typeof(CategorySingletonService), (object)"categorysingleton1");
builder.Services.TryAddKeyedSingleton(typeof(CategorySingletonService), (object)"categorysingleton2");
builder.Services.TryAddKeyedScoped(typeof(CategoryScopedService), "categoryscoped1");
builder.Services.TryAddKeyedScoped(typeof(CategoryScopedService), "categoryscoped2");
builder.Services.TryAddKeyedTransient(typeof(CategoryTransientService), "categorytransient1");
builder.Services.TryAddKeyedTransient(typeof(CategoryTransientService), "categorytransient2");
builder.Services.AddKeyedSingleton(typeof(IProductSingletonService), "productsingleton1", typeof(ProductSingletonService));
builder.Services.AddKeyedSingleton(typeof(IProductSingletonService), "productsingleton2", typeof(ProductSingletonService));
builder.Services.AddKeyedScoped(typeof(IProductScopedService), "productscoped1", typeof(ProductScopedService));
builder.Services.AddKeyedScoped(typeof(IProductScopedService), "productscoped2", typeof(ProductScopedService));
builder.Services.AddKeyedTransient(typeof(IProductTransientService), "producttransient1", typeof(ProductTransientService));
builder.Services.AddKeyedTransient(typeof(IProductTransientService), "producttransient2", typeof(ProductTransientService));
builder.Services.TryAddKeyedSingleton(typeof(IProductSingletonService), "productsingleton1", typeof(ProductSingletonService));
builder.Services.TryAddKeyedSingleton(typeof(IProductSingletonService), "productsingleton2", typeof(ProductSingletonService));
builder.Services.TryAddKeyedScoped(typeof(IProductScopedService), "productscoped1", typeof(ProductScopedService));
builder.Services.TryAddKeyedScoped(typeof(IProductScopedService), "productscoped2", typeof(ProductScopedService));
builder.Services.TryAddKeyedTransient(typeof(IProductTransientService), "producttransient1", typeof(ProductTransientService));
builder.Services.TryAddKeyedTransient(typeof(IProductTransientService), "producttransient2", typeof(ProductTransientService));
These extension methods are only resolved at runtime. So if you wrongly registered the IProductTransientService
service with the ProductScopedService
implementation for example, the application would throw the exception when the application is run.
Add service type using the generic class type extension methods
If you want to capture these exceptions when compiled, you can specify the service type using the generic class type extension methods:
// Program.cs
builder.Services.AddKeyedSingleton<CategorySingletonService>("categorysingleton1");
builder.Services.AddKeyedSingleton<CategorySingletonService>("categorysingleton2");
builder.Services.AddKeyedScoped<CategoryScopedService>("categoryscoped1");
builder.Services.AddKeyedScoped<CategoryScopedService>("categoryscoped2");
builder.Services.AddKeyedTransient<CategoryTransientService>("categorytransient1");
builder.Services.AddKeyedTransient<CategoryTransientService>("categorytransient2");
builder.Services.TryAddKeyedSingleton<CategorySingletonService>("categorysingleton1");
builder.Services.TryAddKeyedSingleton<CategorySingletonService>("categorysingleton2");
builder.Services.TryAddKeyedScoped<CategoryScopedService>("categoryscoped1");
builder.Services.TryAddKeyedScoped<CategoryScopedService>("categoryscoped2");
builder.Services.TryAddKeyedTransient<CategoryTransientService>("categorytransient1");
builder.Services.TryAddKeyedTransient<CategoryTransientService>("categorytransient2");
builder.Services.AddKeyedSingleton<IProductSingletonService, ProductSingletonService>("productsingleton1");
builder.Services.AddKeyedSingleton<IProductSingletonService, ProductSingletonService>("productsingleton2");
builder.Services.AddKeyedScoped<IProductScopedService, ProductScopedService>("productscoped1");
builder.Services.AddKeyedScoped<IProductScopedService, ProductScopedService>("productscoped2");
builder.Services.AddKeyedTransient<IProductTransientService, ProductTransientService>("producttransient1");
builder.Services.AddKeyedTransient<IProductTransientService, ProductTransientService>("producttransient2");
builder.Services.TryAddKeyedSingleton<IProductSingletonService, ProductSingletonService>("productsingleton1");
builder.Services.TryAddKeyedSingleton<IProductSingletonService, ProductSingletonService>("productsingleton2");
builder.Services.TryAddKeyedScoped<IProductScopedService, ProductScopedService>("productscoped1");
builder.Services.TryAddKeyedScoped<IProductScopedService, ProductScopedService>("productscoped2");
builder.Services.TryAddKeyedTransient<IProductTransientService, ProductTransientService>("producttransient1");
builder.Services.TryAddKeyedTransient<IProductTransientService, ProductTransientService>("producttransient2");
Create a new instance of the service type
You can also manually create a new instance of the keyed service by using the implementation service methods. With these, you use a delegate which has the IServiceProvider
type as a parameter. This is how you do it when defining a type as a parameter.
// Program.cs
builder.Services.AddKeyedSingleton(typeof(CategorySingletonService), "categorysingleton1", (_, key) =>
{
return new CategorySingletonService();
});
builder.Services.AddKeyedSingleton(typeof(CategorySingletonService), "categorysingleton2", (_, key) =>
{
return new CategorySingletonService();
});
builder.Services.AddKeyedScoped(typeof(CategoryScopedService), "categoryscoped1", (serviceProvider, key) =>
{
return new CategoryScopedService(serviceProvider.GetRequiredKeyedService<CategorySingletonService>("categorysingleton1"));
});
builder.Services.AddKeyedScoped(typeof(CategoryScopedService), "categoryscoped2", (serviceProvider, key) =>
{
return new CategoryScopedService(serviceProvider.GetRequiredKeyedService<CategorySingletonService>("categorysingleton2"));
});
builder.Services.AddKeyedTransient(typeof(CategoryTransientService), "categorytransient1", (_, key) =>
{
return new CategoryTransientService();
});
builder.Services.AddKeyedTransient(typeof(CategoryTransientService), "categorytransient2", (_, key) =>
{
return new CategoryTransientService();
});
builder.Services.TryAddKeyedSingleton(typeof(CategorySingletonService), "categorysingleton1", (_, key) =>
{
return new CategorySingletonService();
});
builder.Services.TryAddKeyedSingleton(typeof(CategorySingletonService), "categorysingleton2", (_, key) =>
{
return new CategorySingletonService();
});
builder.Services.TryAddKeyedScoped(typeof(CategoryScopedService), "categoryscoped1", (serviceProvider, key) =>
{
return new CategoryScopedService(serviceProvider.GetRequiredKeyedService<CategorySingletonService>("categorysingleton1"));
});
builder.Services.TryAddKeyedScoped(typeof(CategoryScopedService), "categoryscoped2", (serviceProvider, key) =>
{
return new CategoryScopedService(serviceProvider.GetRequiredKeyedService<CategorySingletonService>("categorysingleton2"));
});
builder.Services.TryAddKeyedTransient(typeof(CategoryTransientService), "categorytransient1", (_, key) =>
{
return new CategoryTransientService();
});
builder.Services.TryAddKeyedTransient(typeof(CategoryTransientService), "categorytransient2", (_, key) =>
{
return new CategoryTransientService();
});
builder.Services.AddKeyedSingleton(typeof(IProductSingletonService), "productsingleton1", (_, key) =>
{
return new ProductSingletonService();
});
builder.Services.AddKeyedSingleton(typeof(IProductSingletonService), "productsingleton2", (_, key) =>
{
return new ProductSingletonService();
});
builder.Services.AddKeyedScoped(typeof(IProductScopedService), "productscoped1", (serviceProvider, key) =>
{
return new ProductScopedService(serviceProvider.GetRequiredKeyedService<IProductSingletonService>("productsingleton1"));
});
builder.Services.AddKeyedScoped(typeof(IProductScopedService), "productscoped2", (serviceProvider, key) =>
{
return new ProductScopedService(serviceProvider.GetRequiredKeyedService<IProductSingletonService>("productsingleton2"));
});
builder.Services.AddKeyedTransient(typeof(IProductTransientService), "producttransient1", (_, key) =>
{
return new ProductTransientService();
});
builder.Services.AddKeyedTransient(typeof(IProductTransientService), "producttransient2", (_, key) =>
{
return new ProductTransientService();
});
builder.Services.TryAddKeyedSingleton(typeof(IProductSingletonService), "productsingleton1", (_, key) =>
{
return new ProductSingletonService();
});
builder.Services.TryAddKeyedSingleton(typeof(IProductSingletonService), "productsingleton2", (_, key) =>
{
return new ProductSingletonService();
});
builder.Services.TryAddKeyedScoped(typeof(IProductScopedService), "productscoped1", (serviceProvider, key) =>
{
return new ProductScopedService(serviceProvider.GetRequiredKeyedService<IProductSingletonService>("productsingleton1"));
});
builder.Services.TryAddKeyedScoped(typeof(IProductScopedService), "productscoped2", (serviceProvider, key) =>
{
return new ProductScopedService(serviceProvider.GetRequiredKeyedService<IProductSingletonService>("productsingleton2"));
});
builder.Services.TryAddKeyedTransient(typeof(IProductTransientService), "producttransient1", (_, key) =>
{
return new ProductTransientService();
});
builder.Services.TryAddKeyedTransient(typeof(IProductTransientService), "producttransient2", (_, key) =>
{
return new ProductTransientService();
});
The alternative is to add the service type using the generic class type extension method:
// Program.cs
builder.Services.AddKeyedSingleton("categorysingleton1", (_, key) =>
{
return new CategorySingletonService();
});
builder.Services.AddKeyedSingleton("categorysingleton2", (_, key) =>
{
return new CategorySingletonService();
});
builder.Services.AddKeyedScoped("categoryscoped1", (serviceProvider, key) =>
{
return new CategoryScopedService(serviceProvider.GetRequiredKeyedService<CategorySingletonService>("categorysingleton1"));
});
builder.Services.AddKeyedScoped("categoryscoped2", (serviceProvider, key) =>
{
return new CategoryScopedService(serviceProvider.GetRequiredKeyedService<CategorySingletonService>("categorysingleton2"));
});
builder.Services.AddKeyedTransient("categorytransient1", (_, key) =>
{
return new CategoryTransientService();
});
builder.Services.AddKeyedTransient("categorytransient2", (_, key) =>
{
return new CategoryTransientService();
});
builder.Services.TryAddKeyedSingleton("categorysingleton1", (_, key) =>
{
return new CategorySingletonService();
});
builder.Services.TryAddKeyedSingleton("categorysingleton2", (_, key) =>
{
return new CategorySingletonService();
});
builder.Services.TryAddKeyedScoped("categoryscoped1", (serviceProvider, key) =>
{
return new CategoryScopedService(serviceProvider.GetRequiredKeyedService<CategorySingletonService>("categorysingleton1"));
});
builder.Services.TryAddKeyedScoped("categoryscoped2", (serviceProvider, key) =>
{
return new CategoryScopedService(serviceProvider.GetRequiredKeyedService<CategorySingletonService>("categorysingleton2"));
});
builder.Services.TryAddKeyedTransient("categorytransient1", (_, key) =>
{
return new CategoryTransientService();
});
builder.Services.TryAddKeyedTransient("categorytransient2", (_, key) =>
{
return new CategoryTransientService();
});
builder.Services.AddKeyedSingleton<IProductSingletonService, ProductSingletonService>("productsingleton1", (_, key) =>
{
return new ProductSingletonService();
});
builder.Services.AddKeyedSingleton<IProductSingletonService, ProductSingletonService>("productsingleton2", (_, key) =>
{
return new ProductSingletonService();
});
builder.Services.AddKeyedScoped<IProductScopedService, ProductScopedService>("productscoped1", (serviceProvider, key) =>
{
return new ProductScopedService(serviceProvider.GetRequiredKeyedService<IProductSingletonService>("productsingleton1"));
});
builder.Services.AddKeyedScoped<IProductScopedService, ProductScopedService>("productscoped2", (serviceProvider, key) =>
{
return new ProductScopedService(serviceProvider.GetRequiredKeyedService<IProductSingletonService>("productsingleton2"));
});
builder.Services.AddKeyedTransient<IProductTransientService, ProductTransientService>("producttransient1", (_, key) =>
{
return new ProductTransientService();
});
builder.Services.AddKeyedTransient<IProductTransientService, ProductTransientService>("producttransient2", (_, key) =>
{
return new ProductTransientService();
});
builder.Services.TryAddKeyedSingleton<IProductSingletonService>("productsingleton1", (_, key) =>
{
return new ProductSingletonService();
});
builder.Services.TryAddKeyedSingleton<IProductSingletonService>("productsingleton2", (_, key) =>
{
return new ProductSingletonService();
});
builder.Services.TryAddKeyedScoped<IProductScopedService>("productscoped1", (serviceProvider, key) =>
{
return new ProductScopedService(serviceProvider.GetRequiredKeyedService<IProductSingletonService>("productsingleton1"));
});
builder.Services.TryAddKeyedScoped<IProductScopedService>("productscoped2", (serviceProvider, key) =>
{
return new ProductScopedService(serviceProvider.GetRequiredKeyedService<IProductSingletonService>("productsingleton2"));
});
builder.Services.TryAddKeyedTransient<IProductTransientService>("producttransient1", (_, key) =>
{
return new ProductTransientService();
});
builder.Services.TryAddKeyedTransient<IProductTransientService>("producttransient2", (_, key) =>
{
return new ProductTransientService();
});
When you need to inject another service into a constructor's parameter, you use the serviceProvider
parameter and call either the GetKeyedService
or GetRequiredKeyedService
method. The difference is that the GetKeyedService
extension method will return null
if the service hasn't been registered, whereas GetKeyedRequiredService
will throw an exception.
This example shows injecting the IProductSingletonService
instance as a parameter when creating a new instance of ProductScopedService
. As it's a keyed service, we need to specify the key so it knows which registered service to resolve.
// Program.cs
builder.Services.AddKeyedScoped<IProductScopedService, ProductScopedService>("productscoped1", (serviceProvider, key) =>
{
return new ProductScopedService(serviceProvider.GetRequiredKeyedService<IProductSingletonService>("productsingleton1"));
});
Injecting keyed services
Like with injecting services, keyed services can be injected in minimal APIs, controllers, Razor views and middleware.
Minimal APIs
Injecting services in minimal API endpoints involves adding the service type as a parameter. In-addition, you need to add the [FromKeyedServices]
attribute and specify the key so it knows which registered service to resolve.
app.MapGet("/minimal/service-lifetimes", (
[FromKeyedServices("productsingleton1")] IProductSingletonService productSingleton1Service,
[FromKeyedServices("productsingleton2")] IProductSingletonService productSingleton2Service,
[FromKeyedServices("productscoped1")] IProductScopedService productScoped1Service,
[FromKeyedServices("productscoped2")] IProductScopedService productScoped2Service,
[FromKeyedServices("producttransient1")] IProductTransientService productTransient1Service,
[FromKeyedServices("producttransient2")] IProductTransientService productTransient2Service
) => new
{
Singleton1 = productSingleton1Service.UtcTime.ToString("HH:mm:ss.ffffff"),
Singleton2 = productSingleton2Service.UtcTime.ToString("HH:mm:ss.ffffff"),
Scoped1 = productScoped1Service.UtcTime.ToString("HH:mm:ss.ffffff"),
Scoped2 = productScoped2Service.UtcTime.ToString("HH:mm:ss.ffffff"),
Transient1 = productTransient1Service.UtcTime.ToString("HH:mm:ss.ffffff"),
Transient2 = productTransient2Service.UtcTime.ToString("HH:mm:ss.ffffff")
});
When running the /minimal/service-lifetimes
endpoint, it will resolve different times even if they are the same type. That's because the key ensures that it's a separate instance:
{
"singleton1": "14:15:38.385346",
"singleton2": "14:15:38.385414",
"scoped1": "14:15:38.385577",
"scoped2": "14:15:38.385663",
"transient1": "14:15:38.385774",
"transient2": "14:15:38.385843"
}
Controllers
When using a controller, you also use the FromKeyedServices
attribute when injecting the service as a parameter in the constructor to resolve the correct service registration:
// WebApiController.cs
[ApiController]
[Route("api/[controller]")]
public class WebApiController : ControllerBase
{
private readonly IProductSingletonService _productSingleton1Service;
private readonly IProductSingletonService _productSingleton2Service;
private readonly IProductScopedService _productScoped1Service;
private readonly IProductScopedService _productScoped2Service;
private readonly IProductTransientService _productTransient1Service;
private readonly IProductTransientService _productTransient2Service;
public WebApiController(
[FromKeyedServices("productsingleton1")] IProductSingletonService productSingleton1Service,
[FromKeyedServices("productsingleton2")] IProductSingletonService productSingleton2Service,
[FromKeyedServices("productscoped1")] IProductScopedService productScoped1Service,
[FromKeyedServices("productscoped2")] IProductScopedService productScoped2Service,
[FromKeyedServices("producttransient1")] IProductTransientService productTransient1Service,
[FromKeyedServices("producttransient2")] IProductTransientService productTransient2Service
)
{
_productSingleton1Service = productSingleton1Service;
_productSingleton2Service = productSingleton2Service;
_productScoped1Service = productScoped1Service;
_productScoped2Service = productScoped2Service;
_productTransient1Service = productTransient1Service;
_productTransient2Service = productTransient2Service;
}
[HttpGet("service-lifetimes")]
public IActionResult ServiceLifetimes()
{
return Ok(new
{
Singleton1 = _productSingleton1Service.UtcTime.ToString("HH:mm:ss.ffffff"),
Singleton2 = _productSingleton2Service.UtcTime.ToString("HH:mm:ss.ffffff"),
Scoped1 = _productScoped1Service.UtcTime.ToString("HH:mm:ss.ffffff"),
Scoped2 = _productScoped2Service.UtcTime.ToString("HH:mm:ss.ffffff"),
Transient1 = _productTransient1Service.UtcTime.ToString("HH:mm:ss.ffffff"),
Transient2 = _productTransient2Service.UtcTime.ToString("HH:mm:ss.ffffff")
});
}
}
It's also possible to inject a keyed service directly into a method. Again, you must remember to include the FromKeyedServices
attribute and specify the key:
// WebApiController.cs
[ApiController]
[Route("api/[controller]")]
public class WebApiController : ControllerBase
{
[HttpGet("product-singleton-1-utc")]
public DateTime ProductSingleton1UtcTime([FromKeyedServices("productsingleton1")] IProductSingletonService productSingleton1Service)
{
return productSingleton1Service.UtcTime;
}
}
Razor pages and views
At the time of publishing this article, there isn't a direct way of injecting keyed services into a Razor page or view. However, you can use the @inject
directive to inject the IServiceProvider
instance and resolve keyed services either using the GetKeyedService
or GetRequiredKeyedService
methods.
// MvcController.cs
[Route("[controller]")]
public class MvcController : Controller
{
private readonly IProductSingletonService _productSingleton1Service;
private readonly IProductSingletonService _productSingleton2Service;
private readonly IProductScopedService _productScoped1Service;
private readonly IProductScopedService _productScoped2Service;
private readonly IProductTransientService _productTransient1Service;
private readonly IProductTransientService _productTransient2Service;
public MvcController(
[FromKeyedServices("productsingleton1")] IProductSingletonService productSingleton1Service,
[FromKeyedServices("productsingleton2")] IProductSingletonService productSingleton2Service,
[FromKeyedServices("productscoped1")] IProductScopedService productScoped1Service,
[FromKeyedServices("productscoped2")] IProductScopedService productScoped2Service,
[FromKeyedServices("producttransient1")] IProductTransientService productTransient1Service,
[FromKeyedServices("producttransient2")] IProductTransientService productTransient2Service
)
{
_productSingleton1Service = productSingleton1Service;
_productSingleton2Service = productSingleton2Service;
_productScoped1Service = productScoped1Service;
_productScoped2Service = productScoped2Service;
_productTransient1Service = productTransient1Service;
_productTransient2Service = productTransient2Service;
}
[HttpGet("service-lifetimes")]
public IActionResult ServiceLifetimes()
{
return View(new MvcServiceLifetimeModel(
_productSingleton1Service.UtcTime,
_productSingleton2Service.UtcTime,
_productScoped1Service.UtcTime,
_productScoped2Service.UtcTime,
_productTransient1Service.UtcTime,
_productTransient2Service.UtcTime
));
}
}
<!-- ServiceLifetimes.cshtml -->
@using RoundTheCode.DI.Models
@using RoundTheCode.DI.Services.Product
@inject IServiceProvider serviceProvider
@model MvcServiceLifetimeModel
@{
var viewSingleton1 = serviceProvider.GetRequiredKeyedService<IProductSingletonService>("productsingleton1");
var viewSingleton2 = serviceProvider.GetRequiredKeyedService<IProductSingletonService>("productsingleton1");
var viewScoped1 = serviceProvider.GetRequiredKeyedService<IProductScopedService>("productscoped1");
var viewScoped2 = serviceProvider.GetRequiredKeyedService<IProductScopedService>("productscoped2");
var viewTransient1 = serviceProvider.GetRequiredKeyedService<IProductTransientService>("producttransient1");
var viewTransient2 = serviceProvider.GetRequiredKeyedService<IProductTransientService>("producttransient2");
}
<h2>Singleton 1</h2>
<p>Controller: @Model.ControllerSingleton1Date.ToString("HH:mm:ss.ffffff")</p>
<p>View: @viewSingleton1.UtcTime.ToString("HH:mm:ss.ffffff")</p>
<h2>Singleton 2</h2>
<p>Controller: @Model.ControllerSingleton2Date.ToString("HH:mm:ss.ffffff")</p>
<p>View: @viewSingleton2.UtcTime.ToString("HH:mm:ss.ffffff")</p>
<h2>Scoped 1</h2>
<p>Controller: @Model.ControllerScoped1Date.ToString("HH:mm:ss.ffffff")</p>
<p>View: @viewScoped1.UtcTime.ToString("HH:mm:ss.ffffff")</p>
<h2>Scoped 2</h2>
<p>Controller: @Model.ControllerScoped2Date.ToString("HH:mm:ss.ffffff")</p>
<p>View: @viewScoped2.UtcTime.ToString("HH:mm:ss.ffffff")</p>
<h2>Transient 1</h2>
<p>Controller: @Model.ControllerTransient1Date.ToString("HH:mm:ss.ffffff")</p>
<p>View: @viewTransient1.UtcTime.ToString("HH:mm:ss.ffffff")</p>
<h2>Transient 2</h2>
<p>Controller: @Model.ControllerTransient2Date.ToString("HH:mm:ss.ffffff")</p>
<p>View: @viewTransient2.UtcTime.ToString("HH:mm:ss.ffffff")</p>
Middleware
Like with minimal and web APIs, you resolve keyed services by using the [FromKeyedServices]
attribute.
As middleware classes are resolved at application startup and last for the duration of the application, you can only add singleton service lifetime instances in the constructor. For scoped and transient service lifetime instances, these need to be added as parameters inside the InvokeAsync
method with the [FromKeyedServices]
attribute.
// ProductMiddleware.cs
public class ProductMiddleware
{
private readonly RequestDelegate _next;
private readonly IProductSingletonService _productSingletonService1;
private readonly IProductSingletonService _productSingletonService2;
public ProductMiddleware(
RequestDelegate next,
[FromKeyedServices("productsingleton1")] IProductSingletonService productSingletonService1,
[FromKeyedServices("productsingleton2")] IProductSingletonService productSingletonService2)
{
_next = next;
_productSingletonService1 = productSingletonService1;
_productSingletonService2 = productSingletonService2;
}
public async Task InvokeAsync(
HttpContext httpContext,
[FromKeyedServices("productscoped1")] IProductScopedService productScopedService1,
[FromKeyedServices("productscoped2")] IProductScopedService productScopedService2,
[FromKeyedServices("producttransient1")] IProductTransientService productTransientService1,
[FromKeyedServices("producttransient2")] IProductTransientService productTransientService2
)
{
httpContext.Items.Add("ProductSingleton1", _productSingletonService1.UtcTime);
httpContext.Items.Add("ProductSingleton2", _productSingletonService2.UtcTime);
httpContext.Items.Add("ProductScoped1", productScopedService1.UtcTime);
httpContext.Items.Add("ProductScoped2", productScopedService2.UtcTime);
httpContext.Items.Add("ProductTransient1", productTransientService1.UtcTime);
httpContext.Items.Add("ProductTransient2", productTransientService2.UtcTime);
await _next(httpContext);
}
}
Primary constructors
It's also possible to inject keyed services using primary constructors. Primary constructors was launched in C# 12 and allows you to declare a service directly into the class declaration.
We've created a class called CategoryPrimaryStorageService
which injects the ICategoryStorageService
as part of the class declaration. We've added the [FromKeyedServices]
attribute and added the homeandkitchen
key to get the instance.
We can then use that instance within that class.
public class CategoryPrimaryStorageService(
[FromKeyedServices("homeandkitchen")] ICategoryStorageService categoryHomeAndKitchenService
) : ICategoryStorageService
{
public List<CategoryTypeDto> Types => categoryHomeAndKitchenService.Types;
}
Different implementations of the same service
Before keyed services was introduced in .NET 8, you could still add multiple implementations of the same service:
// ICategoryStorageService.cs
public interface ICategoryStorageService
{
List<CategoryTypeDto> Types { get; }
}
public class CategoryHomeAndKitchenStorageService : ICategoryStorageService
{
public List<CategoryTypeDto> Types { get; } = new()
{
new(3, "Home and Kitchen")
};
}
public class CategoryComputersStorageService : ICategoryStorageService
{
public List<CategoryTypeDto> Types { get; } = new()
{
new(2, "Computers"),
new(4, "Software")
};
}
builder.Services.AddSingleton<ICategoryStorageService, CategoryComputersStorageService>();
builder.Services.AddSingleton<ICategoryStorageService, CategoryHomeAndKitchenStorageService>();
However it did make it more difficult to resolve when injecting the service. If you inject the ICategoryStorageService
type directly, it would resolve the last service that was registered. In our instance, it would have been the CategoryHomeAndKitchenStorageService
.
An alternative was to inject the ICategoryStorageService
type as an IEnumerable
. That would inject both instances of the ICategoryStorageService
type with the order being the same as how we registered them. So the first index would be the CategoryComputersStorageService
implementation and the second would be the CategoryHomeAndKitchenStorageService
implementation.
// WebApiStorageController.cs
[Route("api/[controller]")]
[ApiController]
public class WebApiStorageController : ControllerBase
{
private readonly ICategoryStorageService _lastCategoryStorageService;
private readonly IEnumerable<ICategoryStorageService> _categoryStorageServices;
public WebApiStorageController(
ICategoryStorageService lastCategoryStorageService,
IEnumerable<ICategoryStorageService> categoryStorageServices,
)
{
_lastCategoryStorageService = lastCategoryStorageService;
_categoryStorageServices = categoryStorageServices;
}
[HttpGet("last-category-type")]
public IActionResult LastCategoryType()
{
return Ok(new
{
_lastCategoryStorageService.Types
});
}
[HttpGet("category-types")]
public IActionResult CategoryTypes()
{
return Ok(new
{
Types = _categoryStorageServices.Select(s => s.Types).ToList()
});
}
}
The LastCategoryType
endpoint which uses the ICategoryStorageService
to resolve the service would output:
{
"types": [
{
"id": 3,
"name": "Home and Kitchen"
}
]
}
Whereas, the CategoryTypes
endpoint which uses the IEnumerable<ICategoryStorageService>
to resolve the services would output:
{
"types": [
[
{
"id": 2,
"name": "Computers"
},
{
"id": 4,
"name": "Software"
}
],
[
{
"id": 3,
"name": "Home and Kitchen"
}
]
]
}
Keyed service is the alternative
The alternative for this is to register both of these services as keyed services. That way, we can resolve the service with the key which makes it a lot more readable.
For the CategoryComputersStorageService
implementation, we'll use the computers
key, where as for the CategoryHomeAndKitchenStorageService
implementation, we'll use the homeandkitchen
key.
// Program.cs
builder.Services.AddKeyedSingleton<ICategoryStorageService, CategoryComputersStorageService >("computers");
builder.Services.AddKeyedSingleton<ICategoryStorageService, CategoryHomeAndKitchenStorageService>("homeandkitchen");
It's then a case of creating two separate instances of the ICategoryStorageService
type in the controller and resolving them in the constructor by using the [FromKeyedServices]
attribute.
// WebApiStorageController.cs
[Route("api/[controller]")]
[ApiController]
public class WebApiStorageController : ControllerBase
{
...
private readonly ICategoryStorageService _categoryComputersStorageService;
private readonly ICategoryStorageService _categoryHomeAndKitchenStorageService;
public WebApiStorageController(
...
[FromKeyedServices("computers")] ICategoryStorageService categoryComputersStorageService,
[FromKeyedServices("homeandkitchen")] ICategoryStorageService categoryHomeAndKitchenStorageService
)
{
...
_categoryComputersStorageService = categoryComputersStorageService;
_categoryHomeAndKitchenStorageService = categoryHomeAndKitchenStorageService;
}
...
[HttpGet("category-keyed-types")]
public IActionResult CategoryKeyedTypes()
{
return Ok(new
{
ComputerTypes = _categoryComputersStorageService.Types,
HomeAndKitchenTypes = _categoryHomeAndKitchenStorageService.Types
});
}
}
Watch the video
Watch the video where we show you the different extension methods to add a keyed service and how to inject them in ASP.NET Core.
And if you want to try out keyed servies for yourself, you can download the code example which gives you examples on how to register a keyed service and how to inject them.
Latest tutorials

