Avatar ·

Разница между AddTransient, AddScoped и AddSingleton в ASP.NET Core: когда и что использовать?

Всем привет! Я недавно начал изучать ASP.NET Core и встроенный механизм внедрения зависимостей (DI). Вроде бы разобрался с регистрацией сервисов в `ConfigureServices`, но меня сбивает с толку выбор времени жизни сервиса. У меня есть простой пример. Допустим, я пишу приложение для интернет-магазина, и у меня есть сервис `CartService`, который управляет корзиной пользователя, и `LoggerService`, который логирует действия. В стартапе я регистрирую их так: ```csharp public void ConfigureServices(IServiceCollection services) { services.AddTransient(); services.AddScoped(); services.AddSingleton(); } ``` Я не до конца понимаю разницу между `AddTransient`, `AddScoped` и `AddSingleton`. В интернете много объяснений, но они часто слишком абстрактны. Мои вопросы: 1. Что произойдет, если я ошибусь с выбором времени жизни? Например, зарегистрирую сервис для работы с БД как Singleton? 2. В каких сценариях нужно использовать каждый из этих типов? 3. Есть ли какие-то подводные камни или проблемы с памятью/потокобезопасностью? Буду благодарен за подробное объяснение с примерами, желательно на русском. Заранее спасибо! <<>>

Краткий обзор: что такое время жизни сервиса?

В ASP.NET Core время жизни сервиса определяет, как часто будет создаваться новый экземпляр класса при запросе его через DI-контейнер. Это критически важно для корректной работы приложения, производительности и избежания утечек памяти.

1. AddTransient — каждый раз новый экземпляр

Сервисы, зарегистрированные как Transient, создаются каждый раз, когда они запрашиваются из контейнера. Даже в рамках одного HTTP-запроса, если вы запросите transient-сервис три раза, вы получите три разных объекта.

// Пример: сервис для генерации уникальных ID
public class IdGenerator : IIdGenerator
{
    private readonly Guid _id = Guid.NewGuid();
    public Guid GetId() => _id;
}

// Регистрация
services.AddTransient<IIdGenerator, IdGenerator>();

// В контроллере (запрос 1)
public class TestController : Controller
{
    public IActionResult Index([FromServices] IIdGenerator gen1, [FromServices] IIdGenerator gen2)
    {
        // gen1.Id != gen2.Id — разные GUID!
        return Ok();
    }
}

Когда использовать:

  • Легковесные, не имеющие состояния (stateless) сервисы.
  • Сервисы, которые хранят временные данные (например, DTO-мапперы).
  • Когда важна потокобезопасность — каждый поток получает свой экземпляр.

Подводные камни:

  • Создание тяжелых объектов (например, с подключением к БД) как Transient приведет к снижению производительности.
  • Если transient-сервис внедрен в singleton, он станет "захваченным" (captive dependency) и будет жить вечно, как singleton. Это частая ошибка!

2. AddScoped — один экземпляр на HTTP-запрос

Сервисы с временем жизни Scoped создаются один раз для каждого HTTP-запроса (или для каждой области — scope). В рамках одного запроса все обращения к scoped-сервису вернут один и тот же экземпляр.

// Сервис для работы с корзиной
public class CartService : ICartService
{
    private readonly List<Item> _items = new();
    public void AddItem(Item item) => _items.Add(item);
    public IEnumerable<Item> GetItems() => _items;
}

// Регистрация
services.AddScoped<ICartService, CartService>();

// В контроллере
public class CartController : Controller
{
    private readonly ICartService _cart;
    public CartController(ICartService cart) => _cart = cart;

    public IActionResult Add(Item item)
    {
        _cart.AddItem(item); // Работаем с той же корзиной в рамках запроса
        return Ok();
    }
}

Когда использовать:

  • Сервисы, которые должны сохранять состояние в рамках одного запроса (например, корзина, Unit of Work, транзакции БД).
  • DbContext в Entity Framework Core — по умолчанию регистрируется как Scoped.
  • Любые сервисы, где нужна согласованность данных внутри одного запроса.

Подводные камни:

  • Scoped-сервис не должен быть внедрен в singleton — это приведет к тому, что singleton получит первый экземпляр и будет использовать его вечно, игнорируя новые запросы.
  • Нельзя использовать в фоновых задачах (BackgroundService) без создания собственного scope.

3. AddSingleton — один экземпляр для всего приложения

Singleton-сервисы создаются один раз при первом запросе (или при запуске, если используется AddSingleton<T>(new T())) и живут до остановки приложения.

// Конфигурация, которая не меняется
public class AppConfig
{
    public string ConnectionString { get; set; }
    public int MaxRetryCount { get; set; }
}

// Регистрация с готовым объектом
var config = new AppConfig { ConnectionString = "...", MaxRetryCount = 3 };
services.AddSingleton<AppConfig>(config);

// Или через фабрику
services.AddSingleton<ICacheService, MemoryCacheService>();

Когда использовать:

  • Сервисы, которые не хранят состояние, зависящее от запроса (например, кэш, конфигурация, логгеры).
  • Сервисы, которые должны быть общими для всего приложения (например, пул соединений с базой данных).
  • Реализация паттерна "Одиночка" (Singleton) в DI-контейнере.

Подводные камни:

  • Потокобезопасность! Singleton-сервис должен быть либо неизменяемым (immutable), либо правильно синхронизировать доступ к общим данным. Иначе — race conditions.
  • Утечка памяти: если singleton хранит ссылки на объекты, которые больше не нужны, они не будут собраны сборщиком мусора до конца работы приложения.
  • Нельзя внедрять Scoped или Transient сервисы в Singleton — это создаст "захваченные зависимости" (captive dependencies).

Какой вариант выбрать? Практические советы

  • По умолчанию используйте AddTransient для stateless сервисов.
  • Для работы с БД (EF Core DbContext) — AddScoped.
  • Для кэша, конфигурации, логгера — AddSingleton.
  • Избегайте внедрения scoped/transient сервисов в singleton.
  • Если сомневаетесь — начните с Transient. Производительность можно профилировать и оптимизировать позже.
<<>>

Дополнение: как избежать ошибки "Captive Dependency"

Одна из самых частых проблем при работе с DI в ASP.NET Core — это захваченные зависимости (captive dependencies). Она возникает, когда сервис с более долгим временем жизни (например, Singleton) внедряет сервис с более коротким временем жизни (Scoped или Transient).

Пример ошибки:

public class MySingletonService
{
    private readonly MyScopedService _scoped;
    public MySingletonService(MyScopedService scoped)
    {
        _scoped = scoped; // Ошибка! Scoped захвачен Singleton
    }
}

// Регистрация
services.AddSingleton<MySingletonService>();
services.AddScoped<MyScopedService>();

В этом случае MyScopedService будет создан только один раз (при создании Singleton) и будет использоваться для всех запросов, что нарушает его логику (например, DbContext будет работать с устаревшими данными).

Как исправить:

  • Используйте IServiceScopeFactory для создания временного scope внутри Singleton:
public class MySingletonService
{
    private readonly IServiceScopeFactory _scopeFactory;
    public MySingletonService(IServiceScopeFactory scopeFactory)
    {
        _scopeFactory = scopeFactory;
    }

    public void DoWork()
    {
        using (var scope = _scopeFactory.CreateScope())
        {
            var scopedService = scope.ServiceProvider.GetRequiredService<MyScopedService>();
            scopedService.DoWork();
        }
    }
}

Дополнительный совет: Включите проверку в режиме разработки. В ASP.NET Core 6+