.NET C# ASP.NET Core Injeção de Dependência .NET 8 Backend

Keyed Services no .NET 8: injeção de dependência com chave para múltiplas implementações

Aprenda a usar Keyed Services no .NET 8 para injetar múltiplas implementações da mesma interface por chave — sem service locator, sem factory manual.

N
Neryx Digital Architects
6 de dezembro de 2025
10 min de leitura
160 profissionais leram
Categoria: Arquitetura Público: Times de engenharia e produto Etapa: Aprendizado

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érioIEnumerable<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çãoImplícitaExplícita ([FromKeyedServices])
Múltiplos do mesmo tipoSem distinçãoDistingue 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/Transient registra 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.
  • enum como 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.

Precisa desenhar a próxima fase com menos retrabalho?

Fazemos discovery técnico para mapear riscos, arquitetura-alvo e sequência de execução antes de investir pesado.

Solicitar Discovery

Newsletter

Receba artigos como este no seu e-mail

Conteúdo técnico sobre arquitetura de software, .NET, IA e gestão de produto. Sem spam.