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