Udostępnij za pośrednictwem


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, IExampleScopedServicei 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 klasach Program lub Startup, 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ż