Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
O .NET dá suporte ao padrão de design de software Injeção de Dependência (DI), que é uma técnica para alcançar a 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. A classe a seguir MessageWriter 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 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);
}
}
}
Nesse caso, a Worker classe cria e depende diretamente da MessageWriter classe. Dependências codificadas como essa são problemáticas e devem ser evitadas pelos seguintes motivos:
- Para substituir
MessageWriterpor uma implementação diferente, você deve modificar aWorkerclasse. - Se
MessageWritertiver dependências, aWorkerclasse também deverá configurá-las. Em um projeto grande com várias classes dependendo daMessageWriter, 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.
O conceito
A injeção de dependência resolve problemas de dependência codificados por meio de:
O uso de uma interface ou classe base para abstrair a implementação da dependência.
Registro da 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, use BuildServiceProvider para criar o contêiner de serviço.
Injeção do serviço no construtor da classe em que 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.
Dica
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 serviço não está relacionado a um serviço Web, embora possa usar um serviço Web.
Como exemplo, suponha que a IMessageWriter interface defina o Write método. Essa interface é implementada por um tipo concreto, MessageWritermostrado anteriormente. O código de exemplo a seguir registra o serviço IMessageWriter com o tipo concreto MessageWriter. O AddSingleton método registra o serviço com um tempo de vida singleton, o que significa que ele não é descartado até que o aplicativo seja desligado.
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 realçadas:
- Crie uma instância do construtor de aplicativos host.
- Configure os serviços registrando o
Workercomo um serviço hospedado e registrar a interfaceIMessageWritercomo um serviço singleton com uma implementação correspondente da classeMessageWriter. - Crie o host e execute-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 criar automaticamente uma instância de Worker e fornecer a implementação de IMessageWriter correspondente como argumento.
Usando o padrão DI, o serviço de trabalho não usa o tipo MessageWriter concreto, apenas a interface IMessageWriter que ele implementa. Esse design facilita a alteração da implementação que o serviço de trabalho usa sem modificar o serviço de trabalho. O serviço de trabalho também não cria uma instância de MessageWriter. O contêiner de DI cria a instância.
Agora, imagine que você deseja substituir MessageWriter com um tipo que usa o serviço de log 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 registrar esta nova IMessageWriter implementação:
builder.Services.AddSingleton<IMessageWriter, LoggingMessageWriter>();
Dica
O contêiner é resolvido aproveitando ILogger<TCategoryName> (genéricos), o que elimina a necessidade de registrar todos os tipos construídos (genéricos).
Comportamento da injeção de construtor
Os serviços podem ser resolvidos usando IServiceProvider (o contêiner de serviço interno) ou ActivatorUtilities.
ActivatorUtilities cria objetos que não estão registrados no contêiner e são usados com alguns recursos de estrutura.
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 IServiceProvider ou ActivatorUtilities resolvem serviços, a injeção de construtor requer um construtor público.
Quando ActivatorUtilities resolve serviços, a injeção de construtor requer que exista apenas 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.
Regras de seleção do 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:
public class ExampleService
{
public ExampleService()
{
}
public ExampleService(ILogger<ExampleService> logger)
{
// ...
}
public ExampleService(ServiceA serviceA, ServiceB serviceB)
{
// ...
}
}
No código anterior, suponha que o logging tenha sido adicionado e seja resolvível do provedor 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 defina mais parâmetros, os tipos ServiceA e ServiceB 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#:
Aviso
Esse ExampleService código com parâmetros de tipo ambíguos resolvíveis por DI gera uma exceção.
Não faça isso– ele se destina a mostrar o que significa "tipos ambíguos resolvíveis por 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 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 de DI tenta resolver o ExampleService tipo, ele gera uma exceção, pois os dois construtores são ambíguos.
Evite a ambiguidade definindo um construtor que aceite ambos os tipos resolvíveis por DI:
public class ExampleService
{
public ExampleService()
{
}
public ExampleService(
ILogger<ExampleService> logger,
IOptions<ExampleOptions> options)
{
// ...
}
}
Validação de escopo
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 ciclo de vida do serviço é efetivamente promovido a 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.
Quando um aplicativo é executado no ambiente de desenvolvimento 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 do provedor de serviços raiz.
- Os serviços com escopo não são injetados em singletons.
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 exigir um IServiceProvider, será uma instância com escopo.
Para obter serviços de escopo dentro de implementações de IHostedService, como o BackgroundService, não insera 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 outros serviços.
- 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
Você pode registrar serviços e executar pesquisas com base em uma chave. Em outras palavras, é possível registrar vários serviços com chaves diferentes 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.
key pode ser qualquer object que você quiser, 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...
}
}
Propriedade KeyedService.AnyKey
A KeyedService.AnyKey propriedade fornece uma chave especial para trabalhar com serviços com chave. Você pode registrar um serviço usando KeyedService.AnyKey como um fallback que corresponde a qualquer chave. Isso é útil quando você deseja fornecer uma implementação padrão para qualquer chave que não tenha um registro 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:
- A solicitação
ICachecom chave"premium"retorna aPremiumCacheinstância. - A solicitação
ICachecom qualquer outra chave (como"basic"ou"standard") cria uma novaDefaultCacheusando oAnyKeyfallback.
Importante
A partir do .NET 10, a chamada GetKeyedService() com KeyedService.AnyKey lança um InvalidOperationException porque AnyKey destina-se a ser uma alternativa de registro, e não como uma chave de consulta. Para obter mais informações, consulte Corrigir problemas em GetKeyedService() e GetKeyedServices() com AnyKey.
Consulte também
- Início Rápido: Noções básicas de injeção de dependência
- Tutorial: usar a injeção de dependência no .NET
- Diretrizes de injeção de dependência
- Injeção de dependência no ASP.NET Core
- Padrões de conferência NDC para desenvolvimento de aplicativos DI
- Princípio de dependências explícitas
- Inversão de Contêineres de Controle e o Padrão de Injeção de Dependência (Martin Fowler)