Dependency injection
Escopo: C#. Idiomas específicos deste ecossistema.
DI (Dependency Injection · Injeção de Dependências) é o padrão em que a classe declara no construtor tudo de que precisa, e alguém de fora entrega. Esse alguém é o container do .NET, que monta a cadeia inteira sozinho: para criar o OrderService ele descobre que precisa de um IOrderRepository, cria a implementação registrada, e assim por diante. A classe fica com uma responsabilidade só, que é usar as dependências. E como quem entrega é de fora, o teste entrega um repositório que guarda os dados em memória no lugar do que fala com o banco, sem tocar no código da classe.
Conceitos fundamentais
| Conceito | O que é |
|---|---|
| DI (Dependency Injection · Injeção de Dependências) | Padrão em que o container fornece dependências em vez de a classe construí-las |
| IoC container (Inversion of Control · Inversão de Controle) | Componente que registra serviços e resolve o grafo de dependências por construtor |
| Service Locator (localizador de serviços) | Antipadrão: buscar dependências do container dentro da classe; torna dependências implícitas |
| constructor injection (injeção via construtor) | Forma preferida: parâmetros do construtor declaram tudo que a classe precisa |
| Singleton (instância única) | Tempo de vida em que uma única instância serve todo o app |
| Scoped (por escopo de requisição) | Tempo de vida em que uma instância dura por requisição/escopo |
| Transient (por chamada) | Tempo de vida em que cada resolução cria uma nova instância |
| captive dependency (dependência cativa) | Bug em que Singleton mantém referência a Scoped/Transient, vazando além do escopo |
Service locator
Receber o IServiceProvider no construtor e pedir a ele as dependências lá dentro parece injeção, e funciona ao contrário dela. A assinatura da classe deixa de contar do que ela depende, e descobrir isso passa a exigir ler o corpo de todos os métodos. O teste também perde: em vez de passar um repositório falso, é preciso montar um container inteiro para satisfazer a classe.
❌ Ruim: dependência implícita, acoplado ao container
public class OrderService(IServiceProvider services)
{
public async Task<Result<Invoice>> ProcessOrderAsync(OrderRequest request, CancellationToken ct)
{
var repository = services.GetRequiredService<IOrderRepository>();
var notifier = services.GetRequiredService<INotifier>();
// ...
}
}
✅ Bom: dependências explícitas no contrato
public class OrderService(IOrderRepository orderRepository, INotifier notifier)
{
public async Task<Result<Invoice>> ProcessOrderAsync(OrderRequest request, CancellationToken ct)
{
// ...
}
}
Construtor primário
O C# 12 deixa declarar os parâmetros ao lado do nome da classe e usá-los direto no corpo. Some o trio que toda classe injetada repetia: o campo privado, o parâmetro e a linha que copia um no outro.
❌ Ruim: construtor explícito verboso
public class OrderService
{
private readonly IOrderRepository _orderRepository;
private readonly INotifier _notifier;
public OrderService(IOrderRepository orderRepository, INotifier notifier)
{
_orderRepository = orderRepository;
_notifier = notifier;
}
}
✅ Bom: primary constructor, DI direta e concisa
public class OrderService(IOrderRepository orderRepository, INotifier notifier)
{
private readonly IOrderRepository _orderRepository = orderRepository;
private readonly INotifier _notifier = notifier;
}
Tempo de vida de cada dependência
Ao registrar um serviço, você decide quanto tempo a instância dele vive. Scoped cria uma por requisição HTTP, que é o certo para repositório e DbContext, porque a requisição inteira compartilha a mesma transação. Transient cria uma nova a cada pedido. Singleton cria uma só e a mantém enquanto a aplicação estiver de pé.
| Lifetime | Instância | Quando usar |
|---|---|---|
Scoped | Uma por request HTTP | Repositórios, services de domínio, DbContext |
Transient | Nova a cada resolução | Objetos leves e sem estado compartilhado |
Singleton | Uma para toda a aplicação | Configuração, cache, IHttpClientFactory |
A combinação perigosa é a captive dependency (dependência cativa): um Singleton que recebe um Scoped no construtor guarda para sempre a primeira instância que chegou. O DbContext daquela primeira requisição continua vivo, sendo usado pelas requisições seguintes. O bug aparece longe da causa, como dado de um usuário aparecendo na sessão de outro.
❌ Ruim: singleton captura scoped
builder.Services.AddSingleton<ReportService>();
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
public class ReportService(IOrderRepository orderRepository) { } // capturado na primeira resolução
✅ Bom: lifetimes compatíveis
builder.Services.AddScoped<ReportService>();
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
Interface para testabilidade
Declare a dependência pela interface (IOrderRepository), e não pela classe concreta (SqlOrderRepository). A produção registra a implementação que fala com o banco; o teste registra uma que guarda tudo em memória. O código do serviço permanece o mesmo nos dois casos, e é isso que permite testar a regra de negócio sem subir um banco.
❌ Ruim: dependência concreta, impossível substituir em testes
public class OrderService(SqlOrderRepository orderRepository) { }
✅ Bom: dependência por interface, substituível
public class OrderService(IOrderRepository orderRepository) { }
// produção
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
// testes
services.AddScoped<IOrderRepository, FakeOrderRepository>();
Registrar os handlers varrendo o assembly
Num domínio com dezenas de handlers, a lista de registro cresce uma linha por handler, e o handler novo que ninguém registrou só quebra quando a rota é chamada. Dá para varrer o assembly com reflection e registrar tudo que implementa uma interface marcadora, que é uma interface vazia criada só para identificar essas classes. Handler novo passa a ser registrado por existir.
❌ Ruim: registro manual, cresce junto com os handlers
public static WebApplicationBuilder AddOrders(this WebApplicationBuilder builder)
{
builder.Services.AddScoped<FindOrdersHandler>();
builder.Services.AddScoped<FindOrderByIdHandler>();
builder.Services.AddScoped<CreateOrderHandler>();
builder.Services.AddScoped<UpdateOrderHandler>();
builder.Services.AddScoped<CancelOrderHandler>();
// a cada novo handler, uma nova linha aqui
return builder;
}
✅ Bom: registro por convenção via reflection
// interface marcadora: sem métodos, só para identificar handlers no assembly
public interface IHandler { }
public class CreateOrderHandler(OrderService orderService) : IHandler { }
public class FindOrderByIdHandler(OrderService orderService) : IHandler { }
public static WebApplicationBuilder AddOrders(this WebApplicationBuilder builder)
{
var assembly = typeof(CreateOrderHandler).Assembly;
var handlerTypes = assembly.GetTypes()
.Where(t => typeof(IHandler).IsAssignableFrom(t) && t is { IsAbstract: false, IsInterface: false });
foreach (var handlerType in handlerTypes)
builder.Services.AddScoped(handlerType);
builder.Services.AddScoped<OrderService>();
return builder;
}
Scrutor é uma biblioteca popular que adiciona assembly scanning fluente ao container nativo do .NET. Útil quando o registro por convenção é usado em vários domínios e a abordagem manual via reflection se repete.
Onde fica o registro
Cada domínio registra as próprias dependências no seu extension method, e o Program.cs chama esses métodos. Veja Project Foundation.
DoDocs v3.7.0 · Desenvolvido por @thiagocajadev · Baseado no trabalho de pmndrs/docs · Poimandres.