← Назад к вопросам

Что такое Dependency Injection в ASP.NET Core? В чём разница между AddTransient, AddScoped и AddSingleton?

2.3 Middle🔥 201 комментариев
#Dependency Injection и IoC

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI7 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Что такое Dependency Injection в ASP.NET Core?

Dependency Injection (DI, внедрение зависимостей) — это архитектурный паттерн проектирования, реализованный на уровне фреймворка ASP.NET Core, который позволяет реализовать принцип Inversion of Control (инверсия управления). Вместо того чтобы классы самостоятельно создавали свои зависимости (через оператор new), зависимости "внедряются" извне — обычно через конструктор, свойства или методы.

В ASP.NET Core DI является первоклассным гражданином (first-class citizen) — весь фреймворк построен вокруг этого паттерна. Контейнер зависимостей встроен в ядро приложения и регистрируется в классе Program.cs (или Startup.cs в более ранних версиях).

Основные преимущества DI:

  • Слабая связанность (loose coupling): Классы зависят от абстракций (интерфейсов), а не от конкретных реализаций.
  • Упрощение тестирования: Зависимости легко подменяются mock-объектами при модульном тестировании.
  • Управление жизненным циклом: Контейнер контролирует время жизни объектов (transient, scoped, singleton).
  • Улучшение поддерживаемости кода: Централизованная регистрация зависимостей упрощает рефакторинг и конфигурацию.

Базовая регистрация зависимостей:

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Регистрация сервисов в контейнере DI
builder.Services.AddScoped<IMyService, MyService>();
builder.Services.AddTransient<IEmailSender, EmailSender>();
builder.Services.AddSingleton<ICacheService, CacheService>();

var app = builder.Build();

Внедрение через конструктор:

public class MyController : ControllerBase
{
    private readonly IMyService _service;
    
    // Зависимость автоматически разрешается контейнером
    public MyController(IMyService service)
    {
        _service = service;
    }
}

Разница между AddTransient, AddScoped и AddSingleton

Эти три метода определяют время жизни (lifetime) зарегистрированных сервисов в контейнере зависимостей ASP.NET Core. Выбор правильного времени жизни критически важен для корректной работы приложения, особенно в многопользовательской среде.

1. AddTransient (транзитный)

builder.Services.AddTransient<IService, ServiceImplementation>();
  • Новый экземпляр создается при каждом запросе зависимости.
  • Для одного запроса, если несколько классов требуют одну и ту же зависимость, каждый получит отдельный экземпляр.
  • Использование: Легковесные, stateless сервисы без внутреннего состояния. Например, валидаторы, преобразователи данных, простые вычислители.
  • Аналогия: Как одноразовый стаканчик — каждый раз берется новый.

Пример сценария:

// Если два класса запрашивают ITransientService в рамках одного HTTP-запроса
public class A(ITransientService service) { }
public class B(ITransientService service) { }

// Классы A и B получат РАЗНЫЕ экземпляры ITransientService

2. AddScoped (скопированный, в рамках области)

builder.Services.AddScoped<IService, ServiceImplementation>();
  • Один экземпляр создается на область (scope), которая в веб-приложении обычно соответствует одному HTTP-запросу.
  • В рамках одного запроса все потребители получат один и тот же экземпляр.
  • Для разных запросов создаются разные экземпляры.
  • Использование: Сервисы, которые должны сохранять состояние в рамках одного запроса. Например, репозитории с контекстом БД (DbContext в Entity Framework Core), сервисы бизнес-логики.
  • Аналогия: Как полотенце в номере отеля — одно на весь срок проживания (запроса).

Пример сценария:

// В рамках одного HTTP-запроса
public class HomeController(IScopedService service1) { }
public class AccountController(IScopedService service2) { }

// Оба контроллера получат ОДИН И ТОТ ЖЕ экземпляр IScopedService для данного запроса
// Но для следующего HTTP-запроса будет создан новый экземпляр

3. AddSingleton (одиночный)

builder.Services.AddSingleton<IService, ServiceImplementation>();
  • Единственный экземпляр создается при первом запросе и существует на протяжении всего времени жизни приложения.
  • Все запросы и все потребители используют один и тот же экземпляр.
  • Требует осторожности: Должен быть потокобезопасным (thread-safe), так как к нему могут обращаться одновременно несколько запросов.
  • Использование: Кеши, конфигурации, логгеры, сервисы подключения к внешним API (где нужно сохранять состояние).
  • Аналогия: Как общий холодильник на кухне — один на всех и всегда доступен.

Пример сценария:

// Глобальный кеш-сервис
public class CacheService : ICacheService
{
    private readonly ConcurrentDictionary<string, object> _cache = new();
    
    public void Set(string key, object value) => _cache[key] = value;
    public object Get(string key) => _cache.GetValueOrDefault(key);
}

// Регистрация
builder.Services.AddSingleton<ICacheService, CacheService>();

// Все контроллеры во всех запросах будут использовать один экземпляр CacheService

Важные особенности и предупреждения

Потокобезопасность

  • Singleton сервисы должны быть спроектированы как потокобезопасные, так как к ним возможен одновременный доступ из множества потоков (HTTP-запросов).
  • Scoped и Transient сервисы обычно не требуют такой тщательной синхронизации, так как каждый запрос/потребитель работает со своим экземпляром.

Disposable объекты

  • Контейнер DI автоматически вызывает Dispose() для Scoped и Singleton сервисов, реализующих IDisposable.
  • Для Transient сервисов, реализующих IDisposable, контейнер также вызывает Dispose(), но это происходит только при уничтожении контейнера (чаще всего при завершении приложения).

Регистрация экземпляра

// Явная регистрация существующего экземпляра как Singleton
var instance = new MyService();
builder.Services.AddSingleton<IMyService>(instance);

Цепочки зависимостей

Время жизни зависимостей должно быть согласованным:

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

Практические рекомендации

  1. По умолчанию используйте AddScoped для сервисов прикладного уровня (репозитории, доменные сервисы).
  2. Используйте AddTransient для простых, быстрых операций без состояния.
  3. Используйте AddSingleton для действительно глобальных ресурсов, но помните о потокобезопасности.
  4. Тестируйте поведение в многопоточных сценариях, особенно для Singleton-сервисов.
  5. Избегайте захвата Scoped-сервисов в Singleton через замыкания или фоновые задачи.

Правильный выбор времени жизни сервиса — ключ к созданию производительных, надежных и легко поддерживаемых приложений на ASP.NET Core. DI контейнер фреймворка предоставляет гибкие механизмы для управления зависимостями, но требует понимания последствий каждого выбора.