Partilhar via


Diretrizes de injeção de dependência

Este artigo fornece diretrizes gerais e boas práticas para implementar a injeção de dependências (DI) em aplicações .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 com estado. Evite criar estado global, projetando as aplicações para utilizar 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 o Dispose em instâncias IDisposable. 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 container 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 anterior destina-se a ter uma vida útil temporá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 contentor de serviço.
  • A estrutura não descarta os serviços automaticamente.
  • O desenvolvedor é responsável por gerir 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 de o escopo terminar.

Solução

Utilize o padrão de fábrica para criar uma instância fora do escopo pai. Nessa situação, o aplicativo geralmente tem 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

Registe a instância com um ciclo de vida delimitado. Use IServiceScopeFactory.CreateScope para criar um novo IServiceScope. Use o IServiceProvider do escopo para obter os serviços necessários. Descarte o escopo quando já não for necessário.

Orientações gerais IDisposable

  • Não registre IDisposable instâncias 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 já não estiver em uso.
  • Não resolva IDisposable instâncias com um tempo de vida transitório ou com escopo no escopo raiz. A única exceção a isto é se a aplicação criar, recriar e descartar IServiceProvider, mas este 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 ciclo 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 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 de serviços transitórios descartáveis capturados por um contentor em relação à limpeza de recursos.

Substituição de contentor 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
  • 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 necessitar de segurança de threads, dependendo de como é usado pelo singleton. O método de fábrica de um serviço singleton, como o segundo argumento de AddSingleton<TService>(IServiceCollection, Func<IServiceProvider,TService>), não necessita ser seguro para threads. Como um construtor type (static), é garantido que ele seja chamado apenas uma vez por um único thread.

Além disso, o processo de resolução de serviços a partir do contentor de injeção de dependência .NET incorporado é seguro para threads.
Depois de um IServiceProvider ou IServiceScope ter sido construído, é seguro resolver serviços simultaneamente a partir de múltiplos threads.

Observação

A segurança de threads do próprio contentor DI apenas garante que criar e resolver serviços é seguro. Isso não torna as instâncias de serviço resolvidas em si seguras contra threads.
Qualquer serviço (especialmente singletons) que mantenha um estado mutável partilhado deve implementar a sua própria lógica de sincronização quando esse estado for acedido em simultâneo.

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 service locator pattern. 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ços 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 BuildServiceProvider quando 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 contêiner para descarte. Isso pode se transformar em um vazamento de memória se resolvido a partir 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 delimitados. Para obter mais informações, consulte Validação de Escopo.
  • Use apenas o tempo de vida singleton para serviços com seu próprio estado que é caro para criar ou compartilhado globalmente. Evite usar o singleton lifetime para serviços que não têm estado próprio. A maioria dos contêineres .NET IoC usa "Transient" como o escopo padrão. Considerações e inconvenientes dos singletons:
    • Segurança de thread: Um singleton deve ser implementado de forma segura para threads.
    • Acoplamento: 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 gráfico de objeto grande vivo na memória durante o tempo de vida do aplicativo.
    • Tolerância a falhas: Se um singleton ou qualquer parte de sua árvore de dependência falhar, ele não poderá se recuperar facilmente.
    • Recarregamento de configuração: Singletons geralmente não suportam "recarga a quente" de valores de configuração.
    • Fuga de escopo: Um singleton pode inadvertidamente adquirir dependências com escopo definido ou transitórias, promovendo-as efetivamente a singletons e causando efeitos colaterais não intencionais.
    • Sobrecarga de inicialização: ao resolver um serviço, o contêiner IoC precisa procurar a instância singleton. Se ele ainda não existe, ele precisa criá-lo de uma maneira thread-safe. Em contraste, um serviço transitório sem estado é muito barato de criar e de eliminar.

Como em todos os conjuntos de recomendações, pode deparar-se com situações em que é necessário ignorar uma recomendação. Exceções são raras e são maioritariamente casos especiais dentro do próprio enquadramento.

DI é uma alternativa aos padrões de acesso a objetos estáticos/globais. Podes não perceber os benefícios do DI se o misturares com acesso estático a objetos.

Exemplo de anti-padrões

Para além das orientações deste artigo, existem vários anti-padrões que deve evitar. Alguns desses anti-padrões são lições aprendidas a partir do 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 estes padrões e evite esses padrões a todo o custo.

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

Quando regista serviços transitórios que implementam IDisposable, por defeito o contentor DI mantém essas referências. Não os descarta até que o recipiente seja descartado quando a aplicação termina se foram resolvidos a partir do contentor, ou até que o âmbito seja descartado se foram resolvidos a partir de um escopo. Pode ocorrer uma fuga de memória se for resolvida a partir do nível do contentor.

Anti-padrão: Descartáveis transitórios sem descartar. Não copie!

No antipadrão anterior, 1.000 ExampleDisposable objetos 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 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}. Existem sobrecargas que aceitam um Func<IServiceProvider, T> onde T é o serviço a ser registado, e o parâmetro é chamado 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 usares Task<TResult>.Result, isso causará um bloqueio.

Anti-padrão: Deadlock com fábrica assíncrona. Não copie!

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

Anti-padrão: Deadlock com problema interno de fábrica assíncrona. Não copie!

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 estiveres a executar este antipadrão e ocorrer um deadlock, podes visualizar os dois processos que estão à espera na janela Parallel Stacks do Visual Studio. Para obter mais informações, consulte Ver threads e tarefas na janela Pilhas paralelas.

Dependência cativa

O termo "dependência cativa", cunhado por Mark Seemann, refere-se à má configuração dos tempos de vida dos serviços, onde um serviço de maior duração mantém cativo um serviço de menor duração.

Anti-padrão: Dependência cativa. Não copie!

No código anterior, Foo é registado como um singleton e Bar está definido com 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 objeto Foo requer um objeto Bar, e como Foo é um singleton e Bar possui um escopo específico, isto resulta em uma configuração incorreta. Como está, Foo é instanciado apenas uma vez, e mantém Bar por sua vida útil, que é mais longa do que a vida útil pretendida de Bar. Considere validar os âmbitos passando validateScopes: true para o BuildServiceProvider(IServiceCollection, Boolean). Quando você valida os escopos, recebe uma InvalidOperationException mensagem semelhante a "Não é possível consumir o serviço com escopo 'Bar' de singleton 'Foo'.".

Para obter mais informações, consulte Validação de Escopo.

Serviço com escopo registado como singleton

Ao utilizar serviços delimitados, se não criar um âmbito ou não estiver dentro de um existente, o serviço torna-se um singleton.

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

Consulte também