Compartilhar via


Diretrizes de injeção de dependência

Este artigo fornece diretrizes gerais e práticas recomendadas para implementar a DI (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 de classes dependentes dentro dos 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 o construtor do tipo final diretamente. Se o tipo final tiver outras dependências, a fábrica 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 IDisposable

  • Não registre instâncias de IDisposable com um tempo de vida transitório. Use o padrão de fábrica para que o serviço resolvido possa ser descartado manualmente quando ele não estiver mais em uso.
  • Não resolva instâncias de IDisposable com um tempo de vida transitório ou com escopo no escopo raiz. A única exceção a isso é se o aplicativo cria ou recria e descarta IServiceProvider, mas esse não é um padrão ideal.
  • Uma dependência de IDisposable recebida via DI não exige que o receptor implemente IDisposable por si mesmo. 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, consulte Implementar um Dispose método ou implementar um DisposeAsync método. 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
  • Contêineres filho
  • Gerenciamento de tempo de vida personalizado
  • Suporte a 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 poderá exigir segurança de threads, dependendo de como ele é 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.

Além disso, o processo de resolução de serviços do contêiner de injeção de dependência embutido do .NET é thread-safe.
Uma vez que um IServiceProvider ou IServiceScope foi construído, é seguro resolver serviços simultaneamente de várias threads.

Observação

A segurança de thread do próprio contêiner de DI garante apenas que a construção e a resolução de serviços sejam seguras. Ele não torna as próprias instâncias de serviço resolvidas thread-safe.
Qualquer serviço (especialmente singletons) que mantém o estado mutável compartilhado deve implementar sua própria lógica de sincronização quando acessado simultaneamente.

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 resolve as dependências em runtime. 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.
  • Ative a validação de escopo para garantir que o aplicativo não tenha singletons que capturem serviços definidos por escopo. Para obter mais informações, confira Validação de escopo.
  • Utilize apenas a vida útil singleton para serviços com estado interno que seja custoso de criar e/ou compartilhado globalmente. Evite usar o tempo de vida singleton para serviços que não possuem estado próprio. A maioria dos contêineres de IoC do .NET usa "Transitório" como o escopo padrão. Considerações e desvantagens dos singletons:
    • Segurança de Thread: um singleton deve ser implementado de maneira segura para utilização em múltiplas threads.
    • Acoplamento: ele pode acoplar solicitações não relacionadas.
    • Desafios de teste: o estado compartilhado e o acoplamento podem dificultar o teste de unidade.
    • Impacto na memória: um singleton pode manter um grande grafo de objetos vivo na memória durante o tempo de execução do aplicativo.
    • Tolerância a falhas: se um singleton ou qualquer parte de sua árvore de dependência falhar, não poderá se recuperar facilmente.
    • Recarregamento de configuração: singletons geralmente não podem dar suporte a "recarregamento frequente" de valores de configuração.
    • Vazamento de escopo: um singleton pode capturar inadvertidamente dependências configuradas ou transitórias, promovendo-as efetivamente para singletons e causando efeitos colaterais inesperados.
    • Sobrecarga de inicialização: ao resolver um serviço, o contêiner IoC precisa localizar a instância singleton. Se ele ainda não existir, ele precisará criá-lo de maneira thread-safe. Por outro lado, um serviço transitório sem estado é muito barato para criar e destruir.

Como todos os conjuntos de recomendações, você pode encontrar situações em que é necessário ignorar uma recomendação. As exceções são raras e são principalmente 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 perceba os benefícios da DI se a misturar com o acesso a objetos estáticos.

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 antipadrões. Não copie o código, não use esses padrões e evite esses padrões a todo custo.

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

Quando você registra serviços transitórios que implementam IDisposable, por padrão, o contêiner de DI mantém essas referências. Ele não os descarta até que o contêiner seja descartado quando o aplicativo for interrompido se eles forem resolvidos do contêiner ou até que o escopo seja descartado se eles foram resolvidos de um escopo. Se for resolvido a partir do nível do contêiner, pode resultar em um vazamento de memória.

Antipadrão: Descartáveis transitórios sem descarte. Não copie!

No antipadrão anterior, 1.000 objetos ExampleDisposable são instanciados e enraizados. Eles não serão descartados até que a serviceProvider instância 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> onde T é o serviço a ser registrado, e o parâmetro é denominado implementationFactory. O método implementationFactory pode ser fornecido como uma expressão lambda, uma função local ou um método. Se a fábrica for assíncrona e você usar Task<TResult>.Result, ela causará um deadlock.

Antipadrão: deadlock com fábrica assíncrona. Não copie!

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).

Antipadrão: deadlock com problema interno de fábrica assíncrona. Não copie!

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", cunhado por Mark Seemann, refere-se à configuração incorreta da duração de vida dos serviços, em que um serviço de vida mais longa mantém um serviço de vida mais curta em cativeiro.

Antipadrão: dependência cativa. Não copie!

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 Foo objeto requer um Bar objeto e, como Foo é um singleton e Bar tem escopo, essa é uma configuração incorreta. Como está, Foo é instanciado apenas uma vez e mantém Bar durante toda a sua vida útil, que é maior do que o tempo de vida planejado para Bar. Considere validar escopos passando validateScopes: true para o BuildServiceProvider(IServiceCollection, Boolean). Ao validar os escopos, você obtém uma InvalidOperationException 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 dentro de um escopo existente, o serviço se tornará um singleton.

Antipadrão: o serviço com escopo torna-se singleton. Não copie!

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