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.