Поделиться через


Руководство. Использование внедрения зависимостей в .NET

В этом руководстве показано, как использовать внедрение зависимостей (DI) в .NET. С помощью расширений Майкрософт, DI управляется путем добавления служб и их настройки в IServiceCollection. Интерфейс IHost предоставляет экземпляр IServiceProvider, который выступает в качестве контейнера всех зарегистрированных служб.

В этом руководстве описано, как:

  • Создание консольного приложения .NET, использующего внедрение зависимостей
  • Создание и настройка универсального узла
  • Создание нескольких интерфейсов и соответствующих реализаций
  • Используйте время жизни службы и область видимости для внедрения зависимостей

Необходимые условия

  • пакет SDK для .NET Core 3.1 или более поздней версии.
  • Знакомство с созданием новых приложений .NET и установкой пакетов NuGet.

Создание консольного приложения

С помощью команды dotnet new или мастера создания нового проекта в IDE создайте консольное приложение .NET с именем ConsoleDI.Example. Добавьте в проект пакет Microsoft.Extensions.Hosting NuGet.

Новый файл проекта консольного приложения должен выглядеть следующим образом:

<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.1" />
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.1" />
  </ItemGroup>

</Project>

Важный

В этом примере для сборки и запуска приложения требуется пакет Microsoft.Extensions.Hosting NuGet. Некоторые метапакеты могут содержать пакет Microsoft.Extensions.Hosting, в этом случае не требуется явная ссылка на пакет.

Добавление интерфейсов

В этом образце приложения вы узнаете, как внедрение зависимости управляет жизненным циклом службы. Вы создадите несколько интерфейсов, представляющих разные сроки существования службы. Добавьте следующие интерфейсы в корневой каталог проекта:

IReportServiceLifetime.cs

using Microsoft.Extensions.DependencyInjection;

namespace ConsoleDI.Example;

public interface IReportServiceLifetime
{
    Guid Id { get; }

    ServiceLifetime Lifetime { get; }
}

Интерфейс IReportServiceLifetime определяет следующее:

  • Свойство Guid Id, представляющее уникальный идентификатор службы.
  • Свойство ServiceLifetime, представляющее время существования службы.

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;
}

Все подинтерфейсы IReportServiceLifetime явно реализуют IReportServiceLifetime.Lifetime по умолчанию. Например, IExampleTransientService явным образом реализует IReportServiceLifetime.Lifetime посредством значения ServiceLifetime.Transient.

Добавление реализаций по умолчанию

Все примеры реализаций инициализируют своё свойство Id с результатом Guid.NewGuid(). Добавьте следующие классы реализации по умолчанию для различных служб в корневой каталог проекта:

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();
}

Каждая реализация определяется как internal sealed и реализует соответствующий интерфейс. Они не обязательно должны быть internal или sealed, однако обычно используются для обработки реализаций как internal, чтобы избежать утечки типов реализаций внешним потребителям. Кроме того, поскольку каждый тип не будет расширен, он помечен как sealed. Например, ExampleSingletonService реализует IExampleSingletonService.

Добавить службу, требующую DI

Добавьте следующий класс аналитика продолжительности службы, который действует как служба в консольном приложении.

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 определяет конструктор, который требует каждого из указанных выше интерфейсов службы, то есть IExampleTransientService, IExampleScopedServiceи IExampleSingletonService. Объект предоставляет один метод, позволяющий потребителю составлять отчет о службе с указанным параметром lifetimeDetails. При вызове метод ReportServiceLifetimeDetails регистрирует уникальный идентификатор каждой службы с сообщением о времени существования службы. Сообщения журнала помогают визуализировать время существования службы.

Регистрация служб для DI

Обновите Program.cs следующим кодом:

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();
}

Каждый метод расширения services.Add{LIFETIME}<{SERVICE}> добавляет (и потенциально настраивает) службы. Мы рекомендуем приложениям следовать этому соглашению. Не размещайте методы расширения в пространстве имен Microsoft.Extensions.DependencyInjection, если вы не создаете официальный пакет Майкрософт. Методы расширения, определенные в пространстве имен Microsoft.Extensions.DependencyInjection:

  • Отображаются в IntelliSense без дополнительных директив using.
  • Уменьшите количество обязательных директив using в классах Program или Startup, где обычно вызываются эти методы расширения.

Приложение:

  • Создает экземпляр IHostBuilder с параметрами построителя хоста .
  • Настраивает службы и добавляет их с соответствующим временем существования службы.
  • Вызывает Build() и присваивает экземпляр IHost.
  • Вызывает ExemplifyScoping, передавая IHost.Services.

Заключение

В этом примере приложения вы создали несколько интерфейсов и соответствующих реализаций. Каждая из этих служб однозначно идентифицируется и связана с ServiceLifetime. В примере приложения демонстрируется регистрация реализаций служб в интерфейсе и регистрация чистых классов без поддержки интерфейсов. Затем в примере приложения показано, как зависимости, определенные как параметры конструктора, разрешаются во время выполнения.

При запуске приложения отображаются выходные данные, аналогичные следующим:

// 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)

В выходных данных приложения вы увидите следующее:

  • Transient сервисы всегда отличаются, создается новый экземпляр с каждым запросом сервиса.
  • Scoped службы изменяются только с новой областью, но являются тем же экземпляром в пределах области.
  • Singleton сервисы всегда одинаковы, новый экземпляр создается лишь один раз.

См. также