AddTransient, AddScoped, AddSingleton
AddTransient, AddScoped e AddSingleton são os três métodos que o ASP.NET usa para registrar dependências, e cada um define um tempo de vida diferente para o objeto resolvido. Injeção de Dependência é uma técnica que implementa um princípio de design chamado IoC, e neste artigo vamos aprender a usá-la no ASP.NET sem cometer erros comuns.
Injeção de Dependência no ASP.NET
O ASP.NET possui recursos nativos para trabalharmos com Dependency Injection (Injeção de Dependência) de forma simples. Ou seja, você não precisa de nenhuma biblioteca externa para registrar e resolver serviços.
Porém, precisamos entender como o framework resolve as dependências para aproveitar melhor esses recursos. Caso contrário, é fácil criar bugs difíceis de rastrear, como dados de um usuário vazando para outro.
Caso esteja com dúvidas sobre DI, sugiro que veja o artigo Dependency Injection, no qual abordo o assunto mais a fundo. Além disso, o artigo Inversion of Control explica o princípio que está por trás da técnica.
Versão em vídeo
Se preferir, você também pode acompanhar o conteúdo deste artigo em vídeo.
Criando dependências
Antes de resolver as dependências, precisamos entender o que elas são. O código abaixo ilustra bem essa visão, já que temos uma dependência de ICustomerRepository.
public class OrderController : Controller
{
private readonly ICustomerRepository _repository;
public OrderController(ICustomerRepository repository)
{
_repository = repository;
}
}
Isso ocorre porque, no construtor do controlador, explicitamos a necessidade de uma abstração ou implementação externa. Dessa forma, fica impossível instanciar um OrderController sem informar um ICustomerRepository.
Acredite se quiser, mas isso é tudo o que precisamos para criar uma dependência no ASP.NET. Contudo, agora vem o segundo problema: resolvê-las.
AddTransient, AddScoped e AddSingleton no ASP.NET
Existem três formas de registrar dependências no ASP.NET: AddTransient, AddScoped e AddSingleton. Todas elas estão ligadas ao tempo de vida do objeto resolvido.
Em todos os cenários, registramos as dependências no Program.cs, antes de chamar builder.Build(). Por exemplo, logo depois de builder.Services.AddControllers().
builder.Services.AddTransient<IService, Service>();
builder.Services.AddScoped<IService, Service>();
builder.Services.AddSingleton<IService, Service>();
Na forma mais comum, o registro informa a abstração (interface) seguida da implementação (classe concreta). Assim, quem pede IService no construtor recebe uma instância de Service.
Outras formas de registro com AddTransient, AddScoped e AddSingleton
Também é possível registrar apenas a classe concreta, sem interface. Nesse caso, o construtor precisa pedir a própria classe.
builder.Services.AddTransient<Service>();
Além disso, você pode entregar uma instância pronta, mas apenas com AddSingleton. Como a instância já existe, o container sempre devolve o mesmo objeto.
builder.Services.AddSingleton(new Service());
Para os demais tempos de vida, use uma factory, ou seja, uma função que cria o objeto. Dessa forma, o container chama a função sempre que precisa de uma nova instância.
// a factory recebe o IServiceProvider para resolver outras dependências
builder.Services.AddScoped<IService>(provider => new Service());
AddTransient
Embora o registro seja parecido, o tempo de vida é bem diferente. No caso do AddTransient, por exemplo, você sempre terá uma nova instância do objeto.
builder.Services.AddTransient<ICustomerRepository, CustomerRepository>();
// ...
public class OrderController : Controller
{
private readonly ICustomerRepository _repository;
public OrderController(ICustomerRepository repository)
{
_repository = repository;
}
}
Tomando como base o código acima, toda vez que o controlador for instanciado, o ASP.NET vai gerar uma nova instância do repositório. Mais do que isso, cada classe que pedir ICustomerRepository recebe um objeto próprio, mesmo dentro da mesma requisição.
Esse cenário é ideal quando queremos executar ações pontuais e já descartar o objeto, o que normalmente cobre a maioria dos casos. Entre AddTransient, AddScoped e AddSingleton, ele é o que menos reaproveita instâncias.
Segundo a documentação de tempos de vida de serviços, em apps que processam requisições, os serviços transientes são descartados ao fim da requisição. Por outro lado, esse tempo de vida gera alocações a cada resolução, já que o container constrói o objeto toda vez.
AddScoped
O AddScoped trabalha de forma parecida com o AddTransient. Entretanto, ele retém o objeto durante toda a requisição e, sempre que invocado, retorna o mesmo objeto.
builder.Services.AddScoped<IDatabase, Database>();
builder.Services.AddTransient<ICustomerRepository, CustomerRepository>();
builder.Services.AddTransient<IProductRepository, ProductRepository>();
// ...
public class CustomerRepository : ICustomerRepository
{
private readonly IDatabase _database;
public CustomerRepository(IDatabase database)
{
_database = database;
}
}
public class ProductRepository : IProductRepository
{
private readonly IDatabase _database;
public ProductRepository(IDatabase database)
{
_database = database;
}
}
public class OrderController : Controller
{
private readonly ICustomerRepository _customerRepository;
private readonly IProductRepository _productRepository;
public OrderController(
ICustomerRepository customerRepository,
IProductRepository productRepository)
{
_customerRepository = customerRepository;
_productRepository = productRepository;
}
}
Tomando como base o código acima, temos agora CustomerRepository e ProductRepository, e ambos dependem de Database. A diferença é que Database usa AddScoped, enquanto os dois repositórios usam AddTransient.
Isso nos diz que, toda vez que o OrderController for instanciado, ele vai criar uma instância de CustomerRepository e outra de ProductRepository. No entanto, ele cria apenas uma instância de Database.
Assim que o container passa pelo construtor de CustomerRepository e resolve Database, ele guarda essa instância. A partir daí, qualquer pedido por Database na mesma requisição retorna o mesmo objeto.
Isso termina no fim da requisição. Depois de enviar a resposta para o client, o ASP.NET descarta os objetos do escopo.
Repare que o exemplo mistura dois métodos no mesmo registro, algo comum em projetos reais. Aliás, combinar AddTransient, AddScoped e AddSingleton na mesma aplicação é o cenário normal, e não a exceção.
O caso do DbContext
O exemplo mais famoso de serviço scoped é o DbContext do Entity Framework Core. Segundo a documentação, o método AddDbContext registra o contexto com tempo de vida scoped por padrão.
Isso faz sentido, porque todos os repositórios de uma requisição compartilham a mesma unidade de trabalho. Dessa forma, um único SaveChanges grava as alterações feitas por diferentes classes.
Se quiser praticar esses conceitos, o curso Dominando Injeção de Dependência traz aulas dedicadas ao tema.
Singleton
Antes de falar do AddSingleton, precisamos entender que Singleton também é o nome de um padrão de projeto. Esse padrão garante que um objeto tenha uma, e apenas uma, instância para a aplicação inteira.
Felizmente, essa é a mesma definição do que o AddSingleton faz no ASP.NET, e é justamente o que o separa dos outros dois métodos do trio AddTransient, AddScoped e AddSingleton. A diferença é que, em vez de implementar o padrão na própria classe, você delega esse controle ao container.
AddSingleton
O maior desafio do AddSingleton é saber quando usá-lo. Afinal, todos os usuários da aplicação vão compartilhar a mesma instância do objeto.
Em um app mobile ou desktop, onde temos apenas um usuário, ou mesmo no Blazor WASM, isso não é um problema. Na web, porém, a situação é diferente.
As aplicações ASP.NET ficam em constante execução, atendendo diferentes usuários. Por isso, tome muito cuidado ao usar o Singleton, já que os dados serão compartilhados entre todas as requisições.
builder.Services.AddSingleton<Configuration>();
// ...
public class OrderController : Controller
{
private readonly Configuration _config;
public OrderController(Configuration config)
{
_config = config;
}
}
Um bom exemplo de uso são as configurações da aplicação, desde que não sejam por usuário. Assim, você as carrega uma única vez no início e as reutiliza depois.
As diretrizes oficiais reforçam dois cuidados. Primeiro, um singleton precisa ser thread safe, porque várias requisições o acessam ao mesmo tempo; segundo, ele só faz sentido para serviços com estado caro de criar ou realmente global.
Descarte de objetos em AddTransient, AddScoped e AddSingleton
O tempo de vida também define quando o container chama o Dispose dos objetos. Em AddTransient, AddScoped e AddSingleton, o container descarta automaticamente os serviços que implementam IDisposable e que ele mesmo criou.
Para serviços transientes e scoped, o descarte acontece ao fim do escopo, ou seja, ao fim da requisição. Já os singletons criados pelo container só são descartados quando a aplicação encerra.
Existe, porém, uma exceção importante. Quando você registra uma instância pronta com AddSingleton(new Service()), quem criou o objeto foi você, e por isso o container não o descarta automaticamente.
As diretrizes oficiais também fazem um alerta sobre serviços transientes descartáveis. Se você resolvê-los a partir do container raiz, fora de qualquer escopo, o container os guarda até o fim da aplicação, o que pode virar um vazamento de memória.
Registro condicional com TryAdd
Além dos métodos tradicionais, o namespace Microsoft.Extensions.DependencyInjection.Extensions oferece as versões TryAddTransient, TryAddScoped e TryAddSingleton. A diferença é que elas só registram o serviço se ainda não existir nenhum registro para aquele tipo.
using Microsoft.Extensions.DependencyInjection.Extensions;
// registra apenas se ninguém registrou ICustomerRepository antes
builder.Services.TryAddScoped<ICustomerRepository, CustomerRepository>();
Esse recurso é muito usado por bibliotecas, porque permite oferecer uma implementação padrão sem sobrescrever a escolha da aplicação. O curso de injeção de dependência tem uma aula específica sobre TryAddTransient.
Diferenças entre AddTransient, AddScoped e AddSingleton
A tabela abaixo resume as diferenças entre AddTransient, AddScoped e AddSingleton em uma aplicação web.
| Método | Quando cria a instância | Quando descarta | Uso típico |
|---|---|---|---|
| AddTransient | A cada resolução | Fim da requisição | Serviços leves e sem estado |
| AddScoped | Uma vez por requisição | Fim da requisição | DbContext, unidade de trabalho |
| AddSingleton | Uma vez na aplicação | Quando a aplicação encerra | Configurações, caches, clientes compartilhados |
Em outras palavras, a diferença entre AddTransient, AddScoped e AddSingleton está em quantas vezes o container reaproveita o mesmo objeto. Quanto maior o reaproveitamento, menor o custo de criação, mas maior o risco de compartilhar estado indevido.
Testando AddTransient, AddScoped e AddSingleton na prática
A documentação de injeção de dependência do ASP.NET Core propõe um experimento simples para enxergar os três comportamentos. A ideia é registrar a mesma classe com AddTransient, AddScoped e AddSingleton e comparar um identificador gerado no construtor.
public interface IOperation
{
string OperationId { get; }
}
public interface IOperationTransient : IOperation { }
public interface IOperationScoped : IOperation { }
public interface IOperationSingleton : IOperation { }
public class Operation : IOperationTransient, IOperationScoped, IOperationSingleton
{
// cada instância gera um identificador curto e único
public string OperationId { get; } = Guid.NewGuid().ToString()[^4..];
}
Em seguida, registramos cada interface com um tempo de vida diferente:
builder.Services.AddTransient<IOperationTransient, Operation>();
builder.Services.AddScoped<IOperationScoped, Operation>();
builder.Services.AddSingleton<IOperationSingleton, Operation>();
Ao injetar as três interfaces em um controlador e em um middleware, o resultado fica claro. O identificador transient muda entre as duas classes, o scoped se repete na mesma requisição e o singleton nunca muda.
Esse experimento é ótimo para fixar o conceito. Além disso, ele ajuda a explicar o comportamento para quem está chegando agora no time.
Erros comuns com AddTransient, AddScoped e AddSingleton
O erro mais comum ao combinar AddTransient, AddScoped e AddSingleton é injetar um serviço scoped dentro de um singleton. Como o singleton vive para sempre, ele prende o serviço scoped e o transforma, na prática, em outro singleton.
Esse problema tem até nome: dependência cativa. Imagine um DbContext preso dentro de um singleton e sendo usado por várias requisições ao mesmo tempo, o que costuma gerar erros de concorrência.
Validação de escopo
Felizmente, o ASP.NET ajuda a detectar esse erro. Em ambiente de desenvolvimento, o provedor padrão verifica se serviços scoped não são resolvidos a partir do provedor raiz nem injetados em singletons.
Quando encontra o problema, ele lança uma InvalidOperationException com uma mensagem parecida com "Cannot consume scoped service from singleton". Por isso, não ignore esse erro durante o desenvolvimento.
Usando serviços scoped dentro de singletons
Às vezes, um singleton realmente precisa de um serviço scoped, como em um BackgroundService. Nesse caso, a documentação recomenda injetar IServiceScopeFactory e criar um escopo explícito.
public class CleanupWorker(IServiceScopeFactory scopeFactory) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// cria um escopo para resolver serviços scoped com segurança
using var scope = scopeFactory.CreateScope();
var database = scope.ServiceProvider.GetRequiredService<IDatabase>();
// usa o database dentro do escopo
await Task.CompletedTask;
}
}
Assim, o serviço scoped vive apenas dentro do escopo criado e é descartado ao final. Dessa forma, você respeita o tempo de vida de cada dependência.
Como escolher entre AddTransient, AddScoped e AddSingleton
Para escolher entre AddTransient, AddScoped e AddSingleton, comece pela pergunta mais simples: o objeto guarda estado? Se não guarda, AddTransient costuma ser a escolha mais segura.
Se o objeto precisa ser compartilhado por várias classes durante uma mesma requisição, use AddScoped. Esse é o caso de conexões com banco, unidades de trabalho e informações do usuário autenticado.
Por fim, use AddSingleton apenas quando o objeto for caro de criar e puder ser compartilhado com segurança entre todos os usuários. Configurações carregadas no início e caches globais entram nessa categoria.
Na dúvida, lembre que a regra das dependências vale sempre. Um serviço nunca deve depender de outro com tempo de vida menor que o seu.
Além disso, revise os registros sempre que uma classe ganhar um novo campo de estado. Muitas vezes, um serviço que nasceu sem estado passa a guardar dados, e o tempo de vida escolhido no início deixa de ser o adequado.
Conclusão
Entender AddTransient, AddScoped e AddSingleton é essencial para usar a injeção de dependência do ASP.NET com segurança. Cada método define quando o container cria e descarta o objeto, e essa escolha afeta desempenho, consumo de memória e até a segurança dos dados.
Em resumo, AddTransient entrega sempre uma nova instância, AddScoped compartilha a instância durante a requisição e AddSingleton mantém uma única instância para toda a aplicação. Portanto, escolha o tempo de vida com base no estado do objeto e evite que serviços de vida longa capturem serviços de vida curta.
Para continuar estudando, veja também o artigo ASP.NET Core Dependency Injection e o vídeo sobre DI.
FAQ
Qual a diferença entre AddTransient e AddScoped?
O AddTransient cria uma nova instância a cada resolução. Já o AddScoped cria uma instância por requisição e a reaproveita durante toda ela.
Quando devo usar AddSingleton?
Use AddSingleton para objetos caros de criar e seguros para compartilhar entre todos os usuários. Configurações globais e caches são bons exemplos.
Posso injetar um serviço scoped em um singleton?
Não diretamente, porque o serviço scoped passaria a viver como singleton. Nesse caso, injete IServiceScopeFactory e crie um escopo explícito.
Qual tempo de vida o DbContext usa?
O AddDbContext registra o DbContext como scoped por padrão. Assim, todos os repositórios da requisição compartilham o mesmo contexto.
Posso registrar uma instância pronta com AddScoped?
Não, o registro com instância pronta existe apenas no AddSingleton. Para AddScoped e AddTransient, use uma factory.