Partilhar via


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.

Serviços de design para injeção de dependência

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

  • Evite classes e membros estáticos e 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.
  • Torne os serviços pequenos, bem fatorados e facilmente testados.

Se uma classe tiver muitas dependências injetadas, isso pode ser um sinal de que a classe tem muitas responsabilidades e viola o Princípio de Responsabilidade Única (SRP). Tente refatorar a classe transferindo algumas de suas responsabilidades para novas classes.

Eliminação de serviços

O contêiner é responsável pela limpeza dos tipos que cria e chama DisposeIDisposable instâncias. Os serviços resolvidos a partir do contêiner nunca devem ser descartados pelo desenvolvedor. Se um tipo ou fábrica estiver registado como singleton, o contentor elimina 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 precedente destina-se a ter uma vida útil transitória.

namespace ConsoleDisposable.Example;

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

O descartável anterior destina-se a ter uma vida útil definida.

namespace ConsoleDisposable.Example;

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

O descartável anterior destina-se a ter uma vida útil única.

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 ExampleService instância não é criada pelo contêiner de serviço.
  • O quadro não elimina os serviços automaticamente.
  • O desenvolvedor é responsável por descartar os serviços.

Orientação IDisposable para instâncias transitórias e compartilhadas

Vida útil limitada e transitória

Cenário

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

  • A instância é resolvida no escopo raiz (contêiner raiz).
  • A instância deve ser eliminada antes do fim do escopo.

Solução

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

Instância compartilhada, tempo de vida limitado

Cenário

O aplicativo requer uma instância compartilhada IDisposable entre vários serviços, mas a IDisposable instância 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 novo IServiceScopearquivo . Use o escopo para obter os IServiceProvider serviços necessários. Descarte o escopo quando ele não for mais necessário.

Orientações gerais IDisposable

  • Não registre IDisposable instâncias com um tempo de vida transitório. Em vez disso, use o padrão de fábrica.
  • Não resolva IDisposable instâncias com um tempo de vida transitório ou com escopo no escopo raiz. A única exceção é se o aplicativo criar/recriar e descartar IServiceProvider, mas esse não é um padrão ideal.
  • Receber uma IDisposable dependência via DI não requer que o recetor implemente IDisposable a si mesmo. O recetor da IDisposable dependência não deve invocar Dispose essa 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 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 por contêiner em relação à limpeza de recursos.

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

O contêiner de serviço interno foi projetado para atender às necessidades da estrutura e da maioria dos aplicativos de consumo. Recomendamos o uso do contêiner interno, a menos que você precise de um recurso específico que ele não suporta, como:

  • Injeção de propriedade
  • Injeção baseada no nome (somente .NET 7 e versões anteriores. Para obter mais informações, consulte Serviços com chave.)
  • Contentores para crianças
  • Gestão personalizada do tempo de vida
  • Func<T> Suporte para inicialização lenta
  • Registo baseado em convenções

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

Segurança de roscas

Crie serviços singleton seguros para threads. Se um serviço singleton tiver uma dependência de um serviço transitório, o serviço transitório também pode exigir segurança de thread, dependendo de como ele é usado pelo singleton.

O método de fábrica de um serviço singleton, como o segundo argumento para AddSingleton<TService>(IServiceCollection, Func<IServiceProvider,TService>), não precisa ser thread-safe. Como um construtor type (static), é garantido que ele seja chamado apenas uma vez por um único thread.

Recomendações

  • async/await e Task a resolução de serviço baseada não é suportada. Como o C# não oferece suporte a construtores assíncronos, use métodos assíncronos depois de resolver o serviço de forma síncrona.
  • Evite armazenar dados e configuração diretamente no contêiner de serviço. Por exemplo, o carrinho de compras de um usuário normalmente não deve ser adicionado ao contêiner de serviço. A configuração deve usar o padrão de opções. Da mesma forma, evite objetos de "titular de dados" que só existem para permitir o acesso a outro objeto. É melhor solicitar o item real via DI.
  • Evite o acesso estático aos serviços. Por exemplo, evite capturar IApplicationBuilder.ApplicationServices como um campo estático ou propriedade para uso em outro lugar.
  • Mantenha as fábricas de DI rápidas e síncronas.
  • Evite usar o padrão do localizador de serviços. Por exemplo, não invoque GetService para obter uma instância de serviço quando puder usar DI.
  • Outra variação do localizador de serviço a ser evitada é injetar uma fábrica que resolve dependências em tempo de execução. Ambas as práticas misturam estratégias de Inversão de Controle .
  • Evite chamadas para ao BuildServiceProvider configurar serviços. A chamada BuildServiceProvider normalmente acontece quando o desenvolvedor deseja 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 recipiente para eliminação. Isso pode se transformar em um vazamento de memória se resolvido a partir 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, consulte Validação de escopo.

Como todos os conjuntos de recomendações, você pode encontrar situações em que ignorar uma recomendação é necessário. As exceções são raras, na sua maioria casos especiais dentro do próprio quadro.

DI é uma alternativa aos padrões de acesso a objetos estáticos/globais. Talvez você não consiga perceber os benefícios da DI se misturá-la com o acesso a objetos estáticos.

Exemplo de anti-padrões

Além das diretrizes neste artigo, existem vários anti-padrões que você deve evitar. Alguns desses anti-padrões são aprendizados com o desenvolvimento dos próprios tempos de execução.

Aviso

Estes são exemplos de anti-padrõ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 IDisposableo , por padrão, o contêiner DI manterá essas referências, e não Dispose() delas, até que o contêiner seja descartado quando o aplicativo parar, se elas tiverem sido resolvidas a partir do contêiner, ou até que o escopo seja descartado, se elas tiverem sido resolvidas a partir de um escopo. Isso pode se transformar em um vazamento de memória se resolvido a partir do nível do contêiner.

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

No antipadrão anterior, 1.000 ExampleDisposable objetos são instanciados e enraizados. Eles não serão eliminados até que a serviceProvider instância seja descartada.

Para obter mais informações sobre como depurar vazamentos de memória, consulte Depurar um vazamento de memória no .NET.

Fábricas de DI assíncronas podem causar impasses

O termo "fábricas de DI" refere-se aos métodos de sobrecarga que existem ao chamar Add{LIFETIME}. Há sobrecargas aceitando um Func<IServiceProvider, T> onde T está o serviço que está sendo registrado, e o parâmetro é nomeado implementationFactory. O implementationFactory pode ser fornecido como uma expressão lambda, função local ou método. Se a fábrica for assíncrona e você usar Task<TResult>.Resulto , isso causará um impasse.

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

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

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

Para obter mais informações sobre orientação assíncrona, consulte Programação assíncrona: informações e conselhos importantes. Para obter mais informações sobre como depurar deadlocks, consulte Depurar um deadlock no .NET.

Quando você estiver executando esse antipadrão e o deadlock ocorrer, você poderá exibir os dois threads aguardando na janela Parallel Stacks do Visual Studio. Para obter mais informações, consulte Exibir threads e tarefas na janela Pilhas paralelas.

Dependência cativa

O termo "dependência cativa" foi cunhado por Mark Seemann, e refere-se à má configuração da vida útil do serviço, onde um serviço de vida mais longa mantém um serviço de vida mais curta cativo.

Anti-pattern: Captive dependency. Do not copy!

No código anterior, Foo é registrado como um singleton e Bar tem escopo - o que na superfície parece válido. No entanto, considere a implementação do Foo.

namespace DependencyInjection.AntiPatterns;

public class Foo(Bar bar)
{
}

O Foo objeto requer um Bar objeto, e uma vez que Foo é um singleton, e Bar tem escopo - isso é uma configuração incorreta. Como está, Foo seria instanciado apenas uma vez, e se manteria Bar por sua vida, que é mais longa do que a vida útil pretendida de Bar. Você deve considerar a validação de escopos, passando validateScopes: true para o BuildServiceProvider(IServiceCollection, Boolean). Ao validar os escopos, você receberá uma InvalidOperationException mensagem semelhante a "Não é possível consumir o serviço com escopo 'Bar' do singleton 'Foo'.".

Para obter mais informações, consulte 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.

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 é nomeada avoid para mostrar qual exemplo de recuperação está incorreto.

Consulte também