Samouczek: używanie wstrzykiwania zależności na platformie .NET
W tym samouczku pokazano, jak używać iniekcji zależności (DI) na platformie .NET. W przypadku rozszerzenia firmy Microsoft, DI jest zarządzane przez dodawanie usług i konfigurowanie ich w IServiceCollection. Interfejs IHost uwidacznia wystąpienie IServiceProvider, które działa jako kontener wszystkich zarejestrowanych usług.
Z tego samouczka dowiesz się, jak wykonywać następujące działania:
- Tworzenie aplikacji konsolowej platformy .NET korzystającej z wstrzykiwania zależności
- Tworzenie i konfigurowanie hosta ogólnego
- Pisanie kilku interfejsów i odpowiednich implementacji
- Używanie okresu istnienia usługi i określania zakresu dla di
Warunki wstępne
- zestaw SDK platformy .NET Core 3.1 lub nowszy.
- Znajomość tworzenia nowych aplikacji .NET i instalowania pakietów NuGet.
Tworzenie nowej aplikacji konsolowej
Za pomocą polecenia dotnet new lub kreatora nowego projektu IDE utwórz nową aplikację konsolową platformy .NET o nazwie ConsoleDI.Example. Dodaj pakiet NuGet Microsoft.Extensions.Hosting do projektu.
Nowy plik projektu aplikacji konsolowej powinien wyglądać podobnie do następującego:
<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.2" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.2" />
</ItemGroup>
</Project>
Ważny
W tym przykładzie do skompilowania i uruchomienia aplikacji jest wymagany pakiet Microsoft.Extensions.Hosting NuGet. W przypadku niektórych metapakietów pakiet Microsoft.Extensions.Hosting
może być zawarty, w takim przypadku jawne odwołanie do pakietu nie jest wymagane.
Dodawanie interfejsów
W tej przykładowej aplikacji dowiesz się, jak wstrzykiwanie zależności obsługuje cykl życia usługi. Utworzysz kilka interfejsów reprezentujących różne okresy istnienia usługi. Dodaj następujące interfejsy do katalogu głównego projektu:
IReportServiceLifetime.cs
using Microsoft.Extensions.DependencyInjection;
namespace ConsoleDI.Example;
public interface IReportServiceLifetime
{
Guid Id { get; }
ServiceLifetime Lifetime { get; }
}
Interfejs IReportServiceLifetime
definiuje:
- Właściwość
Guid Id
reprezentująca unikatowy identyfikator usługi. - Właściwość ServiceLifetime reprezentująca okres istnienia usługi.
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;
}
Wszystkie podpowierzchniki IReportServiceLifetime
jawnie implementują IReportServiceLifetime.Lifetime
z ustawieniem domyślnym. Na przykład IExampleTransientService
jawnie implementuje IReportServiceLifetime.Lifetime
przy użyciu wartości ServiceLifetime.Transient
.
Dodawanie implementacji domyślnych
W przykładowych implementacjach wszystkie inicjują swoją właściwość Id
z wynikiem Guid.NewGuid(). Dodaj następujące domyślne klasy implementacji dla różnych usług do katalogu głównego projektu:
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();
}
Każda implementacja jest definiowana jako internal sealed
i implementuje odpowiedni interfejs. Nie ma obowiązku stosowania internal
lub sealed
, jednak często traktuje się implementacje jako internal
, aby uniknąć ujawniania typów implementacji odbiorcom zewnętrznym. Ponadto, ponieważ każdy typ nie zostanie rozszerzony, zostanie oznaczony jako sealed
. Na przykład ExampleSingletonService
implementuje IExampleSingletonService
.
Dodaj usługę wymagającą DI
Dodaj następującą klasę raportującą okres istnienia usługi, która działa jako usługa w aplikacji konsolowej:
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
definiuje konstruktor, który wymaga wszystkich wymienionych wcześniej interfejsów usług, czyli IExampleTransientService
, IExampleScopedService
i IExampleSingletonService
. Obiekt udostępnia pojedynczą metodę, która umożliwia użytkownikowi raportowanie o usłudze z użyciem danego parametru lifetimeDetails
. Po wywołaniu metoda ReportServiceLifetimeDetails
rejestruje unikatowy identyfikator każdej usługi z komunikatem o okresie istnienia usługi. Komunikaty dziennika ułatwiają wizualizowanie okresu istnienia usługi.
Rejestracja usług dla DI
Zaktualizuj Program.cs przy użyciu następującego kodu:
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();
}
Każda metoda rozszerzenia services.Add{LIFETIME}<{SERVICE}>
dodaje (i potencjalnie konfiguruje) usługi. Zalecamy, aby aplikacje przestrzegały tej konwencji. Nie umieszczaj metod rozszerzeń w przestrzeni nazw Microsoft.Extensions.DependencyInjection, chyba że tworzysz oficjalny pakiet firmy Microsoft. Metody rozszerzeń zdefiniowane w przestrzeni nazw Microsoft.Extensions.DependencyInjection
:
- Są wyświetlane w IntelliSense bez konieczności stosowania dodatkowych dyrektyw
using
. - Zmniejsz liczbę wymaganych dyrektyw
using
w klasachProgram
lubStartup
, w których te metody rozszerzenia są zwykle wywoływane.
Aplikacja:
- Tworzy wystąpienie IHostBuilder z ustawieniami konstruktora hosta .
- Konfiguruje usługi i dodaje je z odpowiednim okresem istnienia usługi.
- Wywołuje Build() i przypisuje wystąpienie IHost.
- Wywołuje
ExemplifyScoping
, przekazując IHost.Services.
Konkluzja
W tej przykładowej aplikacji utworzono kilka interfejsów i odpowiednich implementacji. Każda z tych usług jest jednoznacznie identyfikowana i sparowana z ServiceLifetime. Przykładowa aplikacja demonstruje rejestrowanie implementacji usług względem interfejsu oraz sposób rejestrowania czystych klas bez tworzenia kopii zapasowych interfejsów. Przykładowa aplikacja pokazuje następnie, jak zależności zdefiniowane jako parametry konstruktora są rozpoznawane w czasie wykonywania.
Po uruchomieniu aplikacji zostaną wyświetlone dane wyjściowe podobne do następujących:
// 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)
Z danych wyjściowych aplikacji można zobaczyć, że:
- Transient usługi są zawsze inne, przy każdym pobraniu usługi tworzona jest nowa instancja.
- Scoped usługi zmieniają się tylko z nowym zakresem, ale w ramach zakresu są tym samym wystąpieniem.
- Singleton usługi są zawsze takie same, nowa instancja jest tworzona tylko jeden raz.
Zobacz też
- Wskazówki dotyczące wstrzykiwania zależności
- Omówienie podstaw iniekcji zależności na platformie .NET
- iniekcja zależności w Core ASP.NET Core