Keyed services
Register several implementations of one interface under distinct keys and resolve them with GetRequiredKeyedService.
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
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;
}ServiceDescriptor.KeyedScoped(typeof(INotificationSender), "email", typeof(EmailNotificationSender));
ServiceDescriptor.KeyedScoped(typeof(INotificationSender), "sms", typeof(SmsNotificationSender));Resolving
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]:
using Microsoft.Extensions.DependencyInjection;
public sealed class EmailOnlyService(
[FromKeyedServices("email")] INotificationSender sender
);GetService<INotificationSender>() returns null for a keyed
registration, and GetKeyedService<INotificationSender>("other") returns null for a key that was never registered.If a service should be reachable both ways, apply the attribute twice — the lifetime attributes
are AllowMultiple = true:
[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.
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.AnyKeyis a property, so it cannot be written here; use it at the resolution site instead. - Writing
ServiceKey = nullexplicitly 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.
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);
}KeyedImplementationFactory argument. It carries the
key the caller asked for at resolution time, which is not necessarily the ServiceKey written on the attribute — most visibly when the service was registered
against KeyedService.AnyKey elsewhere. Treat it as input, not as a constant.A factory whose signature does not match the keyed/unkeyed shape is INJ0010.