Содержание
В отличие от компонентов middleware, которые создаются один раз при запуске приложения, зависимости (сервисы) могут иметь различные жизненные циклы. В этой части мы более подробно остановимся на том, что из себя представляет жизненный цикл зависимостей в ASP.NET Core Web API и чем один жизненный цикл отличается от другого.
Жизненный цикл зависимостей в ASP.NET Core
Всего, в ASP.NET Core, в зависимости от жизненного цикла, можно выделить три типа сервисов:
- Transient — такие сервисы создаются каждый раз, когда их запрашивают из контейнера служб. Сервисы с таким жизненным циклом лучше всего подходит для легковесных сервисов без хранения состояния.
- Scoped — применительно к веб-приложениям Scoped-сервис означает, что сервис создается один раз для каждого запроса клиента (подключения).
- Singleton — такой сервис создается один раз и используется при каждом запросе.
Условно, работу сервисов с различными жизненными циклами в ASP.NET Core Web API можно представить следующим образом:
Теперь рассмотрим более подробно то, как влияет жизненный цикл зависимости на её работу.
Singleton
Сервис с таким жизненным циклом создается один раз при первом запросе и каждый последующий запрос будет использовать один и тот же экземпляр. Продемонстрируем работу такого сервиса. Создадим новое приложение ASP.NET Core Web API на основе контроллеров, добавим в проект папку Services и разместим в этой папке следующий сервис:
public interface ITimerService { public string Time { get; set; } } public class SingletonTime : ITimerService { public string Time { get; set; } public SingletonTime() { Time = DateTime.Now.ToString("HH:mm:ss.ffffff"); } }
В файле Program.cs зарегистрируем этот сервис, как singleton:
builder.Services.AddKeyedSingleton<ITimerService, SingletonTime>("singleton");
AddKeyedSingleton
Теперь добавим в папку Controllers новый контроллер:
using Microsoft.AspNetCore.Mvc; using WebApplication3.Services; namespace WebApplication3.Controllers { [Route("api/[controller]")] [ApiController] public class LifecycleController : ControllerBase { [HttpGet] public string Get([FromKeyedServices("singleton")] ITimerService singleton) { return singleton.Time; } } }
Теперь запустим приложение и посмотрим на результат, выполнив в браузере запрос по адресу localhost[:port]/api/Lifecycle
. Сколько бы раз вы не выполняли этот запрос — в браузере будет выведена строка содержащая одно и то же время, например, у меня это
02:17:18.456587
Это и означает, что сервис создался при первом запросе и «живет» пока не будет закрыто приложение. Чтобы дополнительно убедиться в этом, можно запросить этот же сервис в другом контроллере и посмотреть на результат.
Scoped
Жизненный цикл scoped-сервиса в ASP.NET Core ограничен одним запросом пользователя. Это должно для нас означать, что при каждом новом запросе будет создаваться новый экземпляр сервиса и время в браузере должно изменяться. Добавим такую реализацию сервиса:
public class ScopedTime : ITimerService { public string Time { get; set; } public ScopedTime() { Time = DateTime.Now.ToString("HH:mm:ss.ffffff"); } }
Зарегистрируем сервис:
builder.Services.AddKeyedScoped<ITimerService, ScopedTime>("scoped");
в контроллере, для наглядности, изменим код метода Get()
следующим образом:
[HttpGet] public string Get([FromKeyedServices("singleton")] ITimerService singleton, [FromKeyedServices("scoped")] ITimerService scoped) { return $"Singleton: {singleton.Time}\nScoped: {scoped.Time}"; }
снова запустим приложение и посмотрим на результат двух последовательных запросов. Первый запрос вернет результат:
Singleton: 02:29:39.610928
Scoped: 02:29:39.620370
несколько мс было затрачено на запрос сервиса из контейнера DI, поэтому время для Singleton и Scoped не совпали. Но это и не столь важно для примера. Второй запрос:
Singleton: 02:29:39.610928
Scoped: 02:31:13.044054
Если время у singleton-сервиса не поменялось, то у scoped — он стало другим. Перейдем к третьему варианту — Transient.
Transient
Сервис с таким жизненным циклом создается при каждом его запросе из контейнера DI. И, возможно, что именно в таком подходе кроется проблема сразу разобраться в том, чем отличается scoped-сервис от transient-сервиса. Создадим третью реализацию сервиса и зарегистрируем её в контейнере DI:
public class TransientTime : ITimerService { public string Time { get; set; } public TransientTime() { Time = DateTime.Now.ToString("HH:mm:ss.ffffff"); } }
builder.Services.AddKeyedTransient<ITimerService, TransientTime>("transient");
Чтобы увидеть различие между scoped- и transient-сервисом нам необходимо сделать так, чтобы оба эти сервиса запрашивались из контейнера DI несколько раз в рамках одного запроса пользователя. В этом случае — экземпляр scoped- сервиса должен остаться тем же, а экземпляров transient-сервиса мы должны получить два. Перепишем метод Get()
контроллера следующим образом:
[HttpGet] public async Task<string> Get([FromKeyedServices("singleton")] ITimerService singleton) { var firstScoped = HttpContext.RequestServices.GetRequiredKeyedService<ITimerService>("scoped"); string res = $" First scoped: {firstScoped.Time}"; await Task.Delay(1000); var secondScoped = HttpContext.RequestServices.GetRequiredKeyedService<ITimerService>("scoped");//здесь время должно остаться тем же res = $"{res}\n Second scoped: {secondScoped.Time}"; await Task.Delay(1000); var firstTransient = HttpContext.RequestServices.GetRequiredKeyedService<ITimerService>("transient"); res = $"{res}\n First transient: {firstTransient.Time}"; await Task.Delay(1000); var secondTransient = HttpContext.RequestServices.GetRequiredKeyedService<ITimerService>("transient");//здесь время должно поменяться res = $"{res}\n Second transient: {secondTransient.Time}"; return res ; }
Здесь, для демонстрации различий, запрос сервиса осуществляется с использованием service locator. Запустим приложение и посмотрим на результат:
First scoped: 03:11:24.304557 Second scoped: 03:11:24.304557 First transient: 03:11:26.322593 Second transient: 03:11:27.335615
Как и ожидалось, при двух запросах scoped-сервиса в рамках одного запроса нам возвращается один и тот же экземпляр сервиса о чем свидетельствует строка времени, которая совпадает полностью. В свою очередь, transient-сервисы показывают разное время так как новый экземпляр создается каждый раз, когда происходит запрос зависимости из контейнера DI (в нашем случае — это вызов метода GetRequiredKeyedService()
). В большинстве случае, продемонстрированное различие будет мало заметно, так как работать с сервисами мы, все же, будем более цивилизованными способами, например, запрашивая их в конструкторе контроллера.
В какой момент удаляются сервисы ASP.NET Core?
По умолчанию, сервисы ASP.NET Core удаляются следующим образом:
- singleton — при завершении приложения
- transient и scoped — удаляются в конце запроса.
Методы регистрации сервисов с различными жизненными циклами
Чтобы зарегистрировать ту или иную зависимость в контейнере DI и указать её жизненный цикли используются методы, имеющие следующий шаблон:
Add[lifetime]
AddKeyed[lifetime]
где lifitime
— жизненный цикл зависимости (Transient, Scoped, Singleton). Версия AddKeyed[lifitime]
используется для регистрации сервисов с ключами и доступна в .NET не ниже версии .NET 8. Кроме этого, каждый метод регистрации сервиса имеет ряд перегруженных версий. Так, например, метод AddTransient()
имеет следующие версии:
AddTransient(Type serviceType)
AddTransient(Type serviceType, Type implementationType)
AddTransient(Type serviceType, Func<IServiceProvider,object> implementationFactory)
AddTransient<TService>()
AddTransient<TService, TImplementation>()
AddTransient<TService>(Func<IServiceProvider,TService> implementationFactory)
AddTransient<TService, TImplementation>(Func<IServiceProvider,TImplementation> implementationFactory)
Итого
Сервисы ASP.NET Core могут регистрироваться в контейнере DI с различными сроками жизни (жизненным циклом): Transient-сервисы создаются каждый раз, когда их запрашивают из контейнера служб, Scoped-сервисы — применительно к веб-приложениям, создаются один раз для каждого запроса клиента (подключения), Singleton-сервисы — создаются один раз и используется при каждом запросе.