Introdução: o que é Dependency Injection e por que é necessário
Dependency Injection (DI) é um dos principais padrões de design que implementa o princípio de inversão de dependências (Dependency Inversion Principle) do SOLID. Na prática, isso significa que os objetos não criam suas próprias dependências, mas as recebem de fora. Isso torna o código flexível, testável e com baixo acoplamento.
No ecossistema C# e .NET, o DI tornou-se o padrão de facto. A partir do .NET Core, a plataforma possui seu próprio contêiner de injeção de dependência, usado no ASP.NET Core, serviços em segundo plano e até mesmo em aplicativos de console. Sem entender DI, é impossível imaginar o desenvolvimento moderno em C#.
Neste artigo, vamos analisar o DI com exemplos reais: desde a injeção manual até o uso do contêiner embutido do .NET. Você verá como eliminar acoplamentos rígidos e tornar o código adequado para testes de unidade.
O problema sem Dependency Injection
Vamos imaginar um cenário típico: a classe OrderService processa pedidos e envia notificações por e-mail. Sem DI, o código fica assim:
public class EmailSender{ public void Send(string to, string subject, string body) { Console.WriteLine($"Enviando e-mail para {to}: {subject}"); }}
public class OrderService{ private EmailSender _emailSender;
public OrderService() { // Acoplamento rígido — a classe cria sua própria dependência _emailSender = new EmailSender(); }
public void ProcessOrder(int orderId) { // Lógica de processamento do pedido... _emailSender.Send("user@example.com", "Pedido confirmado", $"Seu pedido #{orderId} foi recebido."); }}Qual é o problema? OrderService está rigidamente acoplado à implementação específica de EmailSender. Se quisermos substituir o envio de e-mail por SMS ou registro em log, teremos que alterar o código de OrderService. O teste também se torna mais difícil: não é possível substituir o EmailSender real por um objeto mock.
Solução: injeção de dependências via construtor
O primeiro passo para o DI é extrair uma abstração (interface) e passar a dependência pelo construtor. Isso é chamado de Constructor Injection.
Passo 1: Criar a interface
public interface INotificationService{ void Send(string to, string subject, string body);}Passo 2: Implementar a interface
public class EmailSender : INotificationService{ public void Send(string to, string subject, string body) { Console.WriteLine($"E-mail enviado: {subject} -> {to}"); }}
public class SmsSender : INotificationService{ public void Send(string to, string subject, string body) { Console.WriteLine($"SMS enviado: {subject} -> {to}"); }}Passo 3: Injetar a dependência pelo construtor
public class OrderService{ private readonly INotificationService _notificationService;
// A dependência é passada de fora public OrderService(INotificationService notificationService) { _notificationService = notificationService; }
public void ProcessOrder(int orderId) { // Lógica de processamento do pedido... _notificationService.Send("user@example.com", "Pedido confirmado", $"Seu pedido #{orderId} foi recebido."); }}Agora, OrderService não sabe qual implementação específica está sendo usada. Podemos usar EmailSender, SmsSender ou qualquer outra classe que implemente INotificationService. O código se tornou flexível e testável.
Usando o contêiner IoC embutido no .NET
Criar todas as dependências manualmente é inconveniente. O .NET possui um contêiner embutido (contêiner IoC) que gerencia automaticamente o ciclo de vida dos objetos. Vamos ver um exemplo para um aplicativo de console.