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


Основные сведения о внедрении зависимостей в .NET

В этой статье вы создадите консольное приложение .NET, которое вручную создает ServiceCollection и соответствующий ServiceProvider. Вы узнаете, как зарегистрировать службы и устранить их с помощью внедрения зависимостей (DI). В этой статье используется пакет NuGet Microsoft.Extensions.DependencyInjection, чтобы продемонстрировать основы внедрения зависимостей в .NET.

Примечание.

Эта статья не использует возможности Generic Host. Более подробное руководство см. в статье Об использовании внедрения зависимостей в .NET.

Начало работы

Чтобы приступить к работе, создайте консольное приложение .NET с именем DI.Basics. Некоторые из наиболее распространенных подходов к созданию консольного проекта ссылаются в следующем списке:

Необходимо добавить ссылку на пакет Microsoft.Extensions.DependencyInjection в файл проекта. Независимо от подхода, убедитесь, что проект похож на следующий XML-файл DI.Basics.csproj :

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

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.4" />
  </ItemGroup>

</Project>

Основы внедрения зависимостей

Внедрение зависимостей — это шаблон проектирования, позволяющий удалять жестко закодированные зависимости и сделать приложение более управляемым и тестируемым. DI — это метод для достижения инверсии управления (IoC) между классами и их зависимостями.

Абстракции для DI в .NET определяются в пакете NuGet Microsoft.Extensions.DependencyInjection.Abstractions

  • IServiceCollection: определяет контракт для коллекции дескрипторов сервисов.
  • IServiceProvider: определяет механизм получения объекта службы.
  • ServiceDescriptor: описывает службу со своим типом службы, реализацией и временем существования.

В .NET DI управляется путем добавления служб и их настройки в IServiceCollection. После регистрации служб создается экземпляр IServiceProvider с помощью вызова метода BuildServiceProvider. Он IServiceProvider выступает в качестве контейнера всех зарегистрированных служб и используется для разрешения служб.

Создание примеров служб

Не все услуги одинаковы. Для некоторых сервисов требуется новый экземпляр каждый раз, когда контейнер сервиса получает их (транзиентные), а другие должны совместно использоваться между запросами (скопированные) или в течение всего времени существования приложения (синглтон). Дополнительные сведения о времени существования службы см. в разделе "Время существования службы".

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

Создайте файл C# с именем IConsole.cs и добавьте следующий код:

public interface IConsole
{
    void WriteLine(string message);
}

Этот файл определяет IConsole интерфейс, предоставляющий один метод WriteLine. Затем создайте новый файл C# с именем DefaultConsole.cs и добавьте следующий код:

internal sealed class DefaultConsole : IConsole
{
    public bool IsEnabled { get; set; } = true;

    void IConsole.WriteLine(string message)
    {
        if (IsEnabled is false)
        {
            return;
        }

        Console.WriteLine(message);
    }
}

Предыдущий код представляет реализацию IConsole интерфейса по умолчанию. Метод WriteLine выполняет условную запись в консоль в зависимости от свойства IsEnabled.

Совет

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

Затем создайте файл IGreetingService.cs и добавьте следующий код C#:

public interface IGreetingService
{
    string Greet(string name);
}

Затем добавьте новый файл C# с именем DefaultGreetingService.cs и добавьте следующий код:

internal sealed class DefaultGreetingService(
    IConsole console) : IGreetingService
{
    public string Greet(string name)
    {
        var greeting = $"Hello, {name}!";

        console.WriteLine(greeting);

        return greeting;
    }
}

Предыдущий код представляет реализацию IGreetingService интерфейса по умолчанию. Реализация сервиса требует IConsole в качестве основного параметра конструктора. Метод Greet:

  • Создает greeting на основании name.
  • Вызывает метод WriteLine на экземпляре IConsole.
  • Возвращает greeting вызывающему.

Последняя служба для создания — это файл FarewellService.cs , добавьте следующий код C#, прежде чем продолжить:

public class FarewellService(IConsole console)
{
    public string SayGoodbye(string name)
    {
        var farewell = $"Goodbye, {name}!";

        console.WriteLine(farewell);

        return farewell;
    }
}

Элемент FarewellService представляет собой конкретный тип, а не интерфейс. Он должен быть объявлен как public, чтобы сделать его доступным для потребителей. В отличие от других типов реализации служб, объявленных как internal и sealed, этот код демонстрирует, что не все службы должны быть интерфейсами. Кроме того, показано, что реализации служб могут sealed препятствовать наследованию и internal ограничивать доступ к сборке.

Program Обновление класса

Откройте файл Program.cs и замените существующий код следующим кодом C#:

using Microsoft.Extensions.DependencyInjection;

// 1. Create the service collection.
var services = new ServiceCollection();

// 2. Register (add and configure) the services.
services.AddSingleton<IConsole>(
    implementationFactory: static _ => new DefaultConsole
    {
        IsEnabled = true
    });
services.AddSingleton<IGreetingService, DefaultGreetingService>();
services.AddSingleton<FarewellService>();

// 3. Build the service provider from the service collection.
var serviceProvider = services.BuildServiceProvider();

// 4. Resolve the services that you need.
var greetingService = serviceProvider.GetRequiredService<IGreetingService>();
var farewellService = serviceProvider.GetRequiredService<FarewellService>();

// 5. Use the services
var greeting = greetingService.Greet("David");
var farewell = farewellService.SayGoodbye("David");

Предыдущий обновленный код демонстрирует практическое руководство.

  • Создайте новый экземпляр ServiceCollection.
  • Регистрация и настройка служб в ServiceCollection:
    • IConsole с помощью перегрузки фабрики реализации возвращает тип DefaultConsole с параметром IsEnabled, установленным на true.
    • Добавляется IGreetingService с соответствующим типом реализации DefaultGreetingService.
    • FarewellService добавляется в качестве конкретного типа.
  • Соберите ServiceProvider из ServiceCollection.
  • Разрешите службы IGreetingService и FarewellService.
  • Используйте завершенные услуги для приветствия и прощания с человеком по имени David.

Если вы обновите свойство IsEnabled объекта DefaultConsole на false, методы Greet и SayGoodbye не будут записывать результативные сообщения в консоль. Подобное изменение помогает продемонстрировать, что IConsole служба внедряется в и IGreetingService службы как FarewellService, которая влияет на поведение приложений.

Все эти сервисы зарегистрированы как одиночные, хотя для этого примера они работают одинаково, если были зарегистрированы как временные или ограниченные сервисы.

Внимание

В этом приведенном примере время существования службы не имеет значения, но в реальном приложении следует тщательно рассмотреть время существования каждой службы.

Запуск примера приложения

Чтобы запустить пример приложения, нажмите клавишу F5 в Visual Studio, Visual Studio Code или выполните dotnet run команду в терминале. Когда приложение завершится, вы увидите следующие выходные данные:

Hello, David!
Goodbye, David!

Дескрипторы служб

Наиболее часто используемые API для добавления служб в ServiceCollection — это универсальные методы расширения, названные с указанием срока существования, такие как:

  • AddSingleton<TService>
  • AddTransient<TService>
  • AddScoped<TService>

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

Для каждой из зарегистрированных в ServiceCollection служб можно вместо этого напрямую вызвать метод Add с экземпляром ServiceDescriptor. Рассмотрим следующие примеры:

services.Add(ServiceDescriptor.Describe(
    serviceType: typeof(IConsole),
    implementationFactory: static _ => new DefaultConsole
    {
        IsEnabled = true
    },
    lifetime: ServiceLifetime.Singleton));

Предыдущий код эквивалентен тому, как служба IConsole была зарегистрирована в ServiceCollection. Метод Add используется для добавления экземпляра ServiceDescriptor, описывающего службу IConsole. Статический метод ServiceDescriptor.Describe делегирует различным конструкторам ServiceDescriptor. Рассмотрим эквивалентный код для IGreetingService службы:

services.Add(ServiceDescriptor.Describe(
    serviceType: typeof(IGreetingService),
    implementationType: typeof(DefaultGreetingService),
    lifetime: ServiceLifetime.Singleton));

Приведенный IGreetingService выше код описывает службу со своим типом службы, типом реализации и временем существования. Наконец, рассмотрим эквивалентный код для FarewellService службы:

services.Add(ServiceDescriptor.Describe(
    serviceType: typeof(FarewellService),
    implementationType: typeof(FarewellService),
    lifetime: ServiceLifetime.Singleton));

Приведенный выше код описывает конкретный FarewellService тип как службы, так и типы реализации. Сервис зарегистрирован как одиночный сервис.

См. также