Injeção de Dependência em C#: Entendendo Singleton, Scoped e Transient
A injeção de dependência é um assunto recorrente no desenvolvimento de software (saiba mais), principalmente em aplicações bem construídas e que procuram seguir os princípios SOLID, ela promove o princípio da inversão de controle (IoC), permitindo que as dependências de uma classe sejam fornecidas externamente, em vez de serem criadas internamente. Isso facilita o teste unitário, a manutenção e a escalabilidade do código.
No ASP.NET Core, o contêiner de DI integrado gerencia o ciclo de vida das dependências através de três tipos principais: Singleton, Scoped e Transient.
No ASP.NET Core, o contêiner de DI integrado gerencia o ciclo de vida das dependências através de três tipos principais: Singleton, Scoped e Transient. Cada um define como e quando uma instância de um serviço é criada e reutilizada!
Tem sido item recorrente em entrevistas, principalmente de desenvolvedores que alguns se confundam com a definição sobre cada um destes ciclos de vida.
Conceitos Básicos
Antes de mergulharmos nos tipos específicos, é importante entender o fluxo básico da DI no ASP.NET Core:
- Registro de Serviços: No método ConfigureServices (ou no Program.cs em .NET 6+), você registra serviços usando o IServiceCollection. (lá nos tempos de Asp.Net Framework, haviam pacotes para gerenciar a injeção de Dependência, como Ninject, Autofac e Castle Windosor).
- Resolução de Dependências: O contêiner resolve dependências automaticamente ao injetar instâncias via construtores, métodos ou propriedades.
- Ciclo de Vida: O tipo de registro determina o ciclo de vida da instância.
Agora, vamos aos detalhes de cada tipo:
Singleton
Conceito
O modo Singleton cria uma única instância do serviço que é compartilhada por toda a aplicação. Essa instância é criada na primeira vez que é solicitada e reutilizada em todas as solicitações subsequentes. É ideal para serviços que não mantêm estado (stateless) ou que precisam de um estado global compartilhado, como caches ou configurações globais.
Vantagens:
- Economia de recursos, pois evita criar múltiplas instâncias.
- Útil para serviços caros de inicializar.
Desvantagens:
- Pode causar problemas de concorrência se o serviço mantiver estado mutável.
- Difícil de testar em cenários isolados.
Exemplo
Imagine um serviço de logging global.
// Interface do serviço
public interface ILoggerService
{
void Log(string message);
}
// Implementação
public class LoggerService : ILoggerService
{
private readonly List<string> _logs = new List<string>(); // Estado compartilhado
public void Log(string message)
{
_logs.Add(message);
Console.WriteLine($"Log: {message}. Total logs: {_logs.Count}");
}
}
// Registro no Program.cs ou Startup.cs
builder.Services.AddSingleton<ILoggerService, LoggerService>(); // Registro como SingletonVocê aí implementa numa classe controller a chamada assim:
public class HomeController : Controller
{
private readonly ILoggerService _logger;
public HomeController(ILoggerService logger)
{
_logger = logger;
}
public IActionResult Index()
{
_logger.Log("Acesso à página inicial");
return View();
}
}Se múltiplos usuários acessarem a página, todos usarão a mesma instância de LoggerService, e o contador de logs será incrementado globalmente.
Scoped
Conceito
O modo Scoped cria uma instância por “escopo”, que em aplicações web ASP.NET Core corresponde a uma solicitação HTTP. Fora do contexto web, você pode criar escopos manualmente com IServiceScopeFactory. É perfeito para serviços que precisam de estado por solicitação, como repositórios de banco de dados que gerenciam transações.
Vantagens:
- Isola o estado por solicitação, evitando vazamentos de dados entre usuários.
- Bom equilíbrio entre desempenho e isolamento.
Desvantagens:
- Não é adequado para estados globais.
- Em cenários não-web, requer gerenciamento manual de escopos.
Exemplo
Considere um serviço de repositório para um banco de dados.
// Interface
public interface IRepository
{
void AddItem(string item);
List<string> GetItems();
}
// Implementação
public class Repository : IRepository
{
private readonly List<string> _items = new List<string>(); // Estado por escopo
public void AddItem(string item)
{
_items.Add(item);
}
public List<string> GetItems()
{
return _items;
}
}
// Registro
builder.Services.AddScoped<IRepository, Repository>(); // Registro como ScopedEm uma classe controller:
public class DataController : Controller
{
private readonly IRepository _repo;
public DataController(IRepository repo)
{
_repo = repo;
}
public IActionResult Add(string item)
{
_repo.AddItem(item);
var items = _repo.GetItems(); // Retorna itens apenas desta solicitação
return Json(items);
}
}Em uma única solicitação HTTP, múltiplas chamadas ao _repo usarão a mesma instância. Em solicitações diferentes, instâncias separadas serão criadas.
Transient
Conceito
O modo Transient cria uma nova instância toda vez que o serviço é solicitado. É o mais “leve” e seguro para serviços que não compartilham estado ou que são baratos de criar.
Vantagens:
- Máximo isolamento; cada resolução é independente.
- Evita problemas de concorrência.
Desvantagens:
- Pode ser ineficiente se o serviço for caro de instanciar.
- Consome mais memória em cenários de alta carga.
Exemplo
Um serviço simples de cálculo.
// Interface
public interface ICalculator
{
int Add(int a, int b);
}
// Implementação
public class Calculator : ICalculator
{
public Calculator()
{
Console.WriteLine("Nova instância de Calculator criada!");
}
public int Add(int a, int b)
{
return a + b;
}
}
// Registro
builder.Services.AddTransient<ICalculator, Calculator>(); // Registro como TransientEm uma classe controller:
public class MathController : Controller
{
private readonly ICalculator _calc;
public MathController(ICalculator calc)
{
_calc = calc; // Nova instância aqui
}
public IActionResult Sum(int a, int b)
{
var result = _calc.Add(a, b); // Se injetar outra dependência que use ICalculator, será outra instância
return Json(result);
}
}Cada injeção ou resolução manual cria uma nova instância, como mostrado no construtor.
Comparação Rápida
Conclusão
Escolher o ciclo de vida correto é crucial para evitar bugs sutis, como vazamentos de memória ou estados compartilhados indesejados. Sempre teste seus serviços em cenários reais e considere o contexto da aplicação (web vs. console). Para mais avançado, explore capturas de dependências (e.g., Singleton capturando Scoped) e use ferramentas como o Visual Studio para depurar o contêiner de DI.
Gostou do artigo? clique no ícone👏e me siga para ver as próximas publicações !! Quer ver mais conteúdos, acesse minhas redes através do Linktree: https://linktree.com/nizzola
Outros Posts sobre Injeção de Dependência
Simplificando a injeção de dependência para serviços e repositórios
Problemas com Worker Services e Injeção de dependência no .NET?
Como fazer injeção de dependência em um console application com .Net Core.
