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 classiProgram
oStartup
cui questi metodi di estensione vengono in genere chiamati.
L'app:
- Crea un'istanza 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
ExemplifyScoping
, passando l'oggetto 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 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.