Esercitazione: usare l'inserimento delle dipendenze in .NET

Questa esercitazione illustra come usare l'inserimento delle dipendenze in .NET. Con le estensioni Microsoft, l'inserimento delle dipendenze viene gestito aggiungendo servizi e configurandoli in un oggetto IServiceCollection. L'interfaccia IHost espone l'istanza IServiceProvider, che funge da contenitore di tutti i servizi registrati.

In questa esercitazione apprenderai a:

  • Creare un'app console .NET che usa l'inserimento delle dipendenze
  • Compilare e configurare un host generico
  • Scrivere diverse interfacce e implementazioni corrispondenti
  • Usare la durata del servizio e la definizione dell'ambito per l'inserimento delle dipendenze

Prerequisiti

  • .NET Core 3.1 SDK o versione successiva
  • Familiarità con la creazione di nuove applicazioni .NET e l'installazione di pacchetti NuGet.

Creare un nuovo progetto di applicazione console

Usando il comando dotnet new o una creazione guidata del nuovo progetto IDE, creare una nuova applicazione console .NET denominata ConsoleDI.Example. Aggiungere il pacchetto NuGet Microsoft.Extensions.Hosting al progetto.

Il nuovo file di progetto dell'app console deve essere simile al seguente:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>true</ImplicitUsings>
    <RootNamespace>ConsoleDI.Example</RootNamespace>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
  </ItemGroup>

</Project>

Importante

In questo esempio, il pacchetto NuGet Microsoft.Extensions.Hosting è necessario per compilare ed eseguire l'app. Alcuni metapacchetti potrebbero contenere il pacchetto Microsoft.Extensions.Hosting, nel qual caso non è necessario un riferimento esplicito al pacchetto.

Aggiungere interfacce

In questa app di esempio, verrà illustrato come l'inserimento delle dipendenze gestisce la durata del servizio. Si creeranno diverse interfacce che rappresentano diverse durate del servizio. Aggiungere le interfacce seguenti alla directory radice del progetto:

IReportServiceLifetime.cs

using Microsoft.Extensions.DependencyInjection;

namespace ConsoleDI.Example;

public interface IReportServiceLifetime
{
    Guid Id { get; }

    ServiceLifetime Lifetime { get; }
}

L'interfaccia IReportServiceLifetime definisce:

  • Proprietà Guid Id che rappresenta l'identificatore univoco del servizio.
  • Proprietà ServiceLifetime che rappresenta la durata del servizio.

IExampleTransientService.cs

using Microsoft.Extensions.DependencyInjection;

namespace ConsoleDI.Example;

public interface IExampleTransientService : IReportServiceLifetime
{
    ServiceLifetime IReportServiceLifetime.Lifetime => ServiceLifetime.Transient;
}

IExampleScopedService.cs

using Microsoft.Extensions.DependencyInjection;

namespace ConsoleDI.Example;

public interface IExampleScopedService : IReportServiceLifetime
{
    ServiceLifetime IReportServiceLifetime.Lifetime => ServiceLifetime.Scoped;
}

IExampleSingletonService.cs

using Microsoft.Extensions.DependencyInjection;

namespace ConsoleDI.Example;

public interface IExampleSingletonService : IReportServiceLifetime
{
    ServiceLifetime IReportServiceLifetime.Lifetime => ServiceLifetime.Singleton;
}

Tutte le sottointerfacce di IReportServiceLifetime implementano in modo esplicito IReportServiceLifetime.Lifetime con un valore predefinito. Ad esempio IExampleTransientService implementa in modo esplicito IReportServiceLifetime.Lifetime con il valore ServiceLifetime.Transient.

Aggiungere implementazioni predefinite

Nell'esempio tutte le implementazioni inizializzano la propria proprietà Id con il risultato di Guid.NewGuid(). Aggiungere le classi di implementazione predefinite seguenti per i vari servizi alla directory radice del progetto:

ExampleTransientService.cs

namespace ConsoleDI.Example;

internal sealed class ExampleTransientService : IExampleTransientService
{
    Guid IReportServiceLifetime.Id { get; } = Guid.NewGuid();
}

ExampleScopedService.cs

namespace ConsoleDI.Example;

internal sealed class ExampleScopedService : IExampleScopedService
{
    Guid IReportServiceLifetime.Id { get; } = Guid.NewGuid();
}

ExampleSingletonService.cs

namespace ConsoleDI.Example;

internal sealed class ExampleSingletonService : IExampleSingletonService
{
    Guid IReportServiceLifetime.Id { get; } = Guid.NewGuid();
}

Ogni implementazione viene definita come internal sealed e implementa l'interfaccia corrispondente. Ad esempio, ExampleSingletonService implementa IExampleSingletonService.

Aggiungere un servizio che richiede l'inserimento delle dipendenze

Aggiungere la classe reporter di durata del servizio seguente, che funge da servizio all'app console:

ServiceLifetimeReporter.cs

namespace ConsoleDI.Example;

internal sealed class ServiceLifetimeReporter(
    IExampleTransientService transientService,
    IExampleScopedService scopedService,
    IExampleSingletonService singletonService)
{
    public void ReportServiceLifetimeDetails(string lifetimeDetails)
    {
        Console.WriteLine(lifetimeDetails);

        LogService(transientService, "Always different");
        LogService(scopedService, "Changes only with lifetime");
        LogService(singletonService, "Always the same");
    }

    private static void LogService<T>(T service, string message)
        where T : IReportServiceLifetime =>
        Console.WriteLine(
            $"    {typeof(T).Name}: {service.Id} ({message})");
}

ServiceLifetimeReporter definisce un costruttore che richiede ognuna delle interfacce di servizio indicate in precedenza, ovvero IExampleTransientService, IExampleScopedService e IExampleSingletonService. L'oggetto espone un singolo metodo che consente al consumer di creare report sul servizio con un determinato parametro lifetimeDetails. Quando viene richiamato, il metodo ReportServiceLifetimeDetails registra l'identificatore univoco di ogni servizio con il messaggio di durata del servizio. I messaggi di log consentono di visualizzare la durata del servizio.

Registrare i servizi per l'inserimento delle dipendenze

Aggiornare Program.cs con il codice seguente:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ConsoleDI.Example;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddTransient<IExampleTransientService, ExampleTransientService>();
builder.Services.AddScoped<IExampleScopedService, ExampleScopedService>();
builder.Services.AddSingleton<IExampleSingletonService, ExampleSingletonService>();
builder.Services.AddTransient<ServiceLifetimeReporter>();

using IHost host = builder.Build();

ExemplifyServiceLifetime(host.Services, "Lifetime 1");
ExemplifyServiceLifetime(host.Services, "Lifetime 2");

await host.RunAsync();

static void ExemplifyServiceLifetime(IServiceProvider hostProvider, string lifetime)
{
    using IServiceScope serviceScope = hostProvider.CreateScope();
    IServiceProvider provider = serviceScope.ServiceProvider;
    ServiceLifetimeReporter logger = provider.GetRequiredService<ServiceLifetimeReporter>();
    logger.ReportServiceLifetimeDetails(
        $"{lifetime}: Call 1 to provider.GetRequiredService<ServiceLifetimeReporter>()");

    Console.WriteLine("...");

    logger = provider.GetRequiredService<ServiceLifetimeReporter>();
    logger.ReportServiceLifetimeDetails(
        $"{lifetime}: Call 2 to provider.GetRequiredService<ServiceLifetimeReporter>()");

    Console.WriteLine();
}

Ogni metodo di estensione services.Add{LIFETIME}<{SERVICE}> aggiunge (e potenzialmente configura) i servizi. È consigliabile che le app seguano questa convenzione. Non inserire i metodi di estensione nello spazio dei nomi Microsoft.Extensions.DependencyInjection a meno che non si stia creando un pacchetto Microsoft ufficiale. Metodi di estensione definiti all'interno dello spazio dei nomi Microsoft.Extensions.DependencyInjection:

  • Vengono visualizzati in IntelliSense senza richiedere blocchi aggiuntivi using.
  • Ridurre il numero di istruzioni necessarie using nelle classi Program o Startup cui questi metodi di estensione vengono in genere chiamati.

L'app:

Conclusione

In questa app di esempio sono state create diverse interfacce e implementazioni corrispondenti. Ognuno di questi servizi viene identificato in modo univoco e associato a un oggetto ServiceLifetime. L'app di esempio illustra la registrazione delle implementazioni del servizio in un'interfaccia e come registrare classi pure senza eseguire il backup delle interfacce. L'app di esempio illustra quindi come vengono risolte le dipendenze definite come parametri del costruttore in fase di esecuzione.

Quando si esegue l'app, viene visualizzato un output simile al seguente:

// Sample output:
// Lifetime 1: Call 1 to provider.GetRequiredService<ServiceLifetimeReporter>()
//     IExampleTransientService: d08a27fa-87d2-4a06-98d7-2773af886125 (Always different)
//     IExampleScopedService: 402c83c9-b4ed-4be1-b78c-86be1b1d908d (Changes only with lifetime)
//     IExampleSingletonService: a61f1ff4-0b14-4508-bd41-21d852484a7b (Always the same)
// ...
// Lifetime 1: Call 2 to provider.GetRequiredService<ServiceLifetimeReporter>()
//     IExampleTransientService: b43d68fb-2c7b-4a9b-8f02-fc507c164326 (Always different)
//     IExampleScopedService: 402c83c9-b4ed-4be1-b78c-86be1b1d908d (Changes only with lifetime)
//     IExampleSingletonService: a61f1ff4-0b14-4508-bd41-21d852484a7b (Always the same)
// 
// Lifetime 2: Call 1 to provider.GetRequiredService<ServiceLifetimeReporter>()
//     IExampleTransientService: f3856b59-ab3f-4bbd-876f-7bab0013d392 (Always different)
//     IExampleScopedService: bba80089-1157-4041-936d-e96d81dd9d1c (Changes only with lifetime)
//     IExampleSingletonService: a61f1ff4-0b14-4508-bd41-21d852484a7b (Always the same)
// ...
// Lifetime 2: Call 2 to provider.GetRequiredService<ServiceLifetimeReporter>()
//     IExampleTransientService: a8015c6a-08cd-4799-9ec3-2f2af9cbbfd2 (Always different)
//     IExampleScopedService: bba80089-1157-4041-936d-e96d81dd9d1c (Changes only with lifetime)
//     IExampleSingletonService: a61f1ff4-0b14-4508-bd41-21d852484a7b (Always the same)

Dall'output dell'app è possibile osservare che:

  • I servizi Transient sono sempre diversi, viene creata una nuova istanza con ogni recupero del servizio.
  • I servizi Scoped cambiano solo con un nuovo ambito, ma sono la stessa istanza all'interno di un ambito.
  • I servizi Singleton sono sempre uguali, una nuova istanza viene creata una sola volta.

Vedi anche