ASP.NET Core Web API. Жизненный цикл зависимостей

В отличие от компонентов middleware, которые создаются один раз при запуске приложения, зависимости (сервисы) могут иметь различные жизненные циклы. В этой части мы более подробно остановимся на том, что из себя представляет жизненный цикл зависимостей в ASP.NET Core Web API и чем один жизненный цикл отличается от другого.

Жизненный цикл зависимостей в ASP.NET Core

Всего, в ASP.NET Core, в зависимости от жизненного цикла, можно выделить три типа сервисов:

  1. Transient — такие сервисы создаются каждый раз, когда их запрашивают из контейнера служб. Сервисы с таким жизненным циклом лучше всего подходит для легковесных сервисов без хранения состояния.
  2. Scopedприменительно к веб-приложениям Scoped-сервис означает, что сервис создается один раз для каждого запроса клиента (подключения).
  3. 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");
Так как далее мы будем рассматривать сервисы с другими жизненными циклами, то здесь мы воспользовались возможностью .NET 8 для множественной регистрации сервисов, используя метод 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 удаляются следующим образом:

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

Подписаться
Уведомить о
guest
0 Комментарий
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии