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:

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 o name.
  • Chama o método WriteLine na instância IConsole.
  • 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 tipo DefaultConsole com o conjunto IsEnabled definido como `true`.
    • O IGreetingService é adicionado com um tipo de implementação correspondente do tipo DefaultGreetingService.
    • O FarewellService é adicionado como um tipo concreto.
  • Compile o ServiceProvider partir da ServiceCollection.
  • Resolva os serviços IGreetingService e FarewellService.
  • 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.

Confira também