Práticas recomendadas, dicas e truques para injeção de dependência de núcleo do ASP.NET

Neste artigo, compartilharei minhas experiências e sugestões sobre o uso de Injeção de Dependência nos aplicativos ASP.NET Core. A motivação por trás desses princípios é;

  • Conceber efetivamente serviços e suas dependências.
  • Impedindo problemas de multiencadeamento.
  • Evitando vazamentos de memória.
  • Prevenção de possíveis erros.

Este artigo pressupõe que você já esteja familiarizado com a Injeção de Dependências e o ASP.NET Core em um nível básico. Caso contrário, leia a documentação da injeção de dependência de núcleo do ASP.NET primeiro.

Fundamentos

Injeção de Construtor

A injeção de construtor é usada para declarar e obter dependências de um serviço na construção do serviço. Exemplo:

classe pública ProductService
{
    private readonly IProductRepository _productRepository;
    public ProductService (IProductRepository productRepository)
    {
        _productRepository = productRepository;
    }
    Public void Delete (ID int)
    {
        _productRepository.Delete (id);
    }
}

O ProductService está injetando IProductRepository como uma dependência em seu construtor e depois o usa dentro do método Delete.

Boas práticas:

  • Defina explicitamente as dependências necessárias no construtor de serviço. Assim, o serviço não pode ser construído sem suas dependências.
  • Atribua dependência injetada a um campo / propriedade somente leitura (para evitar atribuir acidentalmente outro valor a ele dentro de um método).

Injeção de Propriedade

O contêiner de injeção de dependência padrão do ASP.NET Core não suporta injeção de propriedade. Mas você pode usar outro contêiner que suporte a injeção de propriedade. Exemplo:

using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
namespace MyApp
{
    classe pública ProductService
    {
        Public ILogger  Logger {get; conjunto; }
        private readonly IProductRepository _productRepository;
        public ProductService (IProductRepository productRepository)
        {
            _productRepository = productRepository;
            Registrador = NullLogger  .Instance;
        }
        Public void Delete (ID int)
        {
            _productRepository.Delete (id);
            Logger.LogInformation (
                $ "Excluiu um produto com id = {id}");
        }
    }
}

ProductService está declarando uma propriedade Logger com setter público. O contêiner de injeção de dependência pode definir o Agente de Log se estiver disponível (registrado no contêiner de DI anteriormente).

Boas práticas:

  • Use injeção de propriedade apenas para dependências opcionais. Isso significa que seu serviço pode funcionar corretamente sem essas dependências fornecidas.
  • Use Null Object Pattern (como neste exemplo), se possível. Caso contrário, sempre verifique nulo ao usar a dependência.

Localizador de Serviço

O padrão do localizador de serviço é outra maneira de obter dependências. Exemplo:

classe pública ProductService
{
    private readonly IProductRepository _productRepository;
    privado somente leitura ILogger  _logger;
    public ProductService (IServiceProvider serviceProvider)
    {
        _productRepository = serviceProvider
          .GetRequiredService  ();
        _logger = serviceProvider
          .GetService > () ??
            NullLogger  .Instance;
    }
    Public void Delete (ID int)
    {
        _productRepository.Delete (id);
        _logger.LogInformation ($ "Excluiu um produto com id = {id}");
    }
}

O ProductService está injetando o IServiceProvider e resolvendo dependências usando-o. GetRequiredService lança uma exceção se a dependência solicitada não tiver sido registrada antes. Por outro lado, GetService apenas retorna nulo nesse caso.

Quando você resolve serviços dentro do construtor, eles são liberados quando o serviço é lançado. Portanto, você não se importa com a liberação / descarte de serviços resolvidos dentro do construtor (assim como a injeção de construtor e propriedade).

Boas práticas:

  • Não use o padrão do localizador de serviço sempre que possível (se o tipo de serviço for conhecido no tempo de desenvolvimento). Porque isso torna as dependências implícitas. Isso significa que não é possível ver as dependências facilmente ao criar uma instância do serviço. Isso é especialmente importante para testes de unidade em que você pode zombar de algumas dependências de um serviço.
  • Resolva dependências no construtor de serviço, se possível. A resolução em um método de serviço torna seu aplicativo mais complicado e propenso a erros. Vou abordar os problemas e soluções nas próximas seções.

Tempos de vida útil

Há três vidas úteis de serviço na injeção de dependência de núcleo do ASP.NET:

  1. Serviços transitórios são criados toda vez que são injetados ou solicitados.
  2. Os serviços com escopo são criados por escopo. Em um aplicativo Web, toda solicitação da Web cria um novo escopo de serviço separado. Isso significa que os serviços com escopo são geralmente criados por solicitação da web.
  3. Os serviços Singleton são criados por contêiner de DI. Isso geralmente significa que eles são criados apenas uma vez por aplicativo e, em seguida, utilizados por toda a vida útil do aplicativo.

O contêiner de DI controla todos os serviços resolvidos. Os serviços são liberados e descartados quando a vida útil termina:

  • Se o serviço tiver dependências, elas também serão liberadas e descartadas automaticamente.
  • Se o serviço implementar a interface IDisposable, o método Dispose será chamado automaticamente na liberação do serviço.

Boas práticas:

  • Registre seus serviços como transitórios sempre que possível. Porque é simples projetar serviços transitórios. Geralmente, você não se importa com vazamentos de várias threads e de memória e sabe que o serviço tem uma vida útil curta.
  • Use a vida útil do serviço com escopo com cuidado, pois pode ser complicado se você criar escopos de serviço filho ou usar esses serviços a partir de um aplicativo que não seja da Web.
  • Use a vida útil do singleton com cuidado, desde então, você precisará lidar com vários problemas de vazamento de memória e com vários threads.
  • Não dependa de um serviço transitório ou com escopo definido de um serviço singleton. Como o serviço transitório se torna uma instância singleton quando um serviço singleton o injeta e isso pode causar problemas se o serviço transitório não for projetado para suportar esse cenário. O contêiner de DI padrão do ASP.NET Core já lança exceções nesses casos.

Resolvendo serviços em um corpo de método

Em alguns casos, pode ser necessário resolver outro serviço em um método do seu serviço. Nesses casos, verifique se você libera o serviço após o uso. A melhor maneira de garantir isso é criar um escopo de serviço. Exemplo:

classe pública PriceCalculator
{
    private readonly IServiceProvider _serviceProvider;
    public PriceCalculator (IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }
    flutuação pública Calcular (produto do produto, contagem int,
      Digite taxStrategyServiceType)
    {
        using (var scope = _serviceProvider.CreateScope ())
        {
            var taxStrategy = (ITaxStrategy) scope.ServiceProvider
              .GetRequiredService (taxStrategyServiceType);
            var price = product.Price * count;
            preço de retorno + taxStrategy.CalculateTax (price);
        }
    }
}

PriceCalculator injeta o IServiceProvider em seu construtor e o atribui a um campo. O PriceCalculator usa-o dentro do método Calculate para criar um escopo de serviço filho. Ele usa scope.ServiceProvider para resolver serviços, em vez da instância _serviceProvider injetada. Assim, todos os serviços resolvidos a partir do escopo são automaticamente liberados / descartados no final da instrução using.

Boas práticas:

  • Se você estiver resolvendo um serviço em um corpo de método, sempre crie um escopo de serviço filho para garantir que os serviços resolvidos sejam liberados corretamente.
  • Se um método obtiver IServiceProvider como argumento, você poderá resolvê-lo diretamente, sem se preocupar com a liberação / descarte. Criar / gerenciar o escopo do serviço é uma responsabilidade do código que chama seu método. Seguir esse princípio torna seu código mais limpo.
  • Não mantenha uma referência a um serviço resolvido! Caso contrário, isso poderá causar vazamento de memória e você acessará um serviço descartado quando usar a referência do objeto posteriormente (a menos que o serviço resolvido seja único).

Serviços Singleton

Os serviços Singleton geralmente são projetados para manter um estado do aplicativo. Um cache é um bom exemplo de estados de aplicativo. Exemplo:

classe pública FileService
{
    private readonly ConcurrentDictionary  _cache;
    public FileService ()
    {
        _cache = new ConcurrentDictionary  ();
    }
    byte público [] GetFileContent (string filePath)
    {
        retornar _cache.GetOrAdd (filePath, _ =>
        {
            retornar File.ReadAllBytes (filePath);
        });
    }
}

O FileService simplesmente armazena em cache o conteúdo do arquivo para reduzir as leituras do disco. Este serviço deve ser registrado como singleton. Caso contrário, o armazenamento em cache não funcionará conforme o esperado.

Boas práticas:

  • Se o serviço retiver um estado, ele deverá acessar esse estado de maneira segura para threads. Porque todos os pedidos usam simultaneamente a mesma instância do serviço. Eu usei ConcurrentDictionary em vez de Dictionary para garantir a segurança do thread.
  • Não use serviços com escopo ou transitórios de serviços singleton. Porque serviços transitórios podem não ser projetados para serem seguros para threads. Se você precisar usá-los, cuide da multithreading enquanto estiver usando esses serviços (use a trava, por exemplo).
  • Vazamentos de memória geralmente são causados ​​por serviços singleton. Eles não são liberados / descartados até o final do aplicativo. Portanto, se eles instanciarem classes (ou injetarem), mas não as liberarem / descartarem, eles também permanecerão na memória até o final do aplicativo. Certifique-se de liberá-los / descartá-los no momento certo. Consulte a seção Resolução de serviços em um corpo de método acima.
  • Se você armazenar em cache dados (conteúdo do arquivo neste exemplo), deverá criar um mecanismo para atualizar / invalidar os dados em cache quando a fonte de dados original for alterada (quando um arquivo em cache for alterado no disco para este exemplo).

Serviços com escopo

A vida útil com escopo primeiro parece um bom candidato para armazenar por dados de solicitação da web. Porque o ASP.NET Core cria um escopo de serviço por solicitação da web. Portanto, se você registrar um serviço com escopo definido, ele poderá ser compartilhado durante uma solicitação da web. Exemplo:

classe pública RequestItemsService
{
    dicionário readonly privado  _items;
    public RequestItemsService ()
    {
        _items = new Dictionary  ();
    }
    Public void Set (nome da string, valor do objeto)
    {
        _items [nome] = valor;
    }
    objeto público Get (nome da string)
    {
        retornar _itens [nome];
    }
}

Se você registrar o RequestItemsService como definido no escopo e injetá-lo em dois serviços diferentes, poderá obter um item adicionado de outro serviço porque eles compartilharão a mesma instância RequestItemsService. É isso que esperamos dos serviços com escopo.

Mas .. o fato pode não ser sempre assim. Se você criar um escopo de serviço filho e resolver o RequestItemsService a partir do escopo filho, receberá uma nova instância do RequestItemsService e ele não funcionará conforme o esperado. Portanto, o serviço com escopo nem sempre significa instância por solicitação da web.

Você pode pensar que não cometeu um erro tão óbvio (resolver um escopo dentro de um escopo filho). Mas, isso não é um erro (um uso muito regular) e o caso pode não ser tão simples. Se houver um grande gráfico de dependência entre seus serviços, não será possível saber se alguém criou um escopo filho e resolveu um serviço que injeta outro serviço ... que finalmente injeta um serviço com escopo.

Boa prática:

  • Um serviço com escopo definido pode ser pensado como uma otimização, onde é injetado por muitos serviços em uma solicitação da web. Assim, todos esses serviços usarão uma única instância do serviço durante a mesma solicitação da web.
  • Os serviços com escopo não precisam ser projetados como seguros para threads. Porque, eles devem ser normalmente usados ​​por uma única solicitação / thread da web. Mas ... nesse caso, você não deve compartilhar escopos de serviço entre diferentes segmentos!
  • Tenha cuidado se você projetar um serviço com escopo definido para compartilhar dados entre outros serviços em uma solicitação da Web (explicada acima). Você pode armazenar dados por solicitação da web dentro do HttpContext (injetar IHttpContextAccessor para acessá-lo), que é a maneira mais segura de fazer isso. A vida útil do HttpContext não tem escopo. Na verdade, ele não está registrado no DI (é por isso que você não o injeta, mas injeta o IHttpContextAccessor). A implementação HttpContextAccessor usa AsyncLocal para compartilhar o mesmo HttpContext durante uma solicitação da Web.

Conclusão

A injeção de dependência parece simples de usar no início, mas existem possíveis problemas de multithreading e vazamento de memória se você não seguir alguns princípios rígidos. Compartilhei alguns bons princípios com base em minhas próprias experiências durante o desenvolvimento da estrutura do ASP.NET Boilerplate.