Injeção de dependência do .NET

O .NET dá suporte ao padrão de design de software de DI (injeção de dependência), que é uma técnica para conseguir IoC (inversão de controle) entre classes e suas dependências. A injeção de dependência no .NET é uma parte interna da estrutura, juntamente com a configuração, o registro em log e o padrão de opções.

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

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

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

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

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

A classe cria e depende diretamente da instância MessageWriter. As dependências embutidas em código (como no exemplo anterior) são problemáticas e devem ser evitadas por estes motivos:

  • Para substituir MessageWriter por uma implementação diferente, a classe Worker deve ser modificada.
  • Se MessageWriter tiver dependências, elas também deverão ser configuradas pela classe Worker. 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, o IServiceProvider. Normalmente, os serviços são registrados na inicialização do aplicativo e acrescentados 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 interface IMessageWriter define o método Write:

namespace DependencyInjection.Example;

public interface IMessageWriter
{
    void Write(string message);
}

Essa interface é implementada por um tipo concreto, MessageWriter:

namespace DependencyInjection.Example;

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

O código de amostra registra o serviço IMessageWriter com o tipo concreto MessageWriter. O método AddSingleton registra o serviço com um tempo de vida singleton, o tempo de vida do aplicativo. O Tempo de vida de serviço é descrito posteriormente neste artigo.

using DependencyInjection.Example;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddHostedService<Worker>();
builder.Services.AddSingleton<IMessageWriter, MessageWriter>();

using IHost host = builder.Build();

host.Run();

No código anterior, o aplicativo de exemplo:

  • Cria uma instância do construtor de aplicativos do host.

  • Configura os serviços registrando:

    • O Worker como um serviço hospedado. Para obter mais informações, confira Worker Services no .NET.
    • A interface IMessageWriter como um serviço singleton com uma implementação correspondente da classe MessageWriter.
  • Cria o host e o executa.

O host contém o provedor de serviços de injeção de dependência. Ele também contém todos os outros serviços relevantes necessários para criar automaticamente uma instância de Worker e fornecer a implementação de IMessageWriter correspondente como argumento.

namespace DependencyInjection.Example;

public sealed class Worker(IMessageWriter messageWriter) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            messageWriter.Write($"Worker running at: {DateTimeOffset.Now}");
            await Task.Delay(1_000, stoppingToken);
        }
    }
}

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

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

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

namespace DependencyInjection.Example;

public class LoggingMessageWriter(
    ILogger<LoggingMessageWriter> logger) : IMessageWriter
{
    public void Write(string message) =>
        logger.LogInformation("Info: {Msg}", message);
}

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

builder.Services.AddSingleton<IMessageWriter, LoggingMessageWriter>();

O tipo HostApplicationBuilder (builder) faz parte do pacote NuGet Microsoft.Extensions.Hosting.

LoggingMessageWriter depende ILogger<TCategoryName>do que ele solicita no construtor. ILogger<TCategoryName> é um serviço fornecido pela estrutura.

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 ILogger<TCategoryName> aproveitando os tipos abertos (genéricos), eliminando a necessidade de registrar todo tipo construído (genérico).

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

  • Normalmente, é um objeto que fornece um serviço para outros objetos, como o serviço IMessageWriter.
  • 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 implementações IMessageWriter mostradas nos exemplos anteriores foram gravadas para demonstrar a DI básica, não para implementar o registro em log. A maioria dos aplicativos não deve precisar escrever agentes. O código a seguir demonstra o uso do registro padrão, que requer apenas que o Worker seja registrado 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(1_000, stoppingToken);
        }
    }
}

Usando o código anterior, não é necessário atualizar Program.cs, pois o log é fornecido pela estrutura.

Várias regras de descoberta de construtor

Quando um tipo define mais de um construtor, o provedor de serviços tem lógica para determinar qual construtor usar. O construtor com mais parâmetros em que os tipos são resolvíveis por DI é selecionado. Considere o seguinte serviço de exemplo em C#:

public class ExampleService
{
    public ExampleService()
    {
    }

    public ExampleService(ILogger<ExampleService> logger)
    {
        // omitted for brevity
    }

    public ExampleService(FooService fooService, BarService barService)
    {
        // omitted for brevity
    }
}

No código anterior, suponha que o registro em log tenha sido adicionado e seja resolvível do provedor de serviços, mas isso não acontece com os tipos FooService e BarService. O construtor com o parâmetro ILogger<ExampleService> é usado para resolver a instância ExampleService. Embora haja um construtor que defina mais parâmetros, os tipos FooService e BarService não são resolvíveis por DI.

Se houver ambiguidade ao descobrir construtores, uma exceção será gerada. Considere o seguinte serviço de exemplo em C#:

public class ExampleService
{
    public ExampleService()
    {
    }

    public ExampleService(ILogger<ExampleService> logger)
    {
        // omitted for brevity
    }

    public ExampleService(IOptions<ExampleOptions> options)
    {
        // omitted for brevity
    }
}

Aviso

O código ExampleService com parâmetros de tipo solucionáveis di ambíguos geraria uma exceção. Não faça isso — a intenção é mostrar o que se destina a "tipos ambíguos resolvíveis por DI".

No exemplo anterior, há três construtores. O primeiro construtor é sem parâmetros e não requer nenhum serviço do provedor de serviços. Suponha que o registro em log e as opções tenham sido adicionados ao contêiner di e sejam serviços resolvíveis por DI. Quando o contêiner di tentar resolver o tipo ExampleService, ele gerará uma exceção, pois os dois construtores são ambíguos.

Você pode evitar a ambiguidade definindo um construtor que aceita ambos os tipos resolvíveis por DI:

public class ExampleService
{
    public ExampleService()
    {
    }

    public ExampleService(
        ILogger<ExampleService> logger,
        IOptions<ExampleOptions> options)
    {
        // omitted for brevity
    }
}

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 único método de extensão Add{GROUP_NAME} para registrar todos os serviços exigidos por um recurso de estrutura. Por exemplo, o método de extensão AddOptions registra todos os serviços necessários para usar opções.

Serviços fornecidos pela estrutura

Ao usar qualquer um dos padrões disponíveis do host ou do construtor de aplicativos, os padrões são aplicados e os serviços são registrados pela estrutura. Considere alguns dos padrões mais populares do host e do construtor de aplicativos:

Depois de criar um construtor a partir de qualquer uma dessas APIs, o IServiceCollection tem serviços definidos pela estrutura, dependendo de como o host foi configurado. Para aplicativos com base nos modelos do .NET, a estrutura pode registrar centenas de serviços.

A tabela a seguir lista um pequeno exemplo desses serviços registrados por estrutura:

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:

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. Para registrar um serviço como transitório, chame AddTransient.

Em aplicativos que processam solicitações, os serviços transitórios são descartados no final da solicitação. Esse tempo de vida incorre em alocações por solicitação, pois os serviços são resolvidos e construídos sempre. Para obter mais informações, consulte Diretrizes de Injeção de Dependência: diretrizes IDisposable para instâncias transitórias e compartilhadas.

Com escopo

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

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

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

Observação

Não resolva um serviço com escopo de um singleton e tome cuidado para não fazê-lo indiretamente, por exemplo, por meio de um serviço temporário. Pode fazer com que o serviço tenha um estado incorreto durante o processamento das solicitações seguintes. É possível:

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

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

Singleton

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

  • A primeira vez que eles são solicitados.
  • 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 comportamento singleton, permita que o contêiner de serviço gerencie a vida útil 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 um alocador for registrado como singleton, o contêiner descartará o singleton automaticamente.

Registrar 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 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));
No Sim Yes
AddSingleton(new {IMPLEMENTATION})

Exemplos:

services.AddSingleton(new MyDep());
services.AddSingleton(new MyDep(99));
No No Sim

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

Registrar um serviço com apenas um tipo de implementação equivale a registrar esse serviço com o mesmo tipo de implementação e 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 eles 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 tipo de serviço. A segunda chamada para AddSingleton substitui a anterior quando resolvida como IMessageWriter e 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 ConsoleDI.IEnumerableExample;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddSingleton<IMessageWriter, ConsoleMessageWriter>();
builder.Services.AddSingleton<IMessageWriter, LoggingMessageWriter>();
builder.Services.AddSingleton<ExampleService>();

using IHost host = builder.Build();

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

await host.RunAsync();

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

using System.Diagnostics;

namespace ConsoleDI.IEnumerableExample;

public sealed 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 IMessageWritere um IEnumerable<IMessageWriter>. O único IMessageWriter é a última implementação a ser registrada, enquanto IEnumerable<IMessageWriter> representa todas as implementações registradas.

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

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

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

A TryAddSingleton não tem efeito, pois já foi adicionado e a "tentativa" falhará. A declaração ExampleService seria a 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(ServiceDescriptor) 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 da 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 registra 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 objetos ServiceDescriptor. 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 métodos internos Add{LIFETIME} usam a mesma abordagem. Por exemplo, consulte o código-fonte 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 ambiente Development e chama CreateDefaultBuilder para criar o host, o provedor de serviços 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 é criado no contêiner raiz, o tempo de vida do serviço é promovido efetivamente para singleton, porque ele só é descartado 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 no escopo

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

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

namespace WorkerScope.Example;

public sealed class Worker(
    ILogger<Worker> logger,
    IServiceScopeFactory serviceScopeFactory)
    : BackgroundService
{
    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 de IServiceScopeFactory.
  • Cria um IServiceScope para resolver serviços adicionais.
  • Resolve serviços com escopo para consumo.
  • Funciona no processamento de objetos e, em seguida, retransmissão deles e, finalmente, os marca como processados.

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

Serviços com chave

A partir do .NET 8, há suporte para registros e pesquisas de serviços com base em uma chave, o que significa que é possível registrar vários serviços com uma chave diferente e usar essa chave para a pesquisa.

Por exemplo, considere o caso em que você tem implementações diferentes da interface IMessageWriter: MemoryMessageWriter e QueueMessageWriter.

Você pode registrar esses serviços usando a sobrecarga dos métodos de registro do serviço (vistos anteriormente) que dá suporte a uma chave como parâmetro:

services.AddKeyedSingleton<IMessageWriter, MemoryMessageWriter>("memory");
services.AddKeyedSingleton<IMessageWriter, QueueMessageWriter>("queue");

O key não está limitado a string, pode ser qualquer object que desejar, desde que o tipo implemente corretamente Equals.

No construtor da classe que usa IMessageWriter, adicione FromKeyedServicesAttribute para especificar a chave do serviço a ser resolvido:

public class ExampleService
{
    public ExampleService(
        [FromKeyedServices("queue")] IMessageWriter writer)
    {
        // Omitted for brevity...
    }
}

Confira também