C# - Dependency Injection via Console .NET Core - II


Neste artigo veremos realizar a injeção de dependência em uma aplicação Console .NET Core usando o contêiner nativo da ASP .NET Core abordando os tempos de vida dos serviços.  

Na primeira parte do artigo vimos como usar o contêiner de injeção de dependência do  .NET Core em uma aplicação Console.

Veremos agora o conceito de tempo de vida dos serviços injetados.

A vida útil da dependência (ou vida útil do serviço; usarei as palavras "dependência" e "serviço" de forma intercambiável neste artigo) define quando uma dependência é instanciada e por quanto tempo ela dura.

O contêiner de injeção de dependência (DI) é responsável por controlar isso. Tradicionalmente, nas versões anteriores do ASP.NET, os desenvolvedores tinham que usar bibliotecas de terceiros, como o Autofac, para fazer isso. No ASP.NET Core, um simples contêiner de DI é incorporado e não requer muita configuração (e pode ser substituído, se for necessário).

O sistema de injeção de dependência criado no ASP.NET Core nos permite definir as regras de reutilização de instâncias de serviços. Atualmente existem três opções disponíveis:

1 - Singleton - Uma única instância da classe de serviço é criada, armazenada na memória e reutilizada para todas as injeções.  Esta opção pode ser usado para serviços 'custosos' de instanciar como um banco de dados.

Observe que a instância é mantida na memória durante toda a vida útil do aplicativo, portanto, observe o uso de memória. O lado positivo desta opção é que a memória será alocada apenas uma vez, portanto o coletor de lixo terá menos trabalho.

2- Scoped - Cria uma instância do serviço por solicitação do cliente ou request. Podemos dizer que esta opção funciona como o Singleton por solicitação. Todos os middlewares, controladores MVC etc. que participam do tratamento de uma única solicitação obterão a mesma instância. Podemos usar essa opção com o contexto do Entity Framework.

Atenção, ao usar um serviço com esta opção definido em um middleware, injete o serviço no método Invoke ou InvokeAsync. Não injete via injeção de construtor, porque força o serviço a se comportar como um singleton.

3- Transient
-
São criados sempre que solicitados no contêiner de serviço, ou seja sempre que o serviço é resolvido a partir de um contêiner de DI, uma nova instância é criada. Isso pode causar alocações e desalocações freqüentes de memória e, portanto, pode ter um impacto negativo no desempenho, se usado com muita frequência. Por isso é recomendado ser usado com serviços leves e sem estado.

A seguir veremos um exemplo prático de funcionamento deste recurso em uma aplicação Console.

Tempos de vidas do serviço em uma aplicação Console

Crie um projeto Console do tipo .NET Core no VS 2019 ou no VS Code

A seguir inclua o seguinte pacote no seu projeto:

  1. pacote Microsoft.Extensions.DependencyInjection;

A título de exemplo vamos criar as classes para definir cada tempo de vida de um serviço:

Crie uma pasta Services no projeto e nesta pasta crie as classes abaixo:

1- SingletonOperacao

   class SingletonOperacao
    {
        public SingletonOperacao()
        {
            Console.WriteLine("Serviço Singleton foi criado");
        }
    }

2- ScopedOperacao

   class ScopedOperacao
    {
        public ScopedOperacao()
        {
            Console.WriteLine("Serviço Scoped foi criado");
        }
    }

3- TransientOperacao

   class TransientOperacao
    {
        public TransientOperacao()
        {
            Console.WriteLine("Serviço Transient foi criado");
        }
    }

Agora vamos usar o contêiner de injeção de dependência nativo para criar ServiceProvider , registrar os serviços e obter os serviços com diferentes tempos de vida.

Para isso inclua o código a seguir no método Main() da classe Program:

    using CShp_ConsoleLifeTimeDI.Services;
    using Microsoft.Extensions.DependencyInjection;
    using System;
    class Program
    {
        static void Main(string[] args)
        {
            IServiceCollection services = new ServiceCollection();
            services.AddTransient<TransientOperacao>();
            services.AddScoped<ScopedOperacao>();
            services.AddSingleton<SingletonOperacao>();
            var serviceProvider = services.BuildServiceProvider();
            Console.WriteLine("-------- Primeiro Request --------\n");
            var transientService = serviceProvider.GetService<TransientOperacao>();
            var scopedService = serviceProvider.GetService<ScopedOperacao>();
            var singletonService = serviceProvider.GetService<SingletonOperacao>();
            Console.WriteLine();
            Console.WriteLine("-------- Segundo Request --------\n");
            var transientService2 = serviceProvider.GetService<TransientOperacao>();
            var scopedService2 = serviceProvider.GetService<ScopedOperacao>();
            var singletonService2 = serviceProvider.GetService<SingletonOperacao>();
            Console.WriteLine();
            Console.WriteLine(new String('-',30));
            Console.ReadLine();
        }
    }
}

Neste código criamos as instâncias dos serviços duas vezes para ver qual serviço seria recriado.

O resultado pode ser visto na figura abaixo:

Como vemos ao criar os serviços pela segunda vez apenas o serviço Transient é recriado, já os serviços com tempo de vida definidos como Singleton e Scoped não são criados novamente.

Vamos agora criar outro exemplo onde vamos definir a primeira e a segunda instância do serviço no mesmo escopo.

Vamos alterar o código conforme abaixo:

        private static void Demo2()
        {
            IServiceCollection services = new ServiceCollection();
            services.AddTransient<TransientOperacao>();
            services.AddScoped<ScopedOperacao>();
            services.AddSingleton<SingletonOperacao>();
            var serviceProvider = services.BuildServiceProvider();
            Console.WriteLine();
            Console.WriteLine("-------- Primeiro Request --------");
            using (var scope = serviceProvider.CreateScope())
            {
                var transientService = scope.ServiceProvider.GetService<TransientOperacao>();
                var scopedService = scope.ServiceProvider.GetService<ScopedOperacao>();
                var singletonService = scope.ServiceProvider.GetService<SingletonOperacao>();
            }
            Console.WriteLine();
            Console.WriteLine("-------- Segundo Request --------");
            using (var scope = serviceProvider.CreateScope())
            {
                var transientService = scope.ServiceProvider.GetService<TransientOperacao>();
                var scopedService = scope.ServiceProvider.GetService<ScopedOperacao>();
                var singletonService = scope.ServiceProvider.GetService<SingletonOperacao>();
            }
            Console.WriteLine();
            Console.WriteLine("-----------------------------");
        }

Estamos usando o método CreateScope que cria um novo IServiceScope que usamos para resolver serviços com escopo.

A camada de DI específica tem uma interface chamada IServiceScopeFactory para permitir criar um escopo para a injeção de dependência. Chamar CreateScope retorna um IServiceScope que implementa IDisposable. Portanto usamos uma declaração 'using' para proteger sua destruição (e encerrar o escopo):

Executando o projeto iremos obter o seguinte resultado:

Como você pode ver, a instância de serviço definido como Scoped é criada mais de uma vez em escopos diferentes.

Na próxima parte do artigo veremos o registro de instância, o registro de tipo genérico e o registro múltiplo.

Pegue o projeto aqui:   CShp_ConsoleLifeTimeDI.zip

Tu, pois, meu filho, fortifica-te na graça que há em Cristo Jesus.
2 Timóteo 2:1

Referências:


José Carlos Macoratti