ASP.NET Core Dependency Injection
Dependency injection é a técnica que o ASP.NET Core usa para entregar a cada classe os objetos de que ela precisa, sem que ela mesma tenha de criá-los. Ao separar e dividir responsabilidades, criamos dependências, e em algum momento precisamos resolvê-las.
Neste artigo, você vai entender por que depender de abstrações, como registrar dependências com AddScoped, AddTransient e AddSingleton e como injetar objetos nos Controllers. Os exemplos seguem o modelo com a classe Startup, usado nas versões do ASP.NET Core em que o método ConfigureServices concentrava os registros.
O que é dependency injection
É comum, e até saudável, focar na resolução de um problema e ignorar o que está ao redor dele. Os Controllers das nossas APIs são um bom exemplo disso.
Neles executamos os nossos fluxos, deixando de fora itens como o acesso a dados. Então, podemos dizer que um Controller não deve se preocupar com acesso a dados, porque isso é problema do repositório.
Também podemos afirmar que, para uma determinada action de um Controller funcionar, ela precisa de um repositório. Contudo, como esse repositório vai ser instanciado, ou o que ele faz internamente, não é problema dela.
Em suma, delegamos as responsabilidades. É como o seu primeiro dia na empresa: você precisa de uma máquina para trabalhar, mas pouco importa quem a entrega ou como ela chega até você.
Segundo a documentação oficial da Microsoft sobre injeção de dependência no ASP.NET Core, o dependency injection é um padrão de design para alcançar a Inversão de Controle (IoC) entre classes e suas dependências. Em outras palavras, quem cria os objetos deixa de ser a classe consumidora e passa a ser um container.
Se você quiser revisar os fundamentos antes de seguir, o balta.io tem um artigo introdutório sobre injeção de dependência que cobre IoC e os conceitos básicos.
Dependa sempre da abstração
Em diferentes pontos da aplicação, você vai criar interfaces. Elas funcionam como contratos que dizem apenas o que deve ser feito, e não como.
Assim, as interfaces são abstrações entre o que deve ser feito e a forma de fazer. Esse é o coração do princípio da inversão de dependência, o "D" do SOLID.
Se você assistiu ao curso 1975, Modelando Domínios Ricos, provavelmente percebeu que criamos interfaces para os repositórios. Fizemos isso para abstrair a implementação e, depois, testar o código com versões falsas, como um FakeRepository.
Embora seja possível usar dependency injection diretamente sobre implementações concretas, como no exemplo abaixo, essa escolha cobra um preço:
// A dependência é uma classe concreta, e não uma abstração
public void Test(MyRepository repository)
{
// ...
}
Se o MyRepository do código acima tiver outra dependência, que por sua vez tem outra dependência, e assim por diante, o código vira uma bagunça. Além disso, trocar a implementação passa a exigir alterações em todos os consumidores.
O problema de resolver dependências à mão
Para enxergar o problema, veja este exemplo sem abstrações e sem container:
void Main()
{
var command = new CreateUserCommand("A", "B");
var uow = new UnitOfWork();
var context = new DataContext(uow);
var repository = new UserRepository(context);
var handler = new UserHandler(repository);
var result = handler.Handle(command);
}
public class DataContext
{
public DataContext(UnitOfWork uow) { /* ... */ }
}
public class UserRepository
{
public UserRepository(DataContext context) { /* ... */ }
}
public class UserHandler
{
public UserHandler(UserRepository repository) { /* ... */ }
}
Note que, para chamar o handler do usuário, precisamos primeiro do repositório. Ele, por sua vez, precisa do DataContext, que precisa do UnitOfWork.
Esse encadeamento recebe o nome de árvore de dependências, ou grafo de objetos. Sem abstração e sem um lugar central para montá-lo, você repete todo esse trabalho toda vez que precisar do handler.
Na prática, isso gera três problemas que a documentação da Microsoft também aponta:
- Acoplamento: para trocar uma implementação, você precisa alterar todas as classes que a criam diretamente.
- Configuração espalhada: cada consumidor precisa saber montar as dependências da sua dependência.
- Testes difíceis: como a classe cria o objeto internamente, fica complicado substituí-lo por uma versão falsa.
Resolvendo dependências com o dependency injection do ASP.NET Core
A solução é resolver as dependências em um único lugar, e um ótimo local para isso é a inicialização da aplicação. Dessa forma, cada classe só declara o que precisa, e o container cuida do resto.
O ASP.NET Core traz um container de dependency injection simples e direto, já integrado ao framework. Ou seja, você não precisa instalar nenhum pacote adicional para começar.
Esse container implementa a interface IServiceProvider. Além disso, o próprio framework já registra centenas de serviços nele, como logging, configuração e opções.
Container nativo ou framework externo
Você pode usar outros frameworks de injeção de dependência, como o Ninject, e a documentação prevê a substituição do container padrão. Mesmo assim, vale fazer a pergunta certa antes de trocar.
O container nativo do ASP.NET Core é bom e atende à maior parte dos cenários. Frameworks externos costumam anunciar números impressionantes de desempenho e recursos extras, mas você realmente precisa de tudo isso?
Por isso, comece pelo container nativo. Troque apenas quando surgir uma necessidade concreta que ele não resolve.
Tempos de vida no dependency injection
Ao registrar um serviço, você também escolhe o seu tempo de vida. Ele define quando o container cria uma nova instância e quando reaproveita uma existente.
O ASP.NET Core oferece três tempos de vida, cada um com o seu método de registro. A escolha errada é uma das fontes mais comuns de bugs com dependency injection, então vale entender cada um com calma.
AddScoped
O AddScoped cria uma instância por escopo, e no ASP.NET Core o framework cria um escopo para cada requisição. Assim, dado um tipo ou abstração A, o container resolve com uma instância de B e reaproveita essa mesma instância até o fim da requisição.
Em outras palavras, sempre que uma dependência dessa abstração aparecer durante a mesma requisição, o container entrega o mesmo objeto. Em uma nova requisição, porém, ele cria outra instância.
Esse cenário é muito comum com DataContext, fábricas de conexão e itens ligados a banco de dados, porque não queremos mais de uma conexão por requisição. Tanto que o Entity Framework registra os contextos com tempo de vida scoped por padrão.
Ao término da requisição, o container descarta o escopo e chama o Dispose dos serviços que implementam IDisposable. Depois disso, o garbage collector (GC) se encarrega de liberar a memória.
AddTransient
Diferente do AddScoped, no AddTransient cada resolução de dependência gera um novo objeto. Ou seja, mesmo dentro da mesma requisição, duas classes que pedem o mesmo serviço recebem instâncias diferentes.
Esse tempo de vida é ideal para cenários em que você não quer reaproveitar nada. Por exemplo, serviços leves e sem estado, que não guardam informação entre uma chamada e outra.
AddSingleton
O AddSingleton cria uma única instância para toda a vida da aplicação. Então, todas as requisições e todas as classes recebem exatamente o mesmo objeto.
Ele serve bem para serviços caros de criar e que podem ser compartilhados, como caches em memória ou configurações. No entanto, exige cuidado, porque o mesmo objeto atende várias requisições ao mesmo tempo e precisa ser seguro para uso concorrente.
Também evite injetar um serviço scoped dentro de um singleton. Nesse caso, o serviço scoped passaria a viver tanto quanto a aplicação, o que quebra a ideia de uma instância por requisição.
Para resumir os três tempos de vida:
| Método | Instância | Uso típico |
|---|---|---|
AddTransient |
Nova a cada resolução | Serviços leves e sem estado |
AddScoped |
Uma por requisição | DataContext e repositórios ligados a ele |
AddSingleton |
Uma para toda a aplicação | Caches e configurações compartilhadas |
Em resumo, o tempo de vida é a decisão mais importante ao configurar o dependency injection. Na dúvida, comece com AddScoped para serviços ligados ao banco e AddTransient para serviços sem estado.
Registrando o dependency injection no Startup
Tanto o AddScoped quanto o AddTransient ficam no método ConfigureServices da classe Startup da sua API, como mostra o código abaixo:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
// Uma instância por requisição
services.AddScoped<StoreDataContext, StoreDataContext>();
// Uma nova instância a cada injeção
services.AddTransient<IProductRepository, ProductRepository>();
}
Note que registramos o StoreDataContext com AddScoped, porque ele mantém as conexões com o banco de dados e também as transações. Afinal, não queremos várias conexões ou transações abertas quando ele for usado em mais de um lugar na mesma requisição.
Já o ProductRepository ficou com AddTransient, porque não precisamos manter estado dentro dele. Assim, a cada injeção, o container entrega uma nova instância, que recebe o mesmo StoreDataContext da requisição.
Repare também no registro IProductRepository, ProductRepository. Com ele, os consumidores dependem da interface, e o container sabe qual classe concreta entregar.
Injetando objetos nos Controllers com dependency injection
Com as dependências resolvidas, podemos injetar objetos nos Controllers de duas formas. A primeira é pelo construtor, e a segunda é direto na action, com o atributo FromServices.
Injeção pelo construtor
No exemplo abaixo, a classe tem um campo, normalmente privado e somente leitura, que recebe a instância do nosso objeto:
public class ProductController : Controller
{
private readonly IProductRepository _repository;
// O ASP.NET Core resolve IProductRepository e entrega a instância aqui
public ProductController(IProductRepository repository)
{
_repository = repository;
}
[Route("v1/products")]
[HttpGet]
public IEnumerable<ListProductViewModel> Get()
{
return _repository.Get();
}
}
Primeiro, o Controller declara a dependência no construtor. Em seguida, o ASP.NET Core resolve essa dependência e gera a instância do objeto.
O mais interessante é que o container monta a árvore inteira sozinho. Se o ProductRepository pedir um StoreDataContext no próprio construtor, o ASP.NET Core cria o contexto primeiro e depois entrega o repositório pronto ao Controller.
Compare isso com o exemplo do UserHandler que vimos antes. Lá, cada new precisava aparecer na ordem certa, enquanto aqui basta registrar os serviços uma única vez.
Por fim, o construtor passa a instância para o campo privado, e todas as actions da classe podem usá-la. Essa é a forma mais comum e a que a documentação recomenda, porque deixa explícito tudo o que a classe precisa.
Injeção na action com FromServices
Também existe uma forma mais enxuta de injetar dependências nos Controllers, usando o atributo FromServices:
public class ProductController : Controller
{
[Route("v1/products")]
[HttpGet]
public IEnumerable<ListProductViewModel> Get([FromServices] IProductRepository repository)
{
return repository.Get();
}
}
Ao adicionar o FromServices a um parâmetro de uma action, o ASP.NET Core busca automaticamente o serviço registrado com AddScoped, AddTransient ou AddSingleton e faz a resolução.
O legal dessa forma é que economizamos linhas de código e deixamos os Controllers mais limpos. Além disso, ela é útil quando apenas uma action precisa de um serviço específico.
Por outro lado, se várias actions usam o mesmo serviço, a injeção pelo construtor evita repetição. Na dúvida, prefira o construtor e reserve o FromServices para casos pontuais.
Dependency injection e testes
Um dos maiores ganhos do dependency injection aparece nos testes. Como o Controller depende de IProductRepository, e não de ProductRepository, você pode entregar a ele qualquer implementação da interface.
Foi exatamente isso que fizemos no curso de domínios ricos com o FakeRepository. Veja um exemplo simples:
// Implementação falsa, usada apenas nos testes
public class FakeProductRepository : IProductRepository
{
public IEnumerable<ListProductViewModel> Get()
{
return new List<ListProductViewModel>
{
new ListProductViewModel { Title = "Keyboard" },
new ListProductViewModel { Title = "Mouse" }
};
}
}
// No teste, criamos o Controller manualmente com o repositório falso
var controller = new ProductController(new FakeProductRepository());
var products = controller.Get();
Repare que o teste não precisa de banco de dados, DataContext ou conexão. Assim, ele roda rápido e verifica apenas a lógica do Controller.
Esse cenário só funciona porque o Controller recebe a dependência pelo construtor. Se ele criasse o repositório com new, não haveria como trocar a implementação sem alterar o código de produção.
Boas práticas de dependency injection
Algumas recomendações ajudam a tirar mais proveito do dependency injection no dia a dia. A maioria delas vem das diretrizes oficiais da Microsoft.
- Dependa de interfaces: registre
IProductRepository, ProductRepositoryem vez de depender da classe concreta. - Evite instanciar dependências dentro dos serviços: usar
newdentro de um serviço acopla o código a uma implementação. - Mantenha serviços pequenos: muitas dependências no construtor podem indicar que a classe tem responsabilidades demais.
- Evite o service locator: prefira receber a dependência no construtor a chamar
GetServicemanualmente. - Não descarte serviços do container: o próprio container chama o
Disposedos objetos que ele criou. - Respeite os tempos de vida: um serviço nunca deve depender de outro com tempo de vida menor que o seu.
Seguindo esses pontos, o código fica mais fácil de testar e de evoluir. Afinal, trocar uma implementação passa a ser uma alteração em uma única linha do ConfigureServices.
Conclusão
Criar dependências é cada vez mais comum nas aplicações, mas a conta sempre chega. Felizmente, o dependency injection nativo do ASP.NET Core facilita muito a resolução dessas dependências, sem exigir nenhum framework externo.
Neste artigo, você viu por que depender de abstrações e como o container monta a árvore de dependências por você. Também aprendeu a escolher entre AddScoped, AddTransient e AddSingleton e a injetar serviços pelo construtor ou com FromServices.
Portanto, o próximo passo é revisar os seus Controllers e handlers. Sempre que encontrar um new criando uma dependência, considere registrá-la no container e recebê-la por injeção.
FAQ
Qual a diferença entre AddScoped e AddSingleton?
O AddScoped cria uma instância por requisição. O AddSingleton cria uma única instância compartilhada por toda a aplicação.
Quando usar AddTransient?
Use AddTransient para serviços leves e sem estado. Nesse caso, cada resolução gera um objeto novo.
Preciso de um pacote externo para usar injeção de dependência no ASP.NET Core?
Não. O ASP.NET Core já traz um container nativo, e você só troca por outro framework se tiver uma necessidade específica.
É melhor injetar pelo construtor ou com FromServices?
Na maioria dos casos, o construtor é a melhor opção, porque deixa as dependências explícitas. O FromServices é útil quando apenas uma action precisa do serviço.
Por que o DataContext costuma ser registrado como scoped?
Porque ele mantém conexões e transações com o banco. Com uma instância por requisição, você evita várias conexões abertas ao mesmo tempo.