[FromKeyedServices]

Inject a specific keyed registration into constructors, controller actions and minimal API handlers, and choose between attribute keys and runtime lookup.

Signature
public CheckoutService([FromKeyedServices("stripe")] IPaymentGateway gateway)
  • Added in .NET .NET 8 (Microsoft.Extensions.DependencyInjection.Abstractions 8.0)

Parameters

ParameterTypeDescription
$keyobject?The service key used at registration, for example "stripe" or an enum value. Must be a compile-time constant because it is an attribute argument.

Return value

n/aResolves the parameter with IKeyedServiceProvider.GetRequiredKeyedService(type, key) instead of the unkeyed registration.

Errors

InvalidOperationException when no service is registered for that type and key: at request time for handler parameters, at resolution time for constructor parameters (startup with ValidateOnBuild).

Changelog

  • PHP .NET 8Introduced with keyed services; honoured by the built-in container, ActivatorUtilities, controller actions, minimal API handlers and SignalR hub methods.

Keyed services let one interface have several registrations told apart by a key: AddKeyedSingleton<IPaymentGateway, StripeGateway>("stripe"). [FromKeyedServices("stripe")] on a parameter asks for that one. It works on constructor parameters of anything the container creates, on controller action parameters, on minimal API handler parameters and on hub methods.

Where the key is decided

SituationUse
The consumer always needs the same implementation (the refund job always uses "adyen")[FromKeyedServices("adyen")]
The key comes from the request or data (the order's payment provider)A scoped resolver calling GetKeyedService<IPaymentGateway>(order.Provider); null means unsupported provider (see Keyed Services)
Every implementation is needed (fan-out)Register unkeyed as well and inject IEnumerable<IPaymentGateway>

The attribute argument must be a constant, so it cannot depend on configuration. Use string constants or an enum shared between the registration and the attribute so a typo cannot compile.

For a key that comes from the request or stored data, use GetKeyedService, which returns null for an unknown key, and turn null into a 400 or 409. GetRequiredKeyedService would turn a bad value into an InvalidOperationException and a 500. Reserve it for keys the code itself guarantees, such as constants or validated configuration.

Rules that surprise people

  • Keyed and unkeyed registrations are separate. AddKeyedScoped<IClock, SystemClock>("utc") does not satisfy a plain IClock parameter, and IEnumerable<IClock> contains only the unkeyed ones.
  • Keys compare with Equals: "Stripe" and "stripe" are different keys.
  • An implementation can receive its own key through a [ServiceKey] constructor parameter, handy when one class is registered under several keys.
  • KeyedService.AnyKey registers a fallback that answers any key not registered explicitly.

Production failure modes

A missing key throws InvalidOperationException ("No service for type ... has been registered") on first use, typically in the one rarely used code path. Turn on ValidateOnBuild (on by default in Development) so constructor dependencies are checked at startup, and cover handler parameters with an integration test per endpoint. Lifetimes follow the registration as usual: a keyed singleton that captures a scoped dependency is still a captive dependency.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Charge through the Stripe gateway in a checkout handler while refunds always go through Adyen
  • Pick the payment gateway per request from the provider stored on the order with GetKeyedService
  • Serve every warehouse region from one RegionalWarehouse class that learns its region from [ServiceKey]

Frequently asked questions

Can the key come from configuration?

Not in the attribute: attribute arguments are compile-time constants. Read the key from configuration and call GetRequiredKeyedService, or register an unkeyed service whose factory picks the keyed one.

Does a keyed registration satisfy a normal parameter of the same type?

No. Keyed and unkeyed registrations are separate, and IEnumerable<T> only returns unkeyed ones. Register both if both are needed.

What happens if the key is not registered?

Resolution throws InvalidOperationException. For constructor parameters ValidateOnBuild catches it at startup; for handler parameters only a request (or a test) does.

More examples

One class under several keys with [ServiceKey]

<code>[ServiceKey]</code> injects the key the instance was resolved with, so one implementation can serve every region without a subclass per key. Each key still gets its own singleton instance.

Example · csharp
using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();
services.AddKeyedSingleton<IWarehouse, RegionalWarehouse>("eu-west");
services.AddKeyedSingleton<IWarehouse, RegionalWarehouse>("us-east");

using var provider = services.BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true });
Console.WriteLine(provider.GetRequiredKeyedService<IWarehouse>("us-east").Describe());

public interface IWarehouse { string Describe(); }

public class RegionalWarehouse([ServiceKey] string region) : IWarehouse
{
    public string Describe() => $"warehouse in {region}";
}

A fallback for unknown keys

An explicit key always wins over <code>KeyedService.AnyKey</code>. The factory receives the requested key, so the fallback can still behave per key. Use it deliberately: a fallback also hides typos that would otherwise fail loudly. Never resolve an <code>AnyKey</code> registration with an unvalidated key from a request: each distinct key creates and caches its own singleton for the life of the process, so callers could grow memory without bound.

Example · csharp
using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();
services.AddKeyedSingleton<ITaxCalculator, GermanVat>("DE");
// Any country without its own registration gets the flat-rate calculator.
services.AddKeyedSingleton<ITaxCalculator>(KeyedService.AnyKey,
    (_, key) => new FlatRateTax((string)key!, 0.20m));

using var provider = services.BuildServiceProvider();
foreach (var country in new[] { "DE", "FR" })
{
    Console.WriteLine(provider.GetRequiredKeyedService<ITaxCalculator>(country).Describe(100m));
}

public interface ITaxCalculator { string Describe(decimal net); }

public class GermanVat : ITaxCalculator
{
    public string Describe(decimal net) => $"DE: {net * 1.19m:0.00}";
}

public class FlatRateTax(string country, decimal rate) : ITaxCalculator
{
    public string Describe(decimal net) => $"{country} (flat): {net * (1 + rate):0.00}";
}

Keys from an enum, not strings

Enum values are constants, so they are valid attribute arguments, and a misspelt key no longer compiles. Remember that keys compare with <code>Equals</code>: the enum value <code>Channel.Sms</code> and the string <code>"Sms"</code> are different keys.

Example · csharp
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddKeyedScoped<INotifier, EmailNotifier>(Channel.Email);
builder.Services.AddKeyedScoped<INotifier, SmsNotifier>(Channel.Sms);

var app = builder.Build();
app.MapPost("/orders/{id:int}/shipped", (int id, [FromKeyedServices(Channel.Sms)] INotifier notifier) =>
    TypedResults.Ok(notifier.Send($"Order {id} has shipped.")));
app.Run();

public enum Channel { Email, Sms }

public interface INotifier { string Send(string message); }
public class EmailNotifier : INotifier { public string Send(string message) => $"email: {message}"; }
public class SmsNotifier : INotifier { public string Send(string message) => $"sms: {message}"; }

Discussion

  • Be the first to comment on this lesson.