Injeção de dependência no .NET

O .NET dá suporte ao padrão de design de software de injeção de dependência (DI), que é uma técnica para obter inversão de controle (IOC) entre classes e suas dependências. A injeção de dependência no .NET é um cidadão de primeira classe, juntamente com configuração, registro em log e o padrão de opções.

Uma dependência é um objeto do qual outro objeto depende. Examine a seguinte MessageWriter classe com um Write método de que outras classes dependem:

public class MessageWriter
{
    public void Write(string message)
    {
        Console.WriteLine($"MessageWriter.Write(message: \"{message}\")");
    }
}

Uma classe pode criar uma instância da MessageWriter classe para fazer uso de seu Write método. No exemplo a seguir, a MessageWriter classe é uma dependência da Worker classe:

public class Worker : BackgroundService
{
    private readonly MessageWriter _messageWriter = new MessageWriter();

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            _messageWriter.Write($"Worker running at: {DateTimeOffset.Now}");
            await Task.Delay(1000, stoppingToken);
        }
    }
}

A classe cria e depende diretamente da MessageWriter classe. As dependências embutidas em código, como no exemplo anterior, são problemáticas e devem ser evitadas pelos seguintes motivos:

  • Para substituir MessageWriter por uma implementação diferente, a Worker classe deve ser modificada.
  • Se MessageWriter tiver dependências, elas também deverão ser configuradas pela Worker classe. Em um projeto grande com várias classes dependendo da MessageWriter, o código de configuração fica pulverizado por todo o aplicativo.
  • É difícil testar a unidade dessa implementação. O aplicativo deve usar uma simulação ou stub da classe MessageWriter, o que não é possível com essa abordagem.

Injeção de dependência trata desses problemas da seguinte maneira:

  • O uso de uma interface ou classe base para abstrair a implementação da dependência.
  • Registrando a dependência em um contêiner de serviço. O .NET fornece um contêiner de serviço interno, IServiceProvider . Normalmente, os serviços são registrados na inicialização do aplicativo e anexados a um IServiceCollection . Depois que todos os serviços forem adicionados, você usará BuildServiceProvider para criar o contêiner de serviço.
  • Injeção do serviço no construtor da classe na qual ele é usado. A estrutura assume a responsabilidade de criar uma instância da dependência e de descartá-la quando não for mais necessária.

Por exemplo, a IMessageWriter interface define o Write método:

namespace DependencyInjection.Example
{
    public interface IMessageWriter
    {
        void Write(string message);
    }
}

Essa interface é implementada por um tipo concreto, MessageWriter:

using System;

namespace DependencyInjection.Example
{
    public class MessageWriter : IMessageWriter
    {
        public void Write(string message)
        {
            Console.WriteLine($"MessageWriter.Write(message: \"{message}\")");
        }
    }
}

O código de exemplo registra o IMessageWriter serviço com o tipo concreto MessageWriter . O AddScoped método registra o serviço com um tempo de vida de escopo, o tempo de vida de uma única solicitação. Os tempos de vida do serviço são descritos posteriormente neste artigo.

using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace DependencyInjection.Example
{
    class Program
    {
        static Task Main(string[] args) =>
            CreateHostBuilder(args).Build().RunAsync();

        static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureServices((_, services) =>
                    services.AddHostedService<Worker>()
                            .AddScoped<IMessageWriter, MessageWriter>());
    }
}

No aplicativo de exemplo, o IMessageWriter serviço é solicitado e usado para chamar o Write método:

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;

namespace DependencyInjection.Example
{
    public class Worker : BackgroundService
    {
        private readonly IMessageWriter _messageWriter;

        public Worker(IMessageWriter messageWriter) =>
            _messageWriter = messageWriter;

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                _messageWriter.Write($"Worker running at: {DateTimeOffset.Now}");
                await Task.Delay(1000, stoppingToken);
            }
        }
    }
}

Usando o padrão DI, o serviço de trabalho:

  • Não usa o tipo concreto MessageWriter , apenas a IMessageWriter interface que o implementa. Isso facilita a alteração da implementação que o controlador usa sem modificar o controlador.
  • Não cria uma instância do MessageWriter , ela é criada pelo contêiner de di.

A implementação da IMessageWriter interface pode ser aprimorada usando a API de registro em log interna:

using Microsoft.Extensions.Logging;

namespace DependencyInjection.Example
{
    public class LoggingMessageWriter : IMessageWriter
    {
        private readonly ILogger<LoggingMessageWriter> _logger;

        public LoggingMessageWriter(ILogger<LoggingMessageWriter> logger) =>
            _logger = logger;

        public void Write(string message) =>
            _logger.LogInformation(message);
    }
}

O ConfigureServices método atualizado registra a nova IMessageWriter implementação:

static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureServices((_, services) =>
            services.AddHostedService<Worker>()
                    .AddScoped<IMessageWriter, LoggingMessageWriter>());

LoggingMessageWriter depende de ILogger<TCategoryName> , que ele solicita no construtor. ILogger<TCategoryName> é um serviço fornecido pelo Framework.

Não é incomum usar a injeção de dependência de uma maneira encadeada. Por sua vez, cada dependência solicitada solicita suas próprias dependências. O contêiner resolve as dependências no grafo e retorna o serviço totalmente resolvido. O conjunto de dependências que precisa ser resolvido normalmente é chamado de árvore de dependência, grafo de dependência ou grafo de objeto.

O contêiner resolve aproveitando ILogger<TCategoryName> os tipos abertos (genéricos), eliminando a necessidade de registrar cada tipo construído (genérico).

Na terminologia de injeção de dependência, um serviço:

  • Normalmente é um objeto que fornece um serviço para outros objetos, como o IMessageWriter serviço.
  • O não está relacionado a um serviço Web, embora o serviço possa usar um serviço Web.

A estrutura fornece um sistema de registro em log robusto. As IMessageWriter implementações mostradas nos exemplos anteriores foram escritas para demonstrar a di básica, não para implementar o registro em log. A maioria dos aplicativos não deve precisar gravar agentes. O código a seguir demonstra como usar o log padrão, que requer que apenas o seja Worker registrado no ConfigureServices como um serviço hospedado AddHostedService :

public class Worker : BackgroundService
{
    private readonly ILogger<Worker> _logger;

    public Worker(ILogger<Worker> logger) =>
        _logger = logger;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
            await Task.Delay(1000, stoppingToken);
        }
    }
}

Usando o código anterior, não há necessidade de atualizar ConfigureServices , porque o registro em log é fornecido pela estrutura.

Registrar grupos de serviços com métodos de extensão

O Microsoft Extensions usa uma Convenção para registrar um grupo de serviços relacionados. A Convenção é usar um método de Add{GROUP_NAME} extensão único para registrar todos os serviços exigidos por um recurso de estrutura. Por exemplo, o AddOptions método de extensão registra todos os serviços necessários para usar as opções.

Serviços fornecidos pela estrutura

O ConfigureServices método registra os serviços que o aplicativo usa, incluindo recursos de plataforma. Inicialmente, o IServiceCollection fornecido para o ConfigureServices tem serviços definidos pela estrutura, dependendo de como o host foi configurado. Para aplicativos baseados nos modelos .NET, a estrutura registra centenas de serviços.

A tabela a seguir lista uma pequena amostra desses serviços registrados no Framework:

Tipo de Serviço Tempo de vida
Microsoft.Extensions.DependencyInjection.IServiceScopeFactory Singleton
IHostApplicationLifetime Singleton
Microsoft.Extensions.Logging.ILogger<TCategoryName> Singleton
Microsoft.Extensions.Logging.ILoggerFactory Singleton
Microsoft.Extensions.ObjectPool.ObjectPoolProvider Singleton
Microsoft.Extensions.Options.IConfigureOptions<TOptions> Transitório
Microsoft.Extensions.Options.IOptions<TOptions> Singleton
System.Diagnostics.DiagnosticListener Singleton
System.Diagnostics.DiagnosticSource Singleton

Tempos de vida do serviço

Os serviços podem ser registrados com um dos seguintes tempos de vida:

  • Transitório
  • Com escopo
  • Singleton

As seções a seguir descrevem cada um dos tempos de vida anteriores. Escolha um tempo de vida apropriado para cada serviço registrado.

Transitório

Serviços temporários de tempo de vida são criados cada vez que são solicitados pelo contêiner de serviço. Esse tempo de vida funciona melhor para serviços leves e sem estado. Registre serviços transitórios com AddTransient .

Em aplicativos que processam solicitações, os serviços transitórios são descartados no final da solicitação.

Com escopo

Para aplicativos Web, um tempo de vida com escopo indica que os serviços são criados uma vez por solicitação do cliente (conexão). Registre os serviços com escopo com AddScoped .

Em aplicativos que processam solicitações, os serviços com escopo são descartados no final da solicitação.

Ao usar Entity Framework Core, o AddDbContext método de extensão registra os DbContext tipos com um tempo de vida com escopo definido por padrão.

Observação

Não resolva um serviço com escopo de um singleton e tenha cuidado para não fazê-lo indiretamente, por exemplo, por meio de um serviço transitório. Pode fazer com que o serviço tenha um estado incorreto durante o processamento das solicitações seguintes. Não há problema em:

  • Resolva um serviço singleton de um serviço com escopo ou transitório.
  • Resolva um serviço com escopo de outro serviço com escopo ou transitório.

Por padrão, no ambiente de desenvolvimento, a resolução de um serviço de outro serviço com um tempo de vida maior gera uma exceção. Para obter mais informações, confira Validação de escopo.

Singleton

Os serviços de vida útil singleton são criados:

  • Na primeira vez que forem solicitadas.
  • Pelo desenvolvedor, ao fornecer uma instância de implementação diretamente para o contêiner. Essa abordagem raramente é necessária.

Cada solicitação subsequente da implementação do serviço do contêiner de injeção de dependência usa a mesma instância. Se o aplicativo exigir um comportamento singleton, permita que o contêiner de serviço gerencie o tempo de vida do serviço. Não implemente o padrão de design singleton e forneça código para descartar o singleton. Os serviços nunca devem ser descartados pelo código que resolveu o serviço do contêiner. Se um tipo ou fábrica for registrado como um singleton, o contêiner descartará o singleton automaticamente.

Registre os serviços singleton com AddSingleton . Os serviços singleton devem ser thread-safe e geralmente são usados em serviços sem estado.

Em aplicativos que processam solicitações, os serviços singleton são descartados quando o ServiceProvider é Descartado no desligamento do aplicativo. Como a memória não é liberada até que o aplicativo seja desligado, considere o uso de memória com um serviço singleton.

Métodos de registro do serviço

A estrutura fornece métodos de extensão de registro de serviço que são úteis em cenários específicos:

Método Automática
objeto
descarte
Vários
implementações
Passar argumentos
Add{LIFETIME}<{SERVICE}, {IMPLEMENTATION}>()

Exemplo:

services.AddSingleton<IMyDep, MyDep>();
Sim Sim Não
Add{LIFETIME}<{SERVICE}>(sp => new {IMPLEMENTATION})

Exemplos:

services.AddSingleton<IMyDep>(sp => new MyDep());
services.AddSingleton<IMyDep>(sp => new MyDep(99));
Sim Sim Sim
Add{LIFETIME}<{IMPLEMENTATION}>()

Exemplo:

services.AddSingleton<MyDep>();
Sim Não Não
AddSingleton<{SERVICE}>(new {IMPLEMENTATION})

Exemplos:

services.AddSingleton<IMyDep>(new MyDep());
services.AddSingleton<IMyDep>(new MyDep(99));
Não Sim Sim
AddSingleton(new {IMPLEMENTATION})

Exemplos:

services.AddSingleton(new MyDep());
services.AddSingleton(new MyDep(99));
Não Não Sim

Para obter mais informações sobre o descarte de tipos, consulte a seção Descarte de serviços.

O registro de um serviço com apenas um tipo de implementação é equivalente ao registro desse serviço com a mesma implementação e tipo de serviço. É por isso que várias implementações de um serviço não podem ser registradas usando os métodos que não usam um tipo de serviço explícito. Esses métodos podem registrar várias instâncias de um serviço, mas todos terão o mesmo tipo de implementação .

Qualquer um dos métodos de registro de serviço acima pode ser usado para registrar várias instâncias de serviço do mesmo tipo de serviço. No exemplo a seguir, AddSingleton é chamado duas vezes com IMessageWriter como o tipo de serviço. A segunda chamada para AddSingleton substitui a anterior quando resolvida como IMessageWriter e a adiciona à anterior quando vários serviços são resolvidos por meio de IEnumerable<IMessageWriter> . Os serviços aparecem na ordem em que foram registrados quando resolvidos por meio de IEnumerable<{SERVICE}> .

using System.Threading.Tasks;
using ConsoleDI.IEnumerableExample;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace ConsoleDI.Example
{
    class Program
    {
        static Task Main(string[] args)
        {
            using IHost host = CreateHostBuilder(args).Build();

            _ = host.Services.GetService<ExampleService>();

            return host.RunAsync();
        }

        static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureServices((_, services) =>
                    services.AddSingleton<IMessageWriter, ConsoleMessageWriter>()
                            .AddSingleton<IMessageWriter, LoggingMessageWriter>()
                            .AddSingleton<ExampleService>());
    }
}

O código-fonte de exemplo anterior registra duas implementações do IMessageWriter .

using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

namespace ConsoleDI.IEnumerableExample
{
    public class ExampleService
    {
        public ExampleService(
            IMessageWriter messageWriter,
            IEnumerable<IMessageWriter> messageWriters)
        {
            Trace.Assert(messageWriter is LoggingMessageWriter);

            var dependencyArray = messageWriters.ToArray();
            Trace.Assert(dependencyArray[0] is ConsoleMessageWriter);
            Trace.Assert(dependencyArray[1] is LoggingMessageWriter);
        }
    }
}

O ExampleService define dois parâmetros de Construtor; um único IMessageWriter e um IEnumerable<IMessageWriter> . O único IMessageWriter é a última implementação a ser registrada, enquanto que IEnumerable<IMessageWriter> representa todas as implementações registradas.

A estrutura também fornece TryAdd{LIFETIME} métodos de extensão, que registram o serviço somente se ainda não houver uma implementação registrada.

No exemplo a seguir, a chamada para AddSingleton registra ConsoleMessageWriter como uma implementação para IMessageWriter . A chamada para TryAddSingleton não tem efeito porque IMessageWriter já tem uma implementação registrada:

services.AddSingleton<IMessageWriter, ConsoleMessageWriter>();
services.TryAddSingleton<IMessageWriter, LoggingMessageWriter>();

O TryAddSingleton não tem efeito, pois já foi adicionado e o "try" falhará. O ExampleService declararia o seguinte:

public class ExampleService
{
    public ExampleService(
        IMessageWriter messageWriter,
        IEnumerable<IMessageWriter> messageWriters)
    {
        Trace.Assert(messageWriter is ConsoleMessageWriter);
        Trace.Assert(messageWriters.Single() is ConsoleMessageWriter);
    }
}

Para obter mais informações, consulte:

Os métodos TryAddEnumerable (Service Descriptor) registram o serviço somente se ainda não houver uma implementação do mesmo tipo. Vários serviços são resolvidos via IEnumerable<{SERVICE}>. Ao registrar serviços, adicione uma instância se um dos mesmos tipos ainda não tiver sido adicionado. Os autores de biblioteca usam TryAddEnumerable para evitar o registro de várias cópias de uma implementação no contêiner.

No exemplo a seguir, a primeira chamada para TryAddEnumerable registra MessageWriter como uma implementação para IMessageWriter1 . A segunda chamada é registrada MessageWriter para IMessageWriter2 . A terceira chamada não tem nenhum efeito porque IMessageWriter1 já tem uma implementação registrada de MessageWriter :

public interface IMessageWriter1 { }
public interface IMessageWriter2 { }

public class MessageWriter : IMessageWriter1, IMessageWriter2
{
}

services.TryAddEnumerable(ServiceDescriptor.Singleton<IMessageWriter1, MessageWriter>());
services.TryAddEnumerable(ServiceDescriptor.Singleton<IMessageWriter2, MessageWriter>());
services.TryAddEnumerable(ServiceDescriptor.Singleton<IMessageWriter1, MessageWriter>());

O registro de serviço geralmente é independente de ordem, exceto ao registrar várias implementações do mesmo tipo.

IServiceCollection é uma coleção de ServiceDescriptor objetos. O exemplo a seguir mostra como registrar um serviço criando e adicionando um ServiceDescriptor :

string secretKey = Configuration["SecretKey"];
var descriptor = new ServiceDescriptor(
    typeof(IMessageWriter),
    _ => new DefaultMessageWriter(secretKey),
    ServiceLifetime.Transient);

services.Add(descriptor);

Os Add{LIFETIME} métodos internos usam a mesma abordagem. Por exemplo, consulte o código-fonte de Addscoped.

Comportamento da injeção de construtor

Os serviços podem ser resolvidos usando:

Os construtores podem aceitar argumentos que não são fornecidos pela injeção de dependência, mas que precisam atribuir valores padrão.

Quando os serviços são resolvidos por IServiceProvider ou ActivatorUtilities, a injeção do construtor exige um construtor público.

Quando os serviços são resolvidos por ActivatorUtilities, a injeção de construtor exige a existência de apenas de um construtor aplicável. Há suporte para sobrecargas de construtor, mas somente uma sobrecarga pode existir, cujos argumentos podem ser todos atendidos pela injeção de dependência.

Validação de escopo

Quando o aplicativo é executado no Development ambiente e chama CreateDefaultBuilder para criar o host, o provedor de serviço padrão executa verificações para verificar se:

  • Os serviços com escopo não são resolvidos do provedor de serviços raiz.
  • Os serviços com escopo não são injetados em singletons.

O provedor de serviços raiz é criado quando BuildServiceProvider é chamado. O tempo de vida do provedor de serviço raiz corresponde ao tempo de vida do aplicativo quando o provedor começa com o aplicativo e é Descartado quando o aplicativo é desligado.

Os serviços com escopo são descartados pelo contêiner que os criou. Se um serviço com escopo for criado no contêiner raiz, o tempo de vida do serviço será efetivamente promovido para singleton porque é descartado apenas pelo contêiner raiz quando o aplicativo é desligado. A validação dos escopos de serviço detecta essas situações quando BuildServiceProvider é chamado.

Cenários de escopo

O IServiceScopeFactory é sempre registrado como um singleton, mas o IServiceProvider pode variar com base no tempo de vida da classe que a contém. Por exemplo, se você resolver serviços de um escopo e qualquer um desses serviços pegar um IServiceProvider , ele será uma instância com escopo.

Para obter serviços de escopo em implementações do IHostedService , como o BackgroundService , não Insira as dependências de serviço por meio de injeção de construtor. Em vez disso, insira IServiceScopeFactory , crie um escopo e, em seguida, resolva as dependências do escopo para usar o tempo de vida do serviço apropriado.

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace WorkerScope.Example
{
    public class Worker : BackgroundService
    {
        private readonly ILogger<Worker> _logger;
        private readonly IServiceScopeFactory _serviceScopeFactory;

        public Worker(ILogger<Worker> logger, IServiceScopeFactory serviceScopeFactory) =>
            (_logger, _serviceScopeFactory) = (logger, serviceScopeFactory);

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                using (IServiceScope scope = _serviceScopeFactory.CreateScope())
                {
                    try
                    {
                        _logger.LogInformation(
                            "Starting scoped work, provider hash: {hash}.",
                            scope.ServiceProvider.GetHashCode());

                        var store = scope.ServiceProvider.GetRequiredService<IObjectStore>();
                        var next = await store.GetNextAsync();
                        _logger.LogInformation("{next}", next);

                        var processor = scope.ServiceProvider.GetRequiredService<IObjectProcessor>();
                        await processor.ProcessAsync(next);
                        _logger.LogInformation("Processing {name}.", next.Name);

                        var relay = scope.ServiceProvider.GetRequiredService<IObjectRelay>();
                        await relay.RelayAsync(next);
                        _logger.LogInformation("Processed results have been relayed.");

                        var marked = await store.MarkAsync(next);
                        _logger.LogInformation("Marked as processed: {next}", marked);
                    }
                    finally
                    {
                        _logger.LogInformation(
                            "Finished scoped work, provider hash: {hash}.{nl}",
                            scope.ServiceProvider.GetHashCode(), Environment.NewLine);
                    }
                }
            }
        }
    }
}

No código anterior, enquanto o aplicativo está em execução, o serviço em segundo plano:

  • Depende do IServiceScopeFactory .
  • Cria um IServiceScope para resolver serviços adicionais.
  • Resolve serviços com escopo para consumo.
  • Funciona em objetos de processamento e, em seguida, retransmiti-los e, por fim, os marca como processado.

No código-fonte de exemplo, você pode ver como as implementações do IHostedService podem se beneficiar de tempos de vida do serviço com escopo.

Confira também