Diretrizes de injeção de dependência

Este artigo fornece diretrizes gerais e práticas recomendadas para implementar a injeção de dependência em aplicativos .NET.

Projetar serviços para injeção de dependência

Ao criar serviços para injeção de dependência:

  • Evite membros e classes estáticos com estado. Evite criar um estado global projetando aplicativos para usar serviços singleton.
  • Evite a instanciação direta das classes dependentes em serviços. A instanciação direta acopla o código a uma implementação específica.
  • Deixe os serviços pequenos, bem fatorados e fáceis de serem testados.

Se uma classe tiver muitas dependências injetadas, isso poderá ser um sinal de que a classe tem muitas responsabilidades e violará o SRP (princípio de responsabilidade única). Tente refatorar a classe movendo algumas das responsabilidades para uma nova classe.

Descarte de serviços

O contêiner é responsável pela limpeza dos tipos que cria e chama Dispose em instâncias de IDisposable. Os serviços resolvidos do contêiner nunca devem ser descartados pelo desenvolvedor. Se um tipo ou um alocador for registrado como singleton, o contêiner descartará o singleton automaticamente.

No exemplo a seguir, os serviços são criados pelo contêiner de serviço e descartados automaticamente:

namespace ConsoleDisposable.Example;

public sealed class TransientDisposable : IDisposable
{
    public void Dispose() => Console.WriteLine($"{nameof(TransientDisposable)}.Dispose()");
}

O descartável anterior deve ter um tempo de vida transitório.

namespace ConsoleDisposable.Example;

public sealed class ScopedDisposable : IDisposable
{
    public void Dispose() => Console.WriteLine($"{nameof(ScopedDisposable)}.Dispose()");
}

O descartável anterior deve ter um tempo de vida com escopo.

namespace ConsoleDisposable.Example;

public sealed class SingletonDisposable : IDisposable
{
    public void Dispose() => Console.WriteLine($"{nameof(SingletonDisposable)}.Dispose()");
}

O descartável anterior deve ter um tempo de vida singleton.

using ConsoleDisposable.Example;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddTransient<TransientDisposable>();
builder.Services.AddScoped<ScopedDisposable>();
builder.Services.AddSingleton<SingletonDisposable>();

using IHost host = builder.Build();

ExemplifyDisposableScoping(host.Services, "Scope 1");
Console.WriteLine();

ExemplifyDisposableScoping(host.Services, "Scope 2");
Console.WriteLine();

await host.RunAsync();

static void ExemplifyDisposableScoping(IServiceProvider services, string scope)
{
    Console.WriteLine($"{scope}...");

    using IServiceScope serviceScope = services.CreateScope();
    IServiceProvider provider = serviceScope.ServiceProvider;

    _ = provider.GetRequiredService<TransientDisposable>();
    _ = provider.GetRequiredService<ScopedDisposable>();
    _ = provider.GetRequiredService<SingletonDisposable>();
}

O console de depuração mostra a seguinte saída de exemplo após a execução:

Scope 1...
ScopedDisposable.Dispose()
TransientDisposable.Dispose()

Scope 2...
ScopedDisposable.Dispose()
TransientDisposable.Dispose()

info: Microsoft.Hosting.Lifetime[0]
      Application started.Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
     Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
     Content root path: .\configuration\console-di-disposable\bin\Debug\net5.0
info: Microsoft.Hosting.Lifetime[0]
     Application is shutting down...
SingletonDisposable.Dispose()

Serviços não criados pelo contêiner de serviço

Considere o seguinte código:

// Register example service in IServiceCollection
builder.Services.AddSingleton(new ExampleService());

No código anterior:

  • A instância de ExampleServicenão é criada pelo contêiner de serviço.
  • A estrutura não descarta os serviços automaticamente.
  • O desenvolvedor é responsável por descartar os serviços.

Diretrizes de IDisposable para instâncias transitórias e compartilhadas

Tempo de vida transitório e limitado

Cenário

O aplicativo requer uma instância de IDisposable com um tempo de vida transitório para os seguintes cenários:

  • A instância é resolvida no escopo raiz (contêiner raiz).
  • A instância deve ser descartada antes que o escopo termine.

Solução

Use o padrão do alocador para criar uma instância fora do escopo pai. Nessa situação, o aplicativo geralmente tem um método Create que chama diretamente o construtor do tipo final. Se o tipo final tiver outras dependências, o alocador poderá:

Instância compartilhada, tempo de vida limitado

Cenário

O aplicativo requer uma instância compartilhada de IDisposable em vários serviços, mas a instância de IDisposable deve ter um tempo de vida limitado.

Solução

Registre a instância com um tempo de vida com escopo. Use IServiceScopeFactory.CreateScope para criar um IServiceScope. Use o IServiceProvider do escopo para obter os serviços necessários. Descarte o escopo quando ele não for mais necessário.

Diretrizes gerais de IDisposable

  • Não registre instâncias de IDisposable com um tempo de vida transitório. Use o padrão do alocador.
  • Não resolva instâncias de IDisposable com um tempo de vida transitório ou com escopo no escopo raiz. A única exceção é se o aplicativo cria/recria e descarta IServiceProvider, mas esse não é um padrão ideal.
  • O recebimento de uma dependência de IDisposable via DI não exige que o próprio receptor implemente IDisposable. O receptor da dependência IDisposable não deve chamar Dispose nessa dependência.
  • Use escopos para controlar o tempo de vida dos serviços. Os escopos não são hierárquicos e não há nenhuma conexão especial entre os escopos.

Para obter mais informações sobre a limpeza de recursos, confira Implementar um método Dispose ou implementar um método DisposeAsync. Além disso, considere o cenário Serviços transitórios descartáveis capturados pelo contêiner, em relação à limpeza de recursos.

Substituição do contêiner de serviço padrão

O contêiner de serviço interno deve atender às necessidades da estrutura e da maioria dos aplicativos de consumo. É recomendado usar o contêiner interno, a menos que você precise de um recurso específico que não seja compatível com ele, como:

  • Injeção de propriedade
  • Injeção baseada apenas no nome (.NET 7 e versões anteriores. Para obter mais informações, confira Serviços com chave.)
  • Contêineres filho
  • Gerenciamento de tempo de vida personalizado
  • Suporte de Func<T> para inicialização lenta
  • Registro baseado em convenção

Os seguintes contêineres de terceiros podem ser usados com aplicativos ASP.NET Core:

Acesso thread-safe

Crie serviços singleton thread-safe. Se um serviço singleton tiver uma dependência de um serviço transitório, o serviço transitório também precisará ter acesso thread-safe, dependendo de como ele for usado pelo singleton.

O método do alocador de um serviço singleton, como o segundo argumento de AddSingleton<TService>(IServiceCollection, Func<IServiceProvider,TService>) não precisa ser thread-safe. Como um construtor do tipo (static), ele tem garantia de ser chamado uma vez por um só thread.

Recomendações

  • Não há suporte para a resolução de serviço baseada em async/await e Task. Como o C# não dá suporte a construtores assíncronos, use métodos assíncronos depois de resolver o serviço de maneira síncrona.
  • Evite armazenar dados e a configuração diretamente no contêiner do serviço. Por exemplo, o carrinho de compras de um usuário normalmente não deve ser adicionado ao contêiner do serviço. A configuração deve usar o padrão de opções. Da mesma forma, evite objetos de "suporte de dados" que existem somente para permitir o acesso a outro objeto. É melhor solicitar o item real por meio da DI.
  • Evite o acesso estático aos serviços. Por exemplo, evite capturar IApplicationBuilder.ApplicationServices como um campo estático ou uma propriedade para uso em outro lugar.
  • Mantenha os alocadores de DI rápidos e síncronos.
  • Evite usar o padrão do localizador de serviço. Por exemplo, não invoque GetService para obter uma instância de serviço quando for possível usar a DI.
  • Outra variação de localizador de serviço a ser evitada é injetar um alocador que resolva as dependências em tempo de execução. Essas duas práticas misturam estratégias de inversão de controle.
  • Evite chamadas para BuildServiceProvider ao configurar os serviços. A chamada para BuildServiceProvider normalmente ocorre quando o desenvolvedor quer resolver um serviço ao registrar outro serviço. Em vez disso, use uma sobrecarga que inclua o IServiceProvider por esse motivo.
  • Os serviços transitórios descartáveis são capturados pelo contêiner para descarte. Isso pode se transformar em uma perda de memória quando resolvido por meio do contêiner de nível superior.
  • Habilite a validação de escopo para garantir que o aplicativo não tenha singletons que capturem serviços com escopo. Para obter mais informações, confira Validação de escopo.

Como todos os conjuntos de recomendações, talvez você encontre situações em que é necessário ignorar uma recomendação. As exceções são raras, ocorrendo em casos especiais dentro da própria estrutura.

A DI é uma alternativa aos padrões de acesso a objeto estático/global. Talvez você não obtenha os benefícios da DI se combiná-lo com o acesso a objeto estático.

Exemplo de antipadrões

Além das diretrizes deste artigo, há vários antipadrões que você deve evitar. Alguns desses antipadrões foram aprendidos durante o desenvolvimento dos próprios runtimes.

Aviso

Estes são exemplos de anti-padrões, não copie o código, não use esses padrões e evite-os a todo custo.

Serviços transitórios descartáveis capturados pelo contêiner

Quando você registrar serviços transitórios que implementem IDisposable, por padrão, o contêiner de DI manterá essas referências e não executará Dispose() nelas até que o contêiner seja descartado quando o aplicativo for interrompido, se eles forem resolvidos em um contêiner, ou até que o escopo seja descartado, se eles forem resolvidos em um escopo. Isso pode se transformar em uma perda de memória quando resolvido no nível do contêiner.

Anti-pattern: Transient disposables without dispose. Do not copy!

No antipadrão anterior, uma instância foi criada em 1.000 objetos ExampleDisposable e eles ficaram com root. Eles não serão descartados até que a instância de serviceProvider seja descartada.

Para obter mais informações sobre como depurar perdas de memória, confira Depurar uma perda de memória no .NET.

Alocadores de DI assíncronos podem causar deadlocks

O termo "alocadores de DI" se refere aos métodos de sobrecarga que existem ao chamar Add{LIFETIME}. Há sobrecargas que aceitam um Func<IServiceProvider, T>em que T é o serviço está sendo registrado e o parâmetro se chama implementationFactory. O método implementationFactory pode ser fornecido como uma expressão lambda, uma função local ou um método. Se o alocador for assíncrono e você usar Task<TResult>.Result, isso causará um deadlock.

Anti-pattern: Deadlock with async factory. Do not copy!

No código anterior, implementationFactory recebe uma expressão lambda em que o corpo chama Task<TResult>.Result em um método de retorno Task<Bar>. Isso causa um deadlock. O método GetBarAsync simplesmente emula uma operação de trabalho assíncrona com Task.Delay e depois chama GetRequiredService<T>(IServiceProvider).

Anti-pattern: Deadlock with async factory inner issue. Do not copy!

Para obter mais informações sobre diretrizes assíncronas, confira Programação assíncrona: informações e recomendações importantes. Para obter mais informações sobre a depuração de deadlocks, confira Depurar um deadlock no .NET.

Quando você estiver executando esse antipadrão e o deadlock ocorrer, você poderá exibir os dois threads que estão esperando na janela Pilhas Paralelas do Visual Studio. Para obter mais informações, confira Exibir threads e tarefas na janela Pilhas Paralelas.

Dependência cativa

O termo "dependência cativa" foi cunhado por Mark Seemann e se refere à configuração incorreta do tempo de vida de serviço, em que um serviço com vida mais longa mantém cativo um serviço de vida mais curta.

Anti-pattern: Captive dependency. Do not copy!

No código anterior, Foo é registrado como singleton e Bar recebe um escopo, o que parece válido superficialmente. No entanto, considere a implementação de Foo.

namespace DependencyInjection.AntiPatterns;

public class Foo(Bar bar)
{
}

O objeto Foo requer um objeto Bar e, como Foo é um singleton e Bar tem um escopo definido, essa é uma configuração incorreta. Assim, a instância de Foo só seria criada uma vez e ficaria atrelada a Bar durante seu tempo de vida, que é maior do que o tempo de vida com escopo pretendido de Bar. Você deve considerar a validação de escopos passando validateScopes: true para o BuildServiceProvider(IServiceCollection, Boolean). Ao validar os escopos, você obterá uma InvalidOperationException com uma mensagem semelhante a "Não é possível consumir o serviço com escopo 'Bar' do singleton 'Foo'.".

Para obter mais informações, confira Validação de escopo.

Serviço com escopo como singleton

Ao usar serviços com escopo, se você não estiver criando um escopo ou se o serviço não estiver em um escopo existente, o serviço se tornará um singleton.

Anti-pattern: Scoped service becomes singleton. Do not copy!

No código anterior, Bar é recuperado dentro de um IServiceScope, o que está correto. O anti-padrão é a recuperação de Bar fora do escopo e a variável é chamada de avoid para mostrar qual exemplo de recuperação está incorreto.

Confira também