Entender as noções básicas de injeção de dependência no .NET
Neste artigo, você vai criar um aplicativo de console do .NET que cria uma ServiceCollection e o ServiceProvider correspondente manualmente. Você vai aprender a registrar serviços e resolvê-los usando injeção de dependência (DI). Este artigo usa o pacote NuGet Microsoft.Extensions.DependencyInjection para demonstrar as noções básicas de DI no .NET.
Observação
Este artigo não aproveita os recursos de Host Genérico. Para obter um guia mais abrangente, confira Usar injeção de dependência no .NET.
Introdução
Para começar, crie um novo aplicativo de console do .NET chamado DI.Basics. Algumas das abordagens mais comuns para criar um projeto de console estão mencionadas na lista a seguir:
- Visual Studio: menu Arquivo > Novo > Projeto.
- Visual Studio Code e as Extensões do Kit de Desenvolvedor em C#: opção do menu Gerenciador de Soluções.
- CLI do .NET: comando
dotnet new console
no terminal.
Você precisa adicionar a referência do pacote à Microsoft.Extensions.DependencyInjection no arquivo do projeto. Independentemente da abordagem, certifique-se de que o projeto seja semelhante ao seguinte XML do arquivo DI.Basics.csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
</ItemGroup>
</Project>
Noções básicas de injeção de dependência
A injeção de dependência é um padrão de design que permite que você remova dependências incluídas no código e torne seu aplicativo mais fácil de manter e testar. A DI é uma técnica para obter uma Inversão de Controle (IoC) entre as classes e suas dependências.
As abstrações de DI no .NET são definidas no pacote NuGet Microsoft.Extensions.DependencyInjection.Abstractions:
- IServiceCollection: define um contrato para uma coleção de descritores do serviço.
- IServiceProvider: define um mecanismo para recuperar um objeto de serviço.
- ServiceDescriptor: descreve um serviço com o respectivo tipo, implementação e tempo de vida do serviço.
No .NET, o DI é gerenciado por meio da adição de serviços e de sua configuração em uma IServiceCollection
. Depois que os serviços são registrados, uma instância de IServiceProvider
é criada chamando o método BuildServiceProvider. O IServiceProvider
atua como um contêiner de todos os serviços registrados e é usado para resolver os serviços.
Criar example serviços
Nem todos os serviços são criados da mesma forma. Alguns serviços requerem uma nova instância sempre que o contêiner de serviço os obtém (transient), enquanto outros devem ser compartilhados entre solicitações (scoped) ou por todo o tempo de vida do aplicativo (singleton). Para obter mais informações sobre os tempos de vida de um serviço, confira Tempos de vida do serviço.
Da mesma forma, alguns serviços expõem apenas um tipo concreto, enquanto outros são expressos como um contrato entre uma interface e um tipo de implementação. Você vai criar diversas variações de serviços para ajudar a demonstrar esses conceitos.
Crie um novo arquivo em C# chamado IConsole.cs e adicione o seguinte código:
public interface IConsole
{
void WriteLine(string message);
}
Esse arquivo define uma interface IConsole
que expõe um único método, WriteLine
. Em seguida, crie um novo arquivo em C# chamado DefaultConsole.cs e adicione o seguinte código:
internal sealed class DefaultConsole : IConsole
{
public bool IsEnabled { get; set; } = true;
void IConsole.WriteLine(string message)
{
if (IsEnabled is false)
{
return;
}
Console.WriteLine(message);
}
}
O código anterior representa a implementação padrão da interface IConsole
. O método WriteLine
grava condicionalmente no console com base na propriedade IsEnabled
.
Dica
A nomenclatura de uma implementação é uma escolha que sua equipe de desenvolvimento deve fazer de comum acordo. O prefixo Default
é uma convenção comum para indicar uma implementação padrão de uma interface, mas não é obrigatória.
Em seguida, crie um arquivo IGreetingService.cs e adicione o seguinte código em C#:
public interface IGreetingService
{
string Greet(string name);
}
Em seguida, adicione um novo arquivo C# chamado DefaultGreetingService.cs e adicione o seguinte código:
internal sealed class DefaultGreetingService(
IConsole console) : IGreetingService
{
public string Greet(string name)
{
var greeting = $"Hello, {name}!";
console.WriteLine(greeting);
return greeting;
}
}
O código anterior representa a implementação padrão da interface IGreetingService
. A implementação do serviço requer um IConsole
como um parâmetro de construtor primário. O método Greet
:
- Cria uma
greeting
levando em conta oname
. - Chama o método
WriteLine
na instânciaIConsole
. - Retorna o
greeting
para o autor da chamada.
O último serviço a ser criado é o arquivo FarewellService.cs. Adicione o seguinte código em C# antes de continuar:
public class FarewellService(IConsole console)
{
public string SayGoodbye(string name)
{
var farewell = $"Goodbye, {name}!";
console.WriteLine(farewell);
return farewell;
}
}
O FarewellService
representa um tipo concreto, não uma interface. Deve ser declarado como public
para que se torne acessível aos consumidores. Ao contrário de outros tipos de implementação de serviço que foram declarados como internal
e sealed
, esse código demonstra que nem todos os serviços precisam ser interfaces. Também mostra que as implementações de serviço podem ser sealed
para impedir a herança e internal
para restringir o acesso ao assembly.
Atualizar a classe Program
Abra o arquivo Program.cs e substitua o código existente pelo seguinte código em C#:
using Microsoft.Extensions.DependencyInjection;
// 1. Create the service collection.
var services = new ServiceCollection();
// 2. Register (add and configure) the services.
services.AddSingleton<IConsole>(
implementationFactory: static _ => new DefaultConsole
{
IsEnabled = true
});
services.AddSingleton<IGreetingService, DefaultGreetingService>();
services.AddSingleton<FarewellService>();
// 3. Build the service provider from the service collection.
var serviceProvider = services.BuildServiceProvider();
// 4. Resolve the services that you need.
var greetingService = serviceProvider.GetRequiredService<IGreetingService>();
var farewellService = serviceProvider.GetRequiredService<FarewellService>();
// 5. Use the services
var greeting = greetingService.Greet("David");
var farewell = farewellService.SayGoodbye("David");
O código atualizado anterior demonstra como fazer:
- Criar uma nova
ServiceCollection
instância. - Registre e configure os serviços na
ServiceCollection
:- O
IConsole
que usa a sobrecarga da fábrica de implementação retorna um tipoDefaultConsole
com o conjuntoIsEnabled
definido como `true`. - O
IGreetingService
é adicionado com um tipo de implementação correspondente do tipoDefaultGreetingService
. - O
FarewellService
é adicionado como um tipo concreto.
- O
- Compile o
ServiceProvider
partir daServiceCollection
. - Resolva os serviços
IGreetingService
eFarewellService
. - Use os serviços resolvidos para cumprimentar e se despedir de uma pessoa chamada
David
.
Se você atualizar a propriedade IsEnabled
do DefaultConsole
como false
, os métodos Greet
e SayGoodbye
irão omitir a gravação das mensagens resultantes no console. Uma alteração como essa ajuda a demonstrar que o serviço do IConsole
é injetado nos serviços IGreetingService
e FarewellService
como uma dependência que influencia o comportamento dos aplicativos.
Todos esses serviços são registrados como singletons, embora, para essa amostra, funcionem de forma idêntica, independentemente de terem sido registrados como transient ou scoped serviços.
Importante
Nesse exemplo artificial example, os tempos de vida do serviço não são importantes, mas em um aplicativo do mundo real você deve refletir cuidadosamente sobre o tempo de vida de cada serviço.
Executar o aplicativo de exemplo
Para executar a amostra de aplicativo, pressione F5 no Visual Studio, no Visual Studio Code, ou execute o comando dotnet run
no terminal. Quando o aplicativo for concluído, você deverá ver o seguinte resultado:
Hello, David!
Goodbye, David!
Descritores do serviço
As APIs mais comumente usadas para adicionar serviços à ServiceCollection
são métodos de extensão genéricos nomeados de acordo com o tempo de vida, como, por exemplo:
AddSingleton<TService>
AddTransient<TService>
AddScoped<TService>
Esses métodos são métodos de conveniência que criam uma instância de ServiceDescriptor e a adicionam à ServiceCollection
. O ServiceDescriptor
é uma classe simples que descreve um serviço com o respectivo tipo de serviço, tipo de implementação e tempo de vida. Também pode descrever instâncias e fábricas de implementação.
Para cada serviço que você registrou na ServiceCollection
você poderia, em vez disso, ter chamado diretamente o método Add
com uma instância de ServiceDescriptor
. Considere os seguintes exemplos:
services.Add(ServiceDescriptor.Describe(
serviceType: typeof(IConsole),
implementationFactory: static _ => new DefaultConsole
{
IsEnabled = true
},
lifetime: ServiceLifetime.Singleton));
O código anterior equivale a como o serviço do IConsole
foi registrado na ServiceCollection
. O método Add
é usado para adicionar uma instância de ServiceDescriptor
que descreve o serviço do IConsole
. O método estático ServiceDescriptor.Describe
delega a vários construtores de ServiceDescriptor
. Considere o código equivalente para o serviço IGreetingService
:
services.Add(ServiceDescriptor.Describe(
serviceType: typeof(IGreetingService),
implementationType: typeof(DefaultGreetingService),
lifetime: ServiceLifetime.Singleton));
O código anterior descreve o serviço IGreetingService
com o respectivo tipo de serviço, tipo de implementação e tempo de vida. Para terminar, considere o código equivalente para o serviço FarewellService
:
services.Add(ServiceDescriptor.Describe(
serviceType: typeof(FarewellService),
implementationType: typeof(FarewellService),
lifetime: ServiceLifetime.Singleton));
O código anterior descreve o tipo concreto de FarewellService
tanto como os tipos de serviço quanto de implementação. O serviço é registrado como singleton serviço.