Executando verificação de segurança...
1

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 UserService e o DbContext compartilham 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 UserService for criado, um novo PasswordHasherService é 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.

Carregando publicação patrocinada...