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:

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:

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 — “Cria uma vez e vive pra sempre”

Cria o serviço uma vez só e nunca mais cria outro.

Quando usar

Podemos utilizar o escopo singleton quando o objeto:

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:

Scoped — Um por request HTTP”

Criar um por requisição HTTP

Quando usar:

Exemplo real

Um serviço de carrinho de comrpas:

public class CarrinhoService
{
    public List<string> Produtos = new();
}

Registro:

builder.Services.AddScoped<CarrinhoService>();

Resultado:

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:

Exemplo real

Serviço de validação

public class ValidadorCpfService
{
    public bool Validar(string cpf) => cpf.Length == 11;
}

Registro:

builder.Services.AddTransient<ValidadorCpfService>();

| --- | --- | --- | --- | --- |

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."

Tudo automático.