Desenvolvedores .NET frequentemente enfrentam um dilema arquitetural comum: como gerenciar múltiplas implementações da mesma interface de forma limpa e eficiente. Tradicionalmente, a solução envolvia estruturas condicionais complexas – longas cadeias de if-else ou switch statements que dificultavam a manutenção e testabilidade do código. Com o lançamento do .NET 8, uma nova funcionalidade nativa de Injeção de Dependência promete revolucionar essa abordagem: os Keyed Services.
O problema dos switch statements em arquiteturas modernas
Antes do .NET 8, registrar múltiplas implementações de uma interface era possível através do método AddScoped, AddTransient ou AddSingleton. No entanto, resolver uma implementação específica em tempo de execução exigia abordagens criativas e muitas vezes desajeitadas. Desenvolvedores frequentemente injetavam IEnumerable<T> de todas as implementações registradas e então aplicavam lógica condicional para selecionar a correta. Essa abordagem não apenas violava o princípio Open/Closed do SOLID, mas também tornava o código frágil – cada nova implementação exigia modificações na lógica de seleção, aumentando o risco de erros e reduzindo a coesão do sistema.
Cenários onde switch statements se tornam problemáticos
Arquiteturas orientadas a eventos representam um caso clássico onde a seleção dinâmica de handlers se torna complexa. Imagine um sistema que processa diferentes tipos de eventos: ordens de compra, pagamentos processados e faturas geradas. Cada tipo de evento requer um handler específico, mas todos compartilham a mesma interface IEventHandler. Sem Keyed Services, uma fábrica de handlers precisaria conter lógica condicional explícita para mapear cada nome de evento ao handler correto. Essa solução escala pobremente – para cada novo tipo de evento, um novo branch condicional deve ser adicionado, violando o princípio de responsabilidade única e tornando a classe da fábrica cada vez mais complexa.
Keyed Services: a solução nativa do .NET 8
Keyed Services introduzem um conceito simples porém poderoso: registrar serviços usando uma chave além do tipo de serviço. Em vez de registrar apenas IEventHandler → ConcreteHandler, você registra IEventHandler → ConcreteHandler com uma chave específica como “PurchaseOrder” ou “PaymentProcessed”. O container de DI mantém um mapeamento entre chaves e implementações, permitindo resolução direta sem lógica condicional. Essa abordagem transforma a relação entre consumidor e provedor de serviços – o consumidor simplesmente solicita uma implementação usando uma chave, sem precisar conhecer ou gerenciar todas as implementações disponíveis.
Registro simplificado com métodos AddKeyed
A API de registro é intuitiva e mantém consistência com os métodos de registro tradicionais. Para registrar um handler específico para eventos de ordem de compra, você usaria: services.AddKeyedScoped<IEventHandler, PurchaseOrderEventHandler>(“PurchaseOrder”). Similarmente, para eventos de pagamento: services.AddKeyedScoped<IEventHandler, PaymentEventHandler>(“PaymentProcessed”). Cada registro associa uma chave string única a uma implementação concreta. O sistema suporta os mesmos ciclos de vida tradicionais – Scoped, Transient e Singleton – garantindo que os serviços gerenciados com chaves se comportem consistentemente com as expectativas dos desenvolvedores .NET.
Implementação prática: da fábrica complexa à elegante
Examinando a evolução de uma implementação tradicional para uma baseada em Keyed Services revela ganhos significativos em simplicidade. Na abordagem tradicional, uma EventHandlerFactory precisaria injetar IServiceProvider e conter lógica condicional explícita. Para três tipos de eventos, isso resultaria em uma estrutura if-else ou switch com três branches, cada um contendo código quase idêntico exceto pela chave usada para resolução. Essa duplicação de código não apenas aumenta a complexidade ciclomática, mas também introduz pontos potenciais de erro – especialmente quando novos desenvolvedores modificam o código sem atenção aos padrões repetitivos.
Transformação radical na fábrica de handlers
A versão inicial com Keyed Services já mostra melhorias, mas ainda mantém vestígios da abordagem condicional. A fábrica verifica o eventName contra constantes pré-definidas e, com base no match, resolve o serviço apropriado. No entanto, o insight crucial vem ao perceber que o eventName recebido como parâmetro é exatamente a chave usada no registro do serviço. Essa simetria permite uma refatoração radical: em vez de verificar condições, a fábrica simplesmente passa o eventName diretamente para o método GetRequiredKeyedService. O resultado é uma redução de dezenas de linhas de código para apenas algumas, eliminando completamente a lógica condicional enquanto mantém a mesma funcionalidade.
Código antes e depois da otimização
Na implementação inicial com Keyed Services, o método HandleEvent continha três blocos if que, embora mais limpos que um switch, ainda representavam lógica condicional explícita. Cada bloco resolvia o serviço usando uma chave hard-coded correspondente ao eventName verificado. A versão otimizada remove todos os ifs, substituindo-os por uma única linha: IEventHandler handler = serviceProvider.GetRequiredKeyedService<IEventHandler>(eventName). Essa linha funciona porque eventName corresponde exatamente às chaves usadas no registro (“PurchaseOrder”, “PaymentProcessed”, etc.). Se eventName não corresponder a nenhuma chave registrada, GetRequiredKeyedService lançará uma exceção apropriada, mantendo o comportamento defensivo.
Casos de uso além de handlers de eventos
Embora handlers de eventos representem um caso de uso natural, Keyed Services aplicam-se a diversos outros cenários arquiteturais. Sistemas de notificação que suportam múltiplos canais (email, SMS, push) beneficiam-se dessa abordagem – cada canal implementa uma interface INotificationChannel, registrada com chaves como “Email” ou “SMS”. Processadores de pagamento com múltiplos gateways (Stripe, PayPal, MercadoPago) seguem o mesmo padrão. Estratégias de negócio que variam por região, tipo de cliente ou plano de assinatura também se encaixam perfeitamente – cada estratégia implementa uma interface comum, com a chave determinando qual estratégia aplicar baseada em contexto de execução.
Integração com IKeyedServiceProvider
Para maximizar os benefícios dos Keyed Services, o .NET 8 introduz a interface IKeyedServiceProvider. Em vez de injetar IServiceProvider genérico, classes que consomem serviços chaveados podem injetar IKeyedServiceProvider diretamente. Esta interface expõe métodos específicos para resolução de serviços chaveados, melhorando a intenção do código e permitindo melhor análise estática. A transição é simples: substitua a injeção de IServiceProvider por IKeyedServiceProvider nos construtores relevantes. Essa mudança sinaliza claramente que a classe depende de serviços resolvidos por chave, melhorando a documentação através do código e facilitando a manutenção por outros desenvolvedores.
Vantagens arquiteturais e de manutenção
A adoção de Keyed Services traz benefícios tangíveis além da simples redução de linhas de código. Arquitetonicamente, essa abordagem promove maior aderência ao Princípio de Responsabilidade Única – a fábrica não precisa mais conhecer o mapeamento entre eventos e handlers, apenas delega essa resolução ao container DI. O Princípio Open/Closed também é melhor atendido – novos tipos de eventos exigem apenas o registro de um novo handler com uma nova chave, sem modificações na fábrica. Testabilidade melhora significativamente, pois a lógica condicional complexa é substituída por comportamento determinístico baseado em chaves, facilitando a criação de mocks e testes unitários.
Impacto na escalabilidade e evolução do sistema
Sistemas que crescem organicamente frequentemente acumulam complexidade acidental na forma de lógica de seleção espalhada por múltiplas camadas. Keyed Services oferecem um padrão consistente para gerenciar variações comportamentais, centralizando a lógica de resolução no container DI enquanto mantêm o consumo simples e direto. Quando um novo comportamento precisa ser adicionado, desenvolvedores seguem um padrão estabelecido: criar uma nova implementação, registrá-la com uma chave apropriada, e consumi-la onde necessário. Essa consistência reduz a curva de aprendizado para novos membros da equipe e diminui a probabilidade de implementações inconsistentes ou duplicadas.
Considerações de performance e boas práticas
A performance de Keyed Services é comparável à resolução tradicional de serviços, com overhead mínimo para o mapeamento chave-implementação. No entanto, desenvolvedores devem estar atentos a práticas recomendadas para evitar armadilhas comuns. Chaves devem ser definidas como constantes string em locais centralizados, preferencialmente em classes estáticas ou enums, para evitar erros de digitação que só seriam descobertos em tempo de execução. A nomenclatura das chaves deve seguir convenções consistentes dentro do domínio da aplicação. Para cenários onde a chave é determinada dinamicamente com base em dados de entrada, validações devem garantir que apenas chaves válidas sejam passadas para GetRequiredKeyedService, preferencialmente com fallbacks apropriados quando necessário.
Compatibilidade com abordagens existentes
Keyed Services não pretendem substituir todas as outras formas de resolução de dependência, mas sim complementá-las. Em muitos sistemas, uma abordagem híbrida será ideal – usando resolução tradicional para serviços únicos e Keyed Services para variações. O container DI do .NET 8 suporta perfeitamente essa coexistência, permitindo que o mesmo tipo de serviço seja registrado tanto tradicionalmente quanto com chaves, sem conflitos. Essa flexibilidade permite migrações graduais – equipes podem começar aplicando Keyed Services a novos requisitos enquanto mantêm implementações existentes intactas, reduzindo o risco e esforço de refatoração.
A evolução do ecossistema .NET continua a endereçar desafios práticos enfrentados por desenvolvedores no dia a dia. Keyed Services representam mais que uma feature técnica – eles encapsulam uma filosofia de design que valoriza simplicidade, manutenibilidade e clareza intencional. À medida que arquiteturas se tornam mais distribuídas e baseadas em eventos, padrões que reduzem complexidade acidental enquanto mantêm flexibilidade tornam-se indispensáveis. A transição de switch statements para resolução baseada em chaves não é apenas uma mudança sintática, mas um passo em direção a sistemas mais resilientes e adaptáveis, onde novas funcionalidades se integram naturalmente sem comprometer a integridade do código existente.

