Injeção de dependência do .NET

O .NET suporta o padrão de design de software de injeção de dependência (DI), que é uma técnica para alcançar Inversão de Controle (IoC) 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 seguinte MessageWriter classe com um Write método do qual 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();

    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 MessageWriter classe. Dependências codificadas, 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 devem ser configuradas Worker pela classe. Em um projeto grande com várias classes, dependendo MessageWriterdo , o código de configuração fica espalhado pelo aplicativo.
  • Esta implementação é difícil de testar por unidade. O aplicativo deve usar uma classe simulada ou stub MessageWriter , o que não é possível com essa abordagem.

A injeção de dependência resolve esses problemas através de:

  • O uso de uma interface ou classe base para abstrair a implementação de dependência.
  • Registro da 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 registados no arranque da aplicação e anexados a um IServiceCollectionficheiro . 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 onde ele é usado. A estrutura assume a responsabilidade de criar uma instância da dependência e eliminá-la quando ela não for mais necessária.

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

namespace DependencyInjection.Example;

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

Esta 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 exemplo registra o IMessageWriter serviço com o tipo MessageWriterconcreto. O AddSingleton método registra o serviço com um tempo de vida único, o tempo de vida do aplicativo. Os tempos de vida do serviço são descritos mais adiante 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 host.

  • Configura os serviços registrando:

    • O Worker como um serviço hospedado. Para obter mais informações, consulte Serviços de trabalho no .NET.
    • A IMessageWriter interface como um serviço singleton com uma implementação correspondente da MessageWriter classe.
  • 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 instanciar automaticamente o Worker e fornecer a implementação correspondente IMessageWriter como um 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 do trabalhador:

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

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

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 IMessageWriter implementação:

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

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

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

Não é incomum usar a injeção de dependência de forma encadeada. Cada dependência solicitada, por sua vez, solicita suas próprias dependências. O contêiner resolve as dependências no gráfico e retorna o serviço totalmente resolvido. O conjunto coletivo de dependências que devem ser resolvidas é normalmente chamado de árvore de dependência, gráfico de dependência ou gráfico de objeto.

O recipiente ILogger<TCategoryName> resolve aproveitando os tipos abertos (genéricos), eliminando a necessidade de registrar todos os tipos construídos (genéricos).

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 IMessageWriter serviç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 robusto. As IMessageWriter implementações mostradas nos exemplos anteriores foram escritas para demonstrar DI básica, não para implementar o registro em log. A maioria dos aplicativos não precisa escrever loggers. O código a seguir demonstra o uso do log padrão, que requer apenas o Worker registro como um serviço AddHostedServicehospedado :

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 há necessidade de atualizar Program.cs, porque o log é fornecido pela estrutura.

Várias regras de descoberta de construtores

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 onde os tipos são DI-resolvable é 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 log foi adicionado e pode ser resolvido pelo provedor de serviços, mas os FooService tipos e BarService não são. O construtor com o ILogger<ExampleService> parâmetro é usado para resolver a ExampleService instância. Mesmo que haja um construtor que define mais parâmetros, os FooService e BarService tipos não são DI-resolvíveis.

Se houver ambiguidade ao descobrir construtores, uma exceção é lançada. 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 ExampleService código com parâmetros de tipo DI-resolvíveis ambíguos lançaria uma exceção. Não faça isso — destina-se a mostrar o que se entende por "tipos ambíguos DI-resolvíveis".

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

Você pode evitar ambiguidade definindo um construtor que aceite ambos os tipos DI-resolvable:

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 Add{GROUP_NAME} método de extensão 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 opções.

Serviços prestados no quadro

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

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 baseados nos modelos .NET, a estrutura pode registrar centenas de serviços.

A tabela a seguir lista uma pequena amostra desses serviços registrados na estrutura:

Tipo de Serviço Vitalício
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

Vida útil 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 adequado para cada serviço registado.

Transitório

Os serviços de tempo de vida transitório são criados sempre que são solicitados a partir do contêiner de serviço. Para registrar um serviço como transitório, ligue AddTransientpara .

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: orientação IDisposable para instâncias transitórias e compartilhadas.

Âmbito de aplicação

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

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 AddDbContext método de DbContext extensão registra tipos com um tempo de vida com escopo por padrão.

Nota

Não resolva um serviço com escopo a partir de um singleton e tenha cuidado para não fazê-lo indiretamente, por exemplo, através de um serviço transitório. Isso pode fazer com que o serviço tenha um estado incorreto ao processar solicitações subsequentes. Não há problema em:

  • Resolva um serviço singleton a partir 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 uma vida útil mais longa gera uma exceção. Para obter mais informações, consulte Validação de escopo.

Singleton

Os serviços vitalícios Singleton são criados:

  • A primeira vez que são solicitados.
  • Pelo desenvolvedor, ao fornecer uma instância de implementação diretamente para o contêiner. Esta 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 por código que resolveu o serviço do contêiner. Se um tipo ou fábrica estiver registado como singleton, o contentor elimina o singleton automaticamente.

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

Em aplicativos que processam solicitações, os serviços singleton são descartados quando o é descartado no desligamento do ServiceProvider 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 registo de serviços

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ático
objeto
eliminação
Várias
Implementações
Passe args
Add{LIFETIME}<{SERVICE}, {IMPLEMENTATION}>()

Exemplo:

services.AddSingleton<IMyDep, MyDep>();
Sim Sim No
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 No 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));
No No Sim

Para obter mais informações sobre a eliminação por tipo, consulte a seção Descarte de serviços .

Registrar um serviço com apenas um tipo de implementação é equivalente a registrar esse 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 aceitam 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 o tipo de serviço. A segunda chamada para AddSingleton substituir a anterior quando resolvida como IMessageWriter e adiciona à anterior quando vários serviços são resolvidos via IEnumerable<IMessageWriter>. Os serviços aparecem na ordem em que foram registrados quando resolvidos via 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 do construtor: um único IMessageWritere um IEnumerable<IMessageWriter>. O único IMessageWriter é a última implementação a ter sido registrada, enquanto o 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 registros 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 a "tentativa" falhará. O ExampleService afirmaria 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(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 registrar várias cópias de uma implementação no contêiner.

No exemplo a seguir, a primeira chamada para TryAddEnumerable registros MessageWriter como uma implementação para IMessageWriter1. O segundo convite regista-se MessageWriter para IMessageWriter2. A terceira chamada não tem 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 do serviço geralmente é independente da ordem, exceto quando se registram 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 métodos internos Add{LIFETIME} usam a mesma abordagem. Por exemplo, consulte o código-fonte AddScoped.

Comportamento de injeção do 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 os argumentos devem atribuir valores padrão.

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

Quando os serviços são resolvidos pelo ActivatorUtilities, a injeção do construtor requer que exista apenas um construtor aplicável. Sobrecargas de construtor são suportadas, mas apenas uma sobrecarga pode existir cujos argumentos podem ser todos atendidos por injeção de dependência.

Validação do âmbito

Quando o aplicativo é executado no Development ambiente e chama CreateApplicationBuilder 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 a partir 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ços 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 ele só será descartado pelo contêiner raiz quando o aplicativo for desligado. A validação de escopos de serviço deteta essas situações quando BuildServiceProvider é chamada.

Cenários de escopo

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

Para obter serviços de escopo dentro de implementações do , como o BackgroundService, não injete as dependências de serviço por meio da injeção do IHostedServiceconstrutor. Em vez disso, injete IServiceScopeFactory, crie um escopo e resolva dependências do escopo para usar o tempo de vida de 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 do IServiceScopeFactory.
  • Cria um IServiceScope para resolver serviços adicionais.
  • Resolve serviços com escopo para consumo.
  • Trabalha no processamento de objetos e, em seguida, retransmiti-los e, finalmente, marca-os como processados.

A partir do 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 chaveados

A partir do .NET 8, há suporte para registros de serviço e pesquisas 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 diferentes implementações da interface IMessageWriter: MemoryMessageWriter e QueueMessageWriter.

Você pode registrar esses serviços usando a sobrecarga dos métodos de registro de serviço (vistos anteriormente) que suportam 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 o que object quiser, desde que o tipo implemente Equalscorretamente.

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

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

Consulte também