Condividi tramite


Esercitazione: Usare l'iniezione delle dipendenze in .NET

Questa esercitazione illustra come usare inserimento delle dipendenze in .NET. Con Microsoft Extensions, la Dependency Injection (inserimento delle dipendenze) viene gestita aggiungendo e configurando i servizi in un IServiceCollection. L'interfaccia IHost espone l'istanza di IServiceProvider, che funge da contenitore di tutti i servizi registrati.

In questa esercitazione si apprenderà come:

  • Creare un'app console .NET che usa l'iniezione di dipendenze
  • Creare 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 una nuova applicazione console

Usando il comando dotnet new o una procedura guidata di creazione di un nuovo progetto in 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 dovrebbe 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.Configuration.Binder" Version="9.0.3" />
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.3" />
  </ItemGroup>

</Project>

Importante

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

Aggiungere interfacce

In questa app di esempio si apprenderà 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 il IReportServiceLifetime.Lifetime con un valore predefinito. Ad esempio, IExampleTransientService implementa in modo esplicito IReportServiceLifetime.Lifetime con il valore di ServiceLifetime.Transient.

Aggiungere implementazioni predefinite

Nell'esempio tutte le implementazioni inizializzano la 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. Non è necessario che siano internal o sealed, ma è comune trattare le implementazioni come internal per evitare l'esposizione di tipi di implementazione ai consumatori esterni. Inoltre, poiché ogni tipo non verrà esteso, viene contrassegnato come sealed. Ad esempio, ExampleSingletonService implementa IExampleSingletonService.

Aggiungi un servizio che richiede l'inserimento delle dipendenze (DI)

Aggiungere la seguente classe di reporter del ciclo di vita del servizio, che funge da servizio all'applicazione 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})");
}

Il ServiceLifetimeReporter definisce un costruttore che richiede ognuna delle interfacce di servizio indicate in precedenza, ovvero IExampleTransientService, IExampleScopedServicee IExampleSingletonService. L'oggetto espone un unico metodo che consente all'utilizzatore di redigere un rapporto 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 DI

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 services.Add{LIFETIME}<{SERVICE}> metodo di estensione aggiunge (e potenzialmente configura) 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 ulteriori direttive di using.
  • Ridurre il numero di direttive di using necessarie nelle classi Program o Startup in cui questi metodi di estensione vengono in genere chiamati.

L'app:

  • Crea un'istanza di IHostBuilder con le impostazioni del generatore host .
  • Configura i servizi e li aggiunge con la durata del servizio corrispondente.
  • Chiama Build() e assegna un'istanza di IHost.
  • Chiama ExemplifyScopingpassando il IHost.Services.

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 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:

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

Vedere anche