Avatar ·

В чем разница между AddTransient, AddScoped и AddSingleton в ASP.NET Core?

Всем привет! Я новичок в ASP.NET Core и пытаюсь разобраться с внедрением зависимостей (Dependency Injection). В Startup.cs я часто вижу три метода: AddTransient, AddScoped и AddSingleton. Я читал документацию, но до конца не понимаю, когда какой использовать. Например, у меня есть простой сервис для генерации случайных чисел: ```csharp public interface IRandomService { int GetRandomNumber(); } public class RandomService : IRandomService { private readonly int _number; public RandomService() { _number = new Random().Next(1, 1000); Console.WriteLine($"Создан экземпляр RandomService с числом {_number}"); } public int GetRandomNumber() => _number; } ``` Я регистрирую его в Startup.cs так: ```csharp services.AddTransient(); // или services.AddScoped(); // или services.AddSingleton(); ``` Когда я вызываю этот сервис в контроллере несколько раз за один запрос, я получаю разные числа. А при разных запросах — иногда одинаковые, иногда разные. Я совсем запутался. Объясните, пожалуйста, на пальцах, чем отличаются эти три варианта и когда какой применять. Заранее спасибо! <<>>

Краткая суть различий

Эти три метода определяют время жизни (lifetime) создаваемых объектов. Проще всего запомнить так:

  • Transient — каждый раз новый объект (одноразовый).
  • Scoped — один объект на один HTTP-запрос.
  • Singleton — один объект на всё приложение (один раз при запуске).

Подробное объяснение с примерами

Давайте разберем ваш пример с RandomService на практике. Представьте, что у нас есть контроллер, который использует этот сервис дважды в одном запросе:

public class TestController : Controller
{
    private readonly IRandomService _service1;
    private readonly IRandomService _service2;

    public TestController(IRandomService service1, IRandomService service2)
    {
        _service1 = service1;
        _service2 = service2;
    }

    public IActionResult Index()
    {
        ViewBag.Num1 = _service1.GetRandomNumber();
        ViewBag.Num2 = _service2.GetRandomNumber();
        return View();
    }
}

1. AddTransient (каждый раз новый)

services.AddTransient<IRandomService, RandomService>();

При каждом обращении к сервису создается новый экземпляр. Даже в рамках одного HTTP-запроса, если вы запрашиваете сервис дважды — получите два разных объекта с разными числами. ViewBag.Num1 и ViewBag.Num2 будут разными.

Когда использовать: для легковесных сервисов без состояния (stateless), которые не хранят данные между вызовами. Например, сервисы для отправки email, хеширования паролей, логирования.

2. AddScoped (один на запрос)

services.AddScoped<IRandomService, RandomService>();

В рамках одного HTTP-запроса создается один экземпляр. Если вы запрашиваете сервис дважды в одном запросе — получите один и тот же объект с одним числом. ViewBag.Num1 и ViewBag.Num2 будут одинаковыми. Но при новом запросе (например, обновлении страницы) создастся новый экземпляр с другим числом.

Когда использовать: для сервисов, которые должны разделять состояние в рамках одного запроса. Например, DbContext в Entity Framework Core, работа с текущим пользователем (IHttpContextAccessor), Unit of Work.

3. AddSingleton (один на всё приложение)

services.AddSingleton<IRandomService, RandomService>();

Создается один экземпляр при первом запросе (или при запуске, если указать AddSingleton<IRandomService, RandomService>(new RandomService())). Этот экземпляр живет, пока работает приложение. Все запросы и все пользователи получают один и тот же объект. В вашем примере число будет одинаковым для всех вызовов и всех запросов.

Когда использовать: для сервисов, которые должны быть общими для всего приложения и не зависеть от запроса. Например, кэширование данных, конфигурация, логирование (если оно не должно менять контекст), сервисы для работы с внешними API (один HttpClient).

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

  • Не используйте Singleton для Scoped-сервисов. Если Singleton-сервис зависит от Scoped-сервиса, то Scoped-сервис станет фактически Singleton (потому что будет захвачен в замыкание). Это частая ошибка — "captive dependency".
  • Не используйте Transient для тяжелых объектов. Если создание объекта дорогое (например, загрузка больших данных), Transient создаст его много раз, что убьет производительность.
  • Singleton не потокобезопасен по умолчанию. Если Singleton хранит изменяемое состояние, нужно синхронизировать доступ (lock, ConcurrentDictionary и т.д.).

Итоговая таблица

  • Transient: Новый объект каждый раз. Для легковесных stateless-сервисов.
  • Scoped: Один объект на запрос. Для сервисов с состоянием в рамках запроса.
  • Singleton: Один объект на всё приложение. Для общих, потокобезопасных сервисов.
<<>>

Дополнение: как проверить время жизни на практике

Чтобы наглядно убедиться в разнице, добавьте в ваш сервис вывод времени создания и уникальный ID:

public class RandomService : IRandomService
{
    private readonly int _number;
    private readonly Guid _id;
    
    public RandomService()
    {
        _id = Guid.NewGuid();
        _number = new Random().Next(1, 1000);
        Console.WriteLine($"[{_id}] Создан в {DateTime.Now:HH:mm:ss.fff}");
    }
    
    public int GetRandomNumber() => _number;
    public Guid GetId() => _id;
}

Теперь в контроллере выведите ID обоих экземпляров. Вы увидите:

  • Transient: разные GUID даже в одном запросе.
  • Scoped: одинаковые GUID в одном запросе, разные — в разных.
  • Singleton: одинаковые GUID всегда.

Еще один важный нюанс: если вы регистрируете сервис как Singleton, но передаете его в контроллер через конструктор, то контроллер сам создается как Scoped (по умолчанию). Это нормально — контроллеры живут один запрос, а Singleton живет вечно. Проблемы начинаются, когда вы внедряете Scoped-сервис в Singleton — этого делать нельзя.

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

services.AddScoped<IMyService>(provider => 
{
    var config = provider.GetRequiredService<IConfiguration>();
    return new MyService(config["SomeKey"]);
});

Это дает полный контроль над созданием объекта.