Antes do .NET 8, registrar múltiplas implementações da mesma interface e escolher a correta em tempo de execução exigia workarounds: factory functions, dicionários de Type, ou dependências de containers externos como Autofac com Named. O resultado era código acoplado ao container ou patterns de service locator que dificultavam testes.
O .NET 8 resolveu isso com Keyed Services: suporte nativo no Microsoft.Extensions.DependencyInjection para registrar e resolver implementações por chave. Limpo, testável, sem dependência de terceiros.
O problema: múltiplas implementações da mesma interface
Imagine um sistema de notificações com três canais — e-mail, SMS e push:
public interface INotificationSender
{
Task SendAsync(string destination, string message);
}
public class EmailSender : INotificationSender { ... }
public class SmsSender : INotificationSender { ... }
public class PushSender : INotificationSender { ... }
Antes do .NET 8, registrar os três e resolver o correto exigia:
// Abordagem antiga — factory acoplada ao container
services.AddScoped<EmailSender>();
services.AddScoped<SmsSender>();
services.AddScoped<PushSender>();
services.AddScoped<Func<string, INotificationSender>>(sp => key => key switch
{
"email" => sp.GetRequiredService<EmailSender>(),
"sms" => sp.GetRequiredService<SmsSender>(),
"push" => sp.GetRequiredService<PushSender>(),
_ => throw new KeyNotFoundException(key)
});
Funciona, mas é frágil: a chave é uma string solta, o teste precisa mockar a Func, e adicionar um novo canal exige mexer no registro central.
A solução: Keyed Services no .NET 8
Registro
Use AddKeyedSingleton, AddKeyedScoped ou AddKeyedTransient com uma chave — qualquer object, tipicamente string ou enum:
// Program.cs
builder.Services.AddKeyedScoped<INotificationSender, EmailSender>("email");
builder.Services.AddKeyedScoped<INotificationSender, SmsSender>("sms");
builder.Services.AddKeyedScoped<INotificationSender, PushSender>("push");
Resolução via atributo [FromKeyedServices]
Em construtores e métodos de ação, use [FromKeyedServices]:
public class NotificationService
{
private readonly INotificationSender _emailSender;
private readonly INotificationSender _smsSender;
public NotificationService(
[FromKeyedServices("email")] INotificationSender emailSender,
[FromKeyedServices("sms")] INotificationSender smsSender)
{
_emailSender = emailSender;
_smsSender = smsSender;
}
}
Em Minimal APIs, funciona direto no parâmetro:
app.MapPost("/notify/{channel}", async (
string channel,
NotificationRequest request,
[FromKeyedServices("email")] INotificationSender email,
[FromKeyedServices("sms")] INotificationSender sms) =>
{
var sender = channel == "email" ? email : sms;
await sender.SendAsync(request.Destination, request.Message);
return Results.NoContent();
});
Resolução via IServiceProvider
Quando a chave é dinâmica (vem de configuração ou banco), use GetRequiredKeyedService:
public class NotificationDispatcher
{
private readonly IServiceProvider _sp;
public NotificationDispatcher(IServiceProvider sp) => _sp = sp;
public async Task DispatchAsync(string channel, string dest, string msg)
{
var sender = _sp.GetRequiredKeyedService<INotificationSender>(channel);
await sender.SendAsync(dest, msg);
}
}
Diferente do service locator clássico, aqui o IServiceProvider está encapsulado dentro do dispatcher — a injeção de dependência dos consumidores continua limpa.
Usando enum como chave (recomendado)
Strings soltas são propensas a typos. Prefira enum:
public enum NotificationChannel { Email, Sms, Push }
builder.Services.AddKeyedScoped<INotificationSender, EmailSender>(NotificationChannel.Email);
builder.Services.AddKeyedScoped<INotificationSender, SmsSender>(NotificationChannel.Sms);
builder.Services.AddKeyedScoped<INotificationSender, PushSender>(NotificationChannel.Push);
public class NotificationService(
[FromKeyedServices(NotificationChannel.Email)] INotificationSender emailSender,
[FromKeyedServices(NotificationChannel.Push)] INotificationSender pushSender)
{
// ...
}
O compilador verifica o valor — sem runtime surprises.
Caso de uso real: Strategy Pattern com configuração por tenant
Keyed Services brilham no Strategy Pattern quando a estratégia depende do contexto de execução:
public interface IStorageProvider
{
Task<Stream> DownloadAsync(string path);
Task UploadAsync(string path, Stream content);
}
public class S3StorageProvider : IStorageProvider { ... }
public class AzureBlobProvider : IStorageProvider { ... }
public class LocalDiskProvider : IStorageProvider { ... }
// Registro
builder.Services.AddKeyedSingleton<IStorageProvider, S3StorageProvider>("s3");
builder.Services.AddKeyedSingleton<IStorageProvider, AzureBlobProvider>("azure");
builder.Services.AddKeyedSingleton<IStorageProvider, LocalDiskProvider>("local");
public class StorageRouter
{
private readonly IServiceProvider _sp;
private readonly ITenantContext _tenant;
public StorageRouter(IServiceProvider sp, ITenantContext
<p>####PUB1####</p>
tenant)
{
_sp = sp;
_tenant = tenant;
}
public IStorageProvider GetProvider()
{
// Cada tenant pode ter seu provider configurado
var key = _tenant.StorageProvider; // "s3", "azure" ou "local"
return _sp.GetRequiredKeyedService<IStorageProvider>(key);
}
}
Adicionar um novo provider de storage é registrar uma linha e criar a implementação — sem tocar em switch/factory.
Caso de uso: múltiplos handlers de evento
Em sistemas orientados a eventos, você pode ter vários handlers para o mesmo tipo de evento registrados por prioridade ou contexto:
public interface IOrderEventHandler
{
Task HandleAsync(OrderCreatedEvent evt);
}
public class InventoryHandler : IOrderEventHandler { ... }
public class BillingHandler : IOrderEventHandler { ... }
public class AnalyticsHandler : IOrderEventHandler { ... }
builder.Services.AddKeyedScoped<IOrderEventHandler, InventoryHandler>("inventory");
builder.Services.AddKeyedScoped<IOrderEventHandler, BillingHandler>("billing");
builder.Services.AddKeyedScoped<IOrderEventHandler, AnalyticsHandler>("analytics");
O dispatcher busca todos os handlers registrados para um evento e os executa:
public class OrderEventDispatcher
{
private readonly IServiceProvider _sp;
public OrderEventDispatcher(IServiceProvider sp) => _sp = sp;
public async Task DispatchAsync(OrderCreatedEvent evt)
{
var keys = new[] { "inventory", "billing", "analytics" };
var tasks = keys
.Select(k => _sp.GetRequiredKeyedService<IOrderEventHandler>(k).HandleAsync(evt));
await Task.WhenAll(tasks);
}
}
Keyed Services vs. IEnumerable<T>
Uma dúvida comum: por que não registrar todos e injetar IEnumerable<INotificationSender>?
// Abordagem alternativa
services.AddScoped<INotificationSender, EmailSender>();
services.AddScoped<INotificationSender, SmsSender>();
public class NotificationService(IEnumerable<INotificationSender> senders) { ... }
A diferença:
| Critério | IEnumerable<T> | Keyed Services |
|---|---|---|
| Seleção dinâmica por chave | ❌ Precisa de lógica extra | ✅ Nativo |
| Resolve apenas o necessário | ❌ Resolve todos | ✅ Resolve só o pedido |
| Legibilidade na injeção | Implícita | Explícita ([FromKeyedServices]) |
| Múltiplos do mesmo tipo | Sem distinção | Distingue por chave |
Use IEnumerable<T> quando quiser aplicar todos os handlers em cadeia (decorators, pipelines). Use Keyed Services quando quiser selecionar um específico.
Testabilidade: sem mudanças na abordagem
Keyed Services não quebram os testes. Como a chave vai para o atributo do construtor, o mock é registrado da mesma forma:
[Fact]
public async Task Should_Send_Via_Email_Channel()
{
// Arrange
var emailMock = new Mock<INotificationSender>();
emailMock.Setup(x => x.SendAsync(It.IsAny<string>(), It.IsAny<string>()))
.Returns(Task.CompletedTask);
var services = new ServiceCollection();
services.AddKeyedScoped<INotificationSender>(_ => emailMock.Object, "email");
services.AddScoped<NotificationDispatcher>();
var sp = services.BuildServiceProvider();
var dispatcher = sp.GetRequiredService<NotificationDispatcher>();
// Act
await dispatcher.DispatchAsync("email", "user@test.com", "Olá");
// Assert
emailMock.Verify(x => x.SendAsync("user@test.com", "Olá"), Times.Once);
}
Para WebApplicationFactory em testes de integração:
var factory = new WebApplicationFactory<Program>().WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
// Substitui a implementação real pela fake
services.AddKeyedScoped<INotificationSender>(_ => emailMock.Object, "email");
});
});
Comparação com Autofac Named Registrations
Se você veio do Autofac, a migração é direta:
// Autofac (antes)
builder.RegisterType<EmailSender>()
.Named<INotificationSender>("email")
.InstancePerLifetimeScope();
// Microsoft DI (depois, .NET 8+)
services.AddKeyedScoped<INotificationSender, EmailSender>("email");
// Autofac resolve
var sender = context.ResolveNamed<INotificationSender>("email");
// Microsoft DI resolve
var sender = sp.GetRequiredKeyedService<INotificationSender>("email");
A principal diferença: o Autofac suporta metadata e filtros mais avançados. Para a maioria dos casos, o suporte nativo do .NET 8 é suficiente e elimina a dependência do pacote.
Resumo
Keyed Services resolvem elegantemente o problema de múltiplas implementações que existia desde o início do Microsoft.Extensions.DependencyInjection:
AddKeyedScoped/Singleton/Transientregistra implementações com uma chave.[FromKeyedServices("chave")]injeta diretamente no construtor — zero acoplamento ao container.GetRequiredKeyedService<T>("chave")resolve dinamicamente quando a chave vem de dados de execução.enumcomo chave elimina magic strings e adiciona verificação em tempo de compilação.- Testes permanecem limpos — a chave entra no registro, não na lógica de negócio.
Se você ainda usa factories manuais, dicionários de Type ou Autofac só por causa dos named registrations, vale migrar — a feature está disponível em qualquer projeto .NET 8+, sem pacotes adicionais.