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

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

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

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

Предварительные требования

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

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

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

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

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net7.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>true</ImplicitUsings>
    <RootNamespace>ConsoleDI.Example</RootNamespace>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.0" />
  </ItemGroup>

</Project>

Важно!

В этом примере для сборки и запуска приложения требуется пакет NuGet Microsoft.Extensions.Hosting . Некоторые метапакеты могут содержать 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 и реализует соответствующий интерфейс. Например, ExampleSingletonService реализует IExampleSingletonService.

Добавление службы, требующей внедрения зависимостей

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

ServiceLifetimeReporter.cs

namespace ConsoleDI.Example;

internal sealed class ServiceLifetimeReporter
{
    private readonly IExampleTransientService _transientService;
    private readonly IExampleScopedService _scopedService;
    private readonly IExampleSingletonService _singletonService;

    public ServiceLifetimeReporter(
        IExampleTransientService transientService,
        IExampleScopedService scopedService,
        IExampleSingletonService singletonService) =>
        (_transientService, _scopedService, _singletonService) =
            (transientService, scopedService, 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 метод регистрирует уникальный идентификатор каждой службы с сообщением о времени существования службы. Сообщения журнала помогают визуализировать время существования службы.

Регистрация служб для внедрения зависимостей

Обновите файл Program.cs, используя следующий код:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ConsoleDI.Example;

using IHost host = Host.CreateDefaultBuilder(args)
    .ConfigureServices(services =>
    {
        services.AddTransient<IExampleTransientService, ExampleTransientService>();
        services.AddScoped<IExampleScopedService, ExampleScopedService>();
        services.AddSingleton<IExampleSingletonService, ExampleSingletonService>();
        services.AddTransient<ServiceLifetimeReporter>();
    })
    .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<ServiceLifetimeLogger>()");

    Console.WriteLine("...");

    logger = provider.GetRequiredService<ServiceLifetimeReporter>();
    logger.ReportServiceLifetimeDetails(
        $"{lifetime}: Call 2 to provider.GetRequiredService<ServiceLifetimeLogger>()");

    Console.WriteLine();
}

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

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

Приложение:

Заключение

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

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

// Sample output:
// Lifetime 1: Call 1 to provider.GetRequiredService<ServiceLifetimeLogger>()
//     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<ServiceLifetimeLogger>()
//     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<ServiceLifetimeLogger>()
//     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<ServiceLifetimeLogger>()
//     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 службы всегда одинаковы, новый экземпляр создается только один раз.

См. также раздел