Partilhar via


Injeção de dependência do .NET

O .NET suporta o padrão de design de software de injeção de dependências (DI), que é uma técnica para alcançar a Inversão de Controlo (IoC) entre classes e as 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.

Um de dependência é um objeto do qual outro objeto depende. A seguinte MessageWriter classe tem um Write método do qual outras classes podem depender:

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

Uma classe pode criar uma instância da MessageWriter classe para usar seu Write método. No exemplo seguinte, 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);
        }
    }
}

Neste caso, a Worker classe cria e depende diretamente da MessageWriter classe. Dependências codificadas como esta são problemáticas e devem ser evitadas pelas seguintes razões:

  • Para substituir MessageWriter por uma implementação diferente, você deve modificar a Worker classe.
  • Se MessageWriter tiver dependências, a Worker classe também deve configurá-las. Num projeto grande com várias classes dependentes de MessageWriter, 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 MessageWriter simulada ou stub, o que não é possível com essa abordagem.

O conceito

A injeção de dependência resolve problemas de dependência codificados de forma rígida através de:

  • O uso de uma interface ou classe base para abstrair a implementação de dependência.

  • Registo da dependência num contentor 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, use BuildServiceProvider para criar o contêiner de serviço.

  • Injeção do serviço no construtor da classe onde este é utilizado.

    A estrutura assume a responsabilidade de criar uma instância da dependência e eliminá-la quando ela não for mais necessária.

Sugestão

Na terminologia de injeção de dependências, um serviço é tipicamente um objeto que fornece um serviço a outros objetos, como o IMessageWriter serviço. O serviço não está relacionado com um serviço web, embora possa usar um serviço web.

Como exemplo, assuma que a IMessageWriter interface define o Write método. Esta interface é implementada por um tipo concreto, MessageWriter, mostrado anteriormente. O seguinte código de exemplo regista o serviço IMessageWriter com o tipo concreto MessageWriter. O AddSingleton método regista o serviço com um ciclo de vida singleton, o que significa que não é descartado até a aplicação encerrar.

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

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

using IHost host = builder.Build();

host.Run();

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

// <SnippetIMW>
public interface IMessageWriter
{
    void Write(string message);
}
// </SnippetIMW>

// <SnippetWorker>
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);
        }
    }
}

// </SnippetWorker>

No exemplo de código anterior, as linhas destacadas:

  • Crie uma instância de desenvolvedor de aplicação anfitriã.
  • Configure os serviços registando o Worker como serviço alojado e a interface IMessageWriter como serviço singleton com uma implementação correspondente da classe MessageWriter.
  • Constrói o host e executa-o.

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.

Ao usar o padrão DI, o serviço de trabalhador não utiliza o tipo MessageWriter concreto, apenas a interface IMessageWriter que implementa. Este design facilita a alteração da implementação utilizada pelo serviço de trabalho sem modificar o próprio serviço de trabalho. O serviço de execução também não cria uma instância de MessageWriter. O contêiner DI cria a instância.

Agora, imagine que quer trocar MessageWriter por um tipo que utiliza o serviço de registo fornecido pelo framework. Crie uma classe LoggingMessageWriter que dependa de ILogger<TCategoryName> solicitando-a no construtor.

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

Para mudar de MessageWriter para LoggingMessageWriter, basta atualizar a chamada para AddSingleton para registar esta nova implementação de IMessageWriter:

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

Sugestão

O contentor resolve ILogger<TCategoryName> aproveitando tipos abertos (genéricos), o que elimina a necessidade de registar cada tipo construído (genérico).

Comportamento de injeção do construtor

Os serviços podem ser resolvidos usando IServiceProvider (o contentor de serviço incorporado) ou ActivatorUtilities. ActivatorUtilities cria objetos que não estão registados no contentor e é usado com algumas funcionalidades do framework.

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 IServiceProvider ou ActivatorUtilities resolver serviços, a injeção do construtor requer um construtor público .

Quando ActivatorUtilities resolve serviços, 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.

Regras de seleção 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 exemplo de serviço:

public class ExampleService
{
    public ExampleService()
    {
    }

    public ExampleService(ILogger<ExampleService> logger)
    {
        // ...
    }

    public ExampleService(ServiceA serviceA, ServiceB serviceB)
    {
        // ...
    }
}

No código anterior, suponha que o registo foi adicionado e pode ser resolvido pelo fornecedor de serviços, mas os tipos ServiceA e ServiceB não são. O construtor com o ILogger<ExampleService> parâmetro resolve a ExampleService instância. Mesmo que haja um construtor que define mais parâmetros, os ServiceA e ServiceB tipos não são DI-resolvable.

Se houver ambiguidade ao descobrir construtores, uma exceção é lançada. Considere o seguinte serviço de exemplo em C#:

Advertência

Este ExampleService código com parâmetros ambíguos de tipo DI resolvíveis lança uma exceção. Não faça isso — ele pretende mostrar o que se entende por "tipos ambíguos resolúveis de DI".

public class ExampleService
{
    public ExampleService()
    {
    }

    public ExampleService(ILogger<ExampleService> logger)
    {
        // ...
    }

    public ExampleService(IOptions<ExampleOptions> options)
    {
        // ...
    }
}

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ça uma exceção, pois os dois construtores são ambíguos.

Evite ambiguidade definindo um construtor que aceite todos os tipos resolvíveis por DI, em vez disso.

public class ExampleService
{
    public ExampleService()
    {
    }

    public ExampleService(
        ILogger<ExampleService> logger,
        IOptions<ExampleOptions> options)
    {
        // ...
    }
}

Validação do âmbito

Os serviços com âmbito são eliminados pelo container que os criou. Se um serviço com âmbito for criado no contentor raiz, a vida útil do serviço é efetivamente promovida para singleton porque só é eliminado pelo contentor raiz quando a aplicação encerra. A validação de escopos de serviço deteta essas situações quando BuildServiceProvider é chamado.

Quando uma aplicação corre no ambiente de desenvolvimento e chama o CreateApplicationBuilder para construir o host, o fornecedor de serviço predefinido realiza verificações para verificar que:

  • 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.

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 resolver serviços dentro de um escopo, e qualquer um desses serviços requerer um IServiceProvider, é uma instância com escopo.

Para obter serviços de escopo dentro de implementações de IHostedService, como o BackgroundService, não injete as dependências de serviço por meio da injeção de construtor. 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 facilitar a resolução de outros serviços.
  • 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

Pode registar serviços e realizar pesquisas baseadas numa chave. Ou seja, é possível registar múltiplos serviços com chaves diferentes e usar essa chave para a consulta.

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. O key pode ser qualquer object que desejar, desde que o tipo implemente Equals corretamente.

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...
    }
}

Propriedade KeyedService.AnyKey

A KeyedService.AnyKey propriedade fornece uma chave especial para trabalhar com serviços vinculados. Pode registar um serviço usando KeyedService.AnyKey como alternativa de recurso que corresponda a qualquer chave. Isto é útil quando se quer fornecer uma implementação padrão para qualquer chave que não tenha um registo explícito.

var services = new ServiceCollection();

// Register a fallback cache for any key.
services.AddKeyedSingleton<ICache>(KeyedService.AnyKey, (sp, key) =>
{
    // Create a cache instance based on the key.
    return new DefaultCache(key?.ToString() ?? "unknown");
});

// Register a specific cache for the "premium" key.
services.AddKeyedSingleton<ICache>("premium", new PremiumCache());

var provider = services.BuildServiceProvider();

// Requesting with "premium" key returns PremiumCache.
var premiumCache = provider.GetKeyedService<ICache>("premium");
Console.WriteLine($"Premium key: {premiumCache}");

// Requesting with any other key uses the AnyKey fallback.
var basicCache = provider.GetKeyedService<ICache>("basic");
Console.WriteLine($"Basic key: {basicCache}");

var standardCache = provider.GetKeyedService<ICache>("standard");
Console.WriteLine($"Standard key: {standardCache}");

No exemplo anterior:

  • Solicitar ICache com a chave "premium" devolve a instância PremiumCache.
  • Solicitar o ICache utilizando qualquer outra chave (como "basic" ou "standard") cria um novo DefaultCache usando o AnyKey alternativa.

Importante

A partir do .NET 10, chamar GetKeyedService() com KeyedService.AnyKey lança uma InvalidOperationException porque AnyKey é destinado a ser uma alternativa para registo, não como uma chave de consulta. Para mais informações, consulte Corrigir problemas no GetKeyedService() e GetKeyedServices() com o AnyKey.

Consulte também