A arte de largar o controlo: um guia humano sobre injecção de dependências em C# é uma reflexão sobre desaprender o instinto de controlar tudo e confiar que as ferramentas certas nos serão entregues no momento certo. Este artigo percorre a filosofia, o desafio e a implementação prática de DI em aplicações .NET modernas.
Todo o programador já escreveu código assim: uma classe que cria as suas próprias dependências, decide os seus próprios colaboradores e acaba por se tornar um monólito de lógica fortemente acoplada. Parece natural — precisas de um logger, crias um logger. Precisas de uma ligação à base de dados, instancias uma ali mesmo, em linha, com a palavra-chave new e uma sensação de controlo absoluto.
O problema não é a palavra-chave new. O problema é o pressuposto que está enterrado dentro dela: que esta classe sabe melhor do que ninguém o que precisa e como o obter.
Quando codificas dependências diretamente, tornas os testes praticamente impossíveis. Tornas a substituição de implementações uma operação cirúrgica. Tornas o teu código frágil exactamente nos sítios onde precisa de ser flexível — nas costuras entre componentes.
A Injecção de Dependências não é um padrão sobre frameworks ou contentores. É uma mudança de mentalidade: deixa de construir coisas que sabem como adquirir o que precisam, e começa a construir coisas que simplesmente declaram o que precisam. Deixa que outra coisa — um contentor, uma fábrica, um harness de testes — entregue.
O primeiro passo é deixar de depender de tipos concretos e começar a depender de interfaces. Este é o contrato que a tua classe anuncia ao mundo.
public interface ITransactionRepository
{
Task<TransactionStatus> GetStatusAsync(Guid transactionId);
Task<bool> CommitAsync(Guid transactionId);
}A lógica de negócio nunca deve importar-se se os dados vêm do SQL Server, de um armazenamento em memória ou de um mock num teste unitário. Só interessa a forma dos dados.
A injecção por construtor é a forma mais explícita e honesta de DI. Torna as dependências visíveis, não opcionais e fáceis de raciocinar.
public class PaymentProcessor
{
private readonly ITransactionRepository _repository;
private readonly ILogger<PaymentProcessor> _logger;
public PaymentProcessor(
ITransactionRepository repository,
ILogger<PaymentProcessor> logger)
{
_repository = repository;
_logger = logger;
}
public async Task<bool> ProcessAsync(Guid transactionId)
{
_logger.LogInformation("A processar transacção {Id}", transactionId);
var status = await _repository.GetStatusAsync(transactionId);
if (status != TransactionStatus.Pending)
{
_logger.LogWarning("A transacção {Id} não está num estado processável.", transactionId);
return false;
}
return await _repository.CommitAsync(transactionId);
}
}Repara no que o PaymentProcessor NÃO faz: não cria uma ligação à base de dados, não sabe que framework de logging está a ser utilizado e não decide o seu próprio ciclo de vida. Simplesmente faz o seu trabalho.
Em .NET, o contentor de DI integrado vive em Microsoft.Extensions.DependencyInjection. Registas as tuas dependências uma vez, no arranque, e o contentor trata da resolução em todo o lado.
var builder = WebApplication.CreateBuilder(args);
// Registar o repositório com tempo de vida scoped (uma vez por pedido HTTP)
builder.Services.AddScoped<ITransactionRepository, SqlTransactionRepository>();
// Registar o processador — o contentor injectará o repositório automaticamente
builder.Services.AddScoped<PaymentProcessor>();
var app = builder.Build();Compreender os tempos de vida é fundamental:
O verdadeiro benefício do DI revela-se no momento dos testes. Com dependências injectadas, podes substituir implementações reais por fakes controlados.
public class PaymentProcessorTests
{
[Fact]
public async Task ProcessAsync_RetornsFalso_QuandoTransaccaoNaoEstaPendente()
{
// Arrange
var mockRepo = new Mock<ITransactionRepository>();
mockRepo
.Setup(r => r.GetStatusAsync(It.IsAny<Guid>()))
.ReturnsAsync(TransactionStatus.Completed);
var logger = NullLogger<PaymentProcessor>.Instance;
var processor = new PaymentProcessor(mockRepo.Object, logger);
// Act
var result = await processor.ProcessAsync(Guid.NewGuid());
// Assert
Assert.False(result);
}
}Sem base de dados. Sem ficheiros de configuração. Sem variáveis de ambiente. Apenas lógica pura, testada em isolamento.
Quando este processador é exposto através de um endpoint de API, o pedido recebido deverá transportar a seguinte estrutura. O controlador recebe-o, extrai o ID da transacção e delega ao PaymentProcessor injectado.
{
"transaction_id": "f3a2d1b0-4e5f-6789-abcd-ef0123456789",
"requested_by": "user-service",
"idempotency_key": "pay-20260615-00042",
"metadata": {
"source": "mobile-checkout",
"retry_attempt": 0,
"currency": "EUR"
}
}A idempotency_key é particularmente importante em contextos financeiros: garante que mesmo que o mesmo pedido seja enviado duas vezes devido a uma nova tentativa de rede, a operação é executada apenas uma vez no backend.
A Injecção de Dependências não é sobre frameworks. Não é sobre anotações, contentores ou o atributo [Inject]. É sobre a disciplina de construir componentes que declaram as suas necessidades em vez de as satisfazerem unilateralmente.
Quando largas a palavra-chave new — quando as tuas classes deixam de adquirir as suas próprias dependências — algo interessante acontece. O teu código torna-se mais fácil de ler, porque as dependências são explícitas. Torna-se mais fácil de testar, porque podes substituir qualquer coisa. Torna-se mais fácil de manter, porque mudar uma implementação não se propaga por todo o código.
A arte de largar o controlo, em software, é a arte de construir coisas que confiam no seu ambiente para lhes fornecer o que precisam. Essa confiança, formalizada através de interfaces e injectada por construtores, é o que separa o código que simplesmente funciona do código que perdura.

SOFTWARE ARCHITECT
Arquiteto de Software especializado em fintech, sistemas europeus de alta disponibilidade e migrações progressivas de monólitos para microsserviços.