Quando se estuda backend, um dos primeiros conceitos importantes que aparece é a Injeção de Dependência (DI - Dependency Injection). Atualmente, o ciclo de desenvolvimento é rápido e exige atualizações constantes. Acontece que ao realizar alterações no seu código, você não deseja ter efeitos colaterais e erros inesperados que afetam outras partes do seu código. Para minimizar o máximo esse problema a solução é criar aplicações modulares onde as dependências entre esses módulos sejam reduzidas e você tenha um baixo acoplamento e uma alta coesão.
O que é dependência? (Código Acoplado)
Toda classe do seu sistema precisa de “coisas” para funcionar, essas “coisas ” são chamadas de dependência.
Exemplo real:
public class PedidoService
{
public void FinalizarPedido()
{
var emailService = new EmailService();
emailService.Enviar("Pedido finalizado!");
}
}
Nesse exemplo, o PedidoService
depende de EmailService
para enviar e-mails. Até o momento parece ok, mas existe um grande problema, o código está acoplado.
O que é forte acoplamento?
PedidoService
conhece demais sobre o funcionamento do EmailService
, ele mesmo cria new
e usa. Ou seja, está preso, travado naquela implementação, que resulta em um código difícil de manter, testar e evoluir.
O que isso causa na prática?
Situação | Problema gerado |
---|---|
Quero trocar o envio de e-mail por SMS | Tem que abrir PedidoService e trocar tudo |
Quero testar PedidoService sem enviar e-mail de verdade |
Não dá, porque ele sempre cria EmailService
|
Quero usar um serviço de e-mail diferente | Tem que mexer dentro de PedidoService
|
Tenho vários serviços usando EmailService
|
Cada um cria um new EmailService() → duplicação de código, desperdício de memória |
A Solução: Inversão de Controle (IoC)
É nesse ponto que nasce um conceito importante: “Quem deve controlar qual serviço eu uso não é minhja classe. É alguém externo”, esse “alguém” é o próprio sistema através de um container de injeção. Desse modo, nosso PedidoService
só vai receber pronto o que ele precisa.
Como fazemos isso na prática?
1. Criamos uma interface (um contrato)
public interface INotificacaoService
{
void Enviar(string mensagem);
}
Aqui estamos dizendo: "Eu não me importo como a mensagem será enviada. Só quero que exista alguém capaz de executar o método Enviar()
." Isso é lindo. Porque o PedidoService não quer saber de e-mail, SMS, WhatsApp, sinal de fumaça... ele só quer enviar.
2. Criamos implementações dessa interface
Enviar por E-mail:
public class EmailService : INotificacaoService
{
public void Enviar(string mensagem)
{
Console.WriteLine("Enviando Email: " + mensagem);
}
}
3. PedidoService não se importa mais com o como
Agora ele só recebe a dependência via construtor:
public class PedidoService
{
private readonly INotificacaoService _notificacaoService;
// INJEÇÃO DE DEPENDÊNCIA VIA CONSTRUTOR
public PedidoService(INotificacaoService notificacaoService)
{
_notificacaoService = notificacaoService;
}
public void FinalizarPedido()
{
_notificacaoService.Enviar("Pedido finalizado!");
}
}
Por que preciso do construtor?
Porque o construtor é o momento onde o .NET vai "enfiar" dentro da sua classe a dependência correta. É tipo assim:
- PedidoService: "Ei .NET, pra eu existir, eu só funciono se alguém me entregar um INotificacaoService."
- .NET: "De boa, fica tranquilo que eu te entrego."
Esse é o papel do construtor na DI.
Mas, quem entrega esse serviço?
O motor por trás disso tudo chama-se: IoC Container (Inversion of Control Container).
É um recurso do .NET que guarda a informação:
- Quem implementa o quê.
- Quem precisa de quem.
- Quem depende de quem.
Registrando serviços no IoC Container
Na class Program.cs
builder.Services.AddScoped<INotificacaoService, EmailService>();
builder.Services.AddScoped<PedidoService>();
Pedido | Entrega |
---|---|
Alguém pedir INotificacaoService | Entrega EmailService |
Alguém pedir PedidoService | Entrega PedidoService (e injeta EmailService dentro dele) |
Escopo de Serviços
Até agora entendemos quem cria os objetos (IoC Container), como ele sabe o que criar (lendo o construtor) e onde ensinamos essas regras (Program.cs).
Agora a questão é: depois que o IoC cria o objeto… ele fica vivo por quanto tempo? É nesse contexto que nasce os escopos de serviços. Escopo de serviço é nada mais do que: O tempo de vida do objeto dentro da aplicação. Existem 3 tipos principais no .NET
- Singleton
- Scoped
- Transient
Singleton — “Cria uma vez e vive pra sempre”
Cria o serviço uma vez só e nunca mais cria outro.
- Fica guardado na memória
- Todo mundo que precisar dele, recebe exatamente a mesma instância
Quando usar:
Podemos utilizar o escopo singleton
quando o objeto:
- Não muda (imutável)
- Não depende de contexto do usuário
- É caro pra criar (evita custo repetido)
- Precisa ser compartilhado
builder.Services.AddSingleton<IMeuServico, MeuServico>();
Exemplo real
Serviço de configuração:
public class AppSettingsService
{
public string ApiKey { get; set; } = "123456";
}
Registro:
builder.Services.AddSingleton<AppSettingsService>();
Resultado:
- Um único AppSettingsService existe pra todos os requests.
Vantagem de performance:
- Não cria sempre → super rápido pra resolver.
- Não consome CPU criando objetos toda hora.
- Ideal pra cache, logger, config, etc.
Riscos:
- Compartilhamento indesejado de dados.
- Thread Safety (atenção em aplicações multi-thread).
- Nunca guardar informação que muda!
Scoped — Um por request HTTP”
Criar um por requisição HTTP
- Dentro do mesmo request é o mesmo objeto
- Quando acaba o request, o objeto morre
Quando usar:
Quando o objeto:
- Trabalha com dados do usuário.
- Precisa de isolamento por requisição.
- Exemplo clássico: DbContext
builder.Services.AddScoped<IMeuServico, MeuServico>();
Transient
Cria sempre que pedir, não importa quem
builder.Services.AddTransient<IMeuServico, MeuServico>();
O .NET faz tudo isso automaticamente assim:
Alguém lá no seu Controller faz:
[ApiController]
[Route("api/[controller]")]
public class PedidoController : ControllerBase
{
private readonly PedidoService _pedidoService;
public PedidoController(PedidoService pedidoService)
{
_pedidoService = pedidoService;
}
}
→ Aí o .NET pensa assim:
"Beleza, preciso montar um PedidoController."
"Pra isso, preciso de um PedidoService."
"Opa, mas o PedidoService precisa de um INotificacaoService..."
"Já sei que INotificacaoService = EmailService (porque me ensinaram no Program.cs)"
"Vou criar EmailService..."
"Passar ele dentro de PedidoService..."
"E passar PedidoService dentro de PedidoController."
Tudo automático.
Registrando serviços no IoC Container
Na class Program.cs
builder.Services.AddScoped<INotificacaoService, EmailService>();
builder.Services.AddScoped<PedidoService>();
Pedido | Entrega |
---|---|
Alguém pedir INotificacaoService | Entrega EmailService |
Alguém pedir PedidoService | Entrega PedidoService (e injeta EmailService dentro dele) |
Escopo de Serviços
Até agora entendemos quem cria os objetos (IoC Container), como ele sabe o que criar (lendo o construtor) e onde ensinamos essas regras (Program.cs).
Agora a questão é: depois que o IoC cria o objeto… ele fica vivo por quanto tempo? É nesse contexto que nasce os escopos de serviços. Escopo de serviço é nada mais do que: O tempo de vida do objeto dentro da aplicação. Existem 3 tipos principais no .NET
- Singleton
- Scoped
- Transient
Singleton — “Cria uma vez e vive pra sempre”
Cria o serviço uma vez só e nunca mais cria outro.
- Fica guardado na memória
- Todo mundo que precisar dele, recebe exatamente a mesma instância
Quando usar
Podemos utilizar o escopo singleton
quando o objeto:
- Não muda (imutável)
- Não depende de contexto do usuário
- É caro pra criar (evita custo repetido)
- Precisa ser compartilhado
builder.Services.AddSingleton<IMeuServico, MeuServico>();
Exemplo real
Serviço de configuração:
public class AppSettingsService
{
public string ApiKey { get; set; } = "123456";
}
Registro:
builder.Services.AddSingleton<AppSettingsService>();
Resultado:
- Um único AppSettingsService existe pra todos os requests.
Scoped — Um por request HTTP”
Criar um por requisição HTTP
- Dentro do mesmo request é o mesmo objeto
- Quando acaba o request, o objeto morre
Quando usar:
- Trabalha com dados do usuário.
- Precisa de isolamento por requisição.
- Exemplo clássico: DbContext\
Exemplo real
Um serviço de carrinho de comrpas:
public class CarrinhoService
{
public List<string> Produtos = new();
}
Registro:
builder.Services.AddScoped<CarrinhoService>();
Resultado:
- João adicionou itens → o carrinho dele é só dele.
- Maria fez outro request → carrinho dela é outro.
Transient — “Pediu? Cria novo.”
Cria sempre que pedir, não importa quem, cria um novo objeto.
Até na mesma requisição, cada injeção cria uma nova instância
Quando usar?
Quando o objeto:
- Não guarda estado
- Não tem problema e ser criado várias vezes
- É leve e simples
Exemplo real
Serviço de validação
public class ValidadorCpfService
{
public bool Validar(string cpf) => cpf.Length == 11;
}
Registro:
builder.Services.AddTransient<ValidadorCpfService>();
Escopo | Quando cria? | Quando morre? | Vantagem | Risco |
---|---|---|---|---|
Singleton | 1 vez só | Quando app morre | Performance altíssima | Compartilhar dados sem querer |
Scoped | 1 por request | Quando request acaba | Isolamento por usuário | Usar dentro de Singleton = erro |
Transient | Sempre | Imediato após uso | Seguro e limpo | Gastar recursos demais |