Entendendo o Lifetime dos Serviços em .NET com Injeção de Dependência
A Injeção de Dependência (DI) é um padrão fundamental no desenvolvimento moderno com .NET, ele permite que componentes e serviços sejam fornecidos automaticamente pelo container, promovendo um codigo mais limpo, testável e desacoplado, e parte essencial desse mecanismo é o ciclo de vida dos serviços (ou service lifetimes), que define por quanto tempo e em que contexto uma instância de serviço criada é utilizada.
O que significa ciclo de vida de serviço?
Quando registramos um serviço no container de DI usando métodos como AddSingleton, AddScoped ou AddTransient, estamos dizendo ao .NET como e quando instanciar o serviço, e como essa instância será compartilhada com as dependências que a requisitam. A seguir, farei um breve resumo de como funciona cada um destes ciclos de vida.
Singleton - Uma única instância para a aplicação
Um serviço registrado como singleton tem apenas uma instância criada durante toda a vida útil da aplicação, ou seja, toda vez que alguém solicitar esse serviço, o container retorna a mesma instância.
Quando usar
É recomendado usar para serviços que:
- Têm estado compartilhado ou configurado uma única vez
- São custosos para criar
- Precisam ser consistentes durante toda a execucação da aplicação.
Observações importantes - Todos os consumidores compartilham a mesma instância
- Deve ser thread-safe, já que será usado por múltiplas requisições simultâneas
Exemplo
public interface IAppSettingsService
{
string ApplicationName { get; }
bool EnableNewFeature { get; }
}
public class AppSettingsService : IAppSettingsService
{
public string ApplicationName { get; } = "Minha API";
public bool EnableNewFeature { get; } = true;
}
Registro:
builder.Services.AddSingleton<IAppSettingsService, AppSettingsService>();
Uso em qualquer lugar:
public class HealthController : ControllerBase
{
private readonly IAppSettingsService _settings;
public HealthController(IAppSettingsService settings)
{
_settings = settings;
}
[HttpGet]
public IActionResult Get()
{
return Ok(_settings.ApplicationName);
}
}
O que acontece na prática:
- A instância de AppSettingsService é criada uma única vez
- Todas as requisições usam a mesma instância
- Não existe risco de dados inconsistentes
Scoped - Uma instância por escopo
Um serviço scoped é criado uma vez por escopo. Em aplicações web ASP.NET Core, normalmente um escopo significa uma requisição HTTP. Isso significa que dentro de uma mesma requisição, cada vez que o serviço for injetado, será retornada a mesma instância, mas em outra requisição, uma nova instância será criada.
Quando usar
Use scoped quando:
- Você precisa compartilhar estado durante uma requisição (como contexto de banco de dados)
- Precisa garantir consistência em toda uma operação (ex: transação) durante uma requisição.
Exemplo
O exemplo mais clássico: DbContext
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(connectionString));
O AddDbContext já é registado como Scoped.
Porque:
- o DbContext mantém estado
- ele controla o tracking das entidades
- tudo deve acontecer dentro da mesma requisição
Exemplo real de um service de negócio:
public interface IUserService
{
Task CreateUserAsync(CreateUserDto dto);
}
public class UserService : IUserService
{
private readonly AppDbContext _context;
public UserService(AppDbContext context)
{
_context = context;
}
public async Task CreateUserAsync(CreateUserDto dto)
{
var user = new User { Name = dto.Name };
_context.Users.Add(user);
await _context.SaveChangesAsync();
}
}
Registro:
builder.Services.AddScoped<IUserService, UserService>();
O que acontece na prática:
- Para cada requisição HTTP, uma instância de
UserServiceé criada - O
UserServicee oDbContextcompartilham a mesma instância - Quando a requisição termina, tudo é descartado
- Evita vazamento de memória, conflito entre requisições e dados "vazando" de um usuário para outro
Transient - Nova instância a cada requisição
Um serviço registrado como transient é criado sempre que for solicitado. Cada vez que o container DI resolve esse serviço (seja em um controller, outro serviço ou várias vezes no mesmo escopo), uma nova instância é gerada.
Quando usar
Use transient para serviços:
- Leves e sem estado (stateless)
- Que não mantêm dados entre chamadas
- Que não dependem de recursos compartilhados
Exemplo
public interface IPasswordHasherService
{
string Hash(string password);
}
public class PasswordHasherService : IPasswordHasherService
{
public string Hash(string password)
{
return BCrypt.Net.BCrypt.HashPassword(password);
}
}
Registro:
builder.Services.AddTransient<IPasswordHasherService, PasswordHasherService>();
Uso:
public class UserService : IUserService
{
private readonly IPasswordHasherService _hasher;
public UserService(IPasswordHasherService hasher)
{
_hasher = hasher;
}
public void CreateUser(string password)
{
var hash = _hasher.Hash(password);
// logica para salvar usuario no banco
}
}
O que acontece na prática:
- Toda vez que o
UserServicefor criado, um novoPasswordHasherServiceé criado - Ele não guarda estado → zero problema
- É barato de criar → ok usar Transient
⚠️ Boas práticas e armadilhas
🚫 Injetar Scoped dentro de Singleton
Uma má prática comum é tentar injetar um serviço Scoped dentro de um Singleton. Isso causa problemas pois o singleton é criado antes de qualquer escopo de requisição existir, e o serviço scoped pode acabar sendo usado de forma incorreta ou permanecer "vivo" além do esperado. Felizmente, o ASP.NET Core, por padrão, irá checkar por esta configuração errada e irá reportar um erro de validação de escopo quando o app iniciar.
✅ Regras gerais
- Singleton pode depender de singleton
- Scoped pode depender de scoped ou transient
- Transient pode depender de qualquer tipo
Conclusão
Entender os tipos de ciclo de vida dos serviços é essencial para escrever aplicações .NET eficientes e confiáveis.
Escolher o ciclo de vida certo ajuda a controlar memória, manter consistência de dados, e evitar bugs difíceis de rastrear.