Содержание
В отличие от компонентов 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-сервисы — создаются один раз и используется при каждом запросе.