Skip to content

Keyed services

Register several implementations of one interface under distinct keys and resolve them with GetRequiredKeyedService.

2 min read

Set ServiceKey on any registration attribute and the generated code emits a keyed ServiceDescriptor instead of a plain one. The service is then resolvable only through the keyed APIs.

A keyed registration

NotificationSenders.cs
using Immediate.Injections.Shared;

public interface INotificationSender
{
	Task SendAsync(string message, CancellationToken token);
}

[RegisterScoped<INotificationSender>(ServiceKey = "email")]
public sealed class EmailNotificationSender : INotificationSender
{
	public Task SendAsync(string message, CancellationToken token) => Task.CompletedTask;
}

[RegisterScoped<INotificationSender>(ServiceKey = "sms")]
public sealed class SmsNotificationSender : INotificationSender
{
	public Task SendAsync(string message, CancellationToken token) => Task.CompletedTask;
}
Generated output
ServiceDescriptor.KeyedScoped(typeof(INotificationSender), "email", typeof(EmailNotificationSender));
ServiceDescriptor.KeyedScoped(typeof(INotificationSender), "sms", typeof(SmsNotificationSender));

Resolving

NotificationService.cs
using Microsoft.Extensions.DependencyInjection;

public sealed class NotificationService(IServiceProvider provider)
{
	public Task NotifyAsync(string channel, string message, CancellationToken token)
	{
		var sender = provider.GetRequiredKeyedService<INotificationSender>(channel);
		return sender.SendAsync(message, token);
	}
}

Constructor injection works too, via [FromKeyedServices]:

EmailOnlyService.cs
using Microsoft.Extensions.DependencyInjection;

public sealed class EmailOnlyService(
	[FromKeyedServices("email")] INotificationSender sender
);

If a service should be reachable both ways, apply the attribute twice — the lifetime attributes are AllowMultiple = true:

BothWays.cs
[RegisterScoped<INotificationSender>]
[RegisterScoped<INotificationSender>(ServiceKey = "email")]
public sealed class EmailNotificationSender : INotificationSender;

That produces two independent descriptors, so under a non-singleton lifetime you get two instances. To share one, register it once for real and let the other resolve through it — see Factories and proxies.

What can be a key

ServiceKey is typed object?, so any expression that C# accepts as an attribute argument works — the value is emitted verbatim into the generated ServiceDescriptor call.

KeyKinds.cs
using Immediate.Injections.Shared;

public enum Channel { Email, Sms }

[RegisterScoped<INotificationSender>(ServiceKey = "email")]           // string literal
public sealed class A : INotificationSender;

[RegisterScoped<INotificationSender>(ServiceKey = B.Key)]             // const field
public sealed class B : INotificationSender
{
	public const string Key = "email-const";
}

[RegisterScoped<INotificationSender>(ServiceKey = Channel.Sms)]       // enum member
public sealed class C : INotificationSender;

[RegisterScoped<INotificationSender>(ServiceKey = 1)]                 // integer
public sealed class D : INotificationSender;

Whatever you use, resolve with the same value — MSDI compares keys with Equals, so Channel.Email and "Email" are different keys.

Two limits follow from this being an attribute argument:

  • The value must be a compile-time constant. KeyedService.AnyKey is a property, so it cannot be written here; use it at the resolution site instead.
  • Writing ServiceKey = null explicitly is treated as no key — the generator emits a plain, unkeyed registration.

Keyed factories

If the registration also names a Factory, the factory signature changes: a keyed factory takes a second object parameter.

KeyedFactoryService.cs
using Immediate.Injections.Shared;
using Microsoft.Extensions.DependencyInjection;

public interface IKeyedFactoryService
{
	object? ServiceKey { get; }
}

[RegisterSingleton<IKeyedFactoryService>(ServiceKey = Key, Factory = nameof(Create))]
public sealed class KeyedFactoryService(object? serviceKey) : IKeyedFactoryService
{
	public const string Key = "factory-key";

	public object? ServiceKey { get; } = serviceKey;

	public static KeyedFactoryService Create(IServiceProvider provider, object? serviceKey) =>
		new(serviceKey);
}

A factory whose signature does not match the keyed/unkeyed shape is INJ0010.

Where to go next