Содержание
Понимание жизненного цикла зависимостей в ASP.NET Core (и не только) — довольно сложная, но, одновременно и важная тема. До сих пор мы особенно не вдавались в этот момент, просто тренируясь регистрировать сервисы, получать их различными способами и т.д. Сегодня попробуем разобраться с жизненным циклом зависимости в ASP.NET на примере.
Виды зависимостей ASP.NET Core с точки зрения их жизненного цикла
С точки зрения жизненного цикла, зависимости можно разделить на три группы:
- Transient — новый объект сервиса создается при каждом обращении к нему. В течение одного запроса может быть несколько обращений к сервису, соответственно при каждом обращении будет создаваться новый объект
- Scoped — для каждой области действия создается свой объект сервиса. Например, применительно к ASP.NET Core областью действия может выступать часть middleware. И если в течение одного запроса есть несколько обращений к одному сервису в его области, то при всех этих обращениях будет использоваться один и тот же объект сервиса.
- Singleton -здесь объект сервиса создается при первом обращении к нему и все последующие обращения к сервису используют один и тот же ранее созданный объект.
Чтобы зарегистрировать ту или иную зависимость в контейнере DI и указать её жизненный цикли используются методы, имеющие следующий шаблон Add[lifetime] — где lifetime — это один из трех вариантов, указанных выше. Например, мы во всех ранее используемых примерах использовали метод:
builder.Services.AddTransient<T>();
Все методы Add[lifiteme] имеют ряд перегруженных версий, но, по сути, все они выполняют одну и ту же задачу — зарегистрировать зависимость с заданным жизненным циклом. Разберемся с тем, в чем отличие зависимостей с различными жизненными циклами. Чтобы продемонстрировать работу сервисов — напишем такой сервис:
public interface ILifetimeService
{
public string GetTime();
}
public class LifeTime : ILifetimeService
{
private readonly Stopwatch _stopwatch = Stopwatch.StartNew();
private int cnt = 0;
public LifeTime()
{
Console.WriteLine("Создан новый объект LifeTime");
}
public string GetTime()
{
cnt++;
return $"{cnt} Время работы {_stopwatch.Elapsed}";
}
}
LifeTime возвращает, по-сути, время существования объекта, количество вызовов GetTime, а при создании объекта — выводит в консоль строку о том, что создан новый объект. Теперь посмотрим, как будет вести себя этот сервис с различными жизненными циклами.
AddTransient
Метод AddTransient регистрирует сервис с жизненным циклом Transient, то есть новый объект сервиса создается при каждом обращении к нему. Посмотрим, что это означает для нас:
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddTransient<ILifetimeService, LifeTime>();
var app = builder.Build();
app.Run(async context =>
{
context.Response.ContentType = "text/plain; charset=utf-8";
if (context.Request.Path != "/favicon.ico")
{
var services = app.Services;
var svc = services.GetService<ILifetimeService>();//первое обращение к сервису
var elapsed_1 = svc?.GetTime();
var svc2 = services.GetService<ILifetimeService>();//второе обращение к сервису
var elapsed_2 = svc2?.GetTime();
await context.Response.WriteAsync($"Первое обращение {elapsed_1} \r\n Второе обращение {elapsed_2}");
}
});
app.Run();
}
}
Здесь мы дважды обращаемся к сервису и, согласно описанию выше — сервис должен быть создан дважды. Результат работы приложения:
Как видите, объект был создан дважды, из-за чего и счётчик вызовов GetTime() всегда имел значение 1.
AddSingleton
Метод AddSingleton регистрирует сервис с жизненным циклом Singleton, то есть объект сервиса создается при первом обращении к нему и не уничтожается. Перепишем всего одну строку кода, чтобы продемонстрировать действие сервиса с таким жизненным циклом:
//builder.Services.AddTransient<ILifetimeService, LifeTime>(); builder.Services.AddSingleton<ILifetimeService, LifeTime>();
Теперь запустим приложение и посмотрим на результат. Для примера я несколько раз обновил страницу приложения, чтобы показать, что счётчик количества вызовов метода GetTime() постоянно изменяется:
Как можно видеть Singleton-сервис создался всего один раз при первом обращении к нему и, затем, созданный объект сервиса использовался во всех следующих запросах.
AddScoped
Это, по-видимому, наиболее сложный в понимании вид сервиса. В Сети можно встретить и такие описания, как: «сервис создаётся единожды для каждого запроса«, что, на мой скромный взгляд, является ошибочным пониманием scoped-сервисов. Правильнее сказать, что такой сервис создается единожды для своей области действия. Во-первых, потому, что DI — это не только про ASP.NET и не всегда мы можем оперировать такими понятиями, как «запрос» или «контекст запроса». А вот вторых, один запрос может обслуживаться десятком различных компонентов middleware, а область действия сервиса может ограничиваться какой-то часть middleware.
Чтобы продемонстрировать всё вышесказанное, напишем такой пример:
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<ILifetimeService, LifeTime>();
var app = builder.Build();
app.Run(async context =>
{
context.Response.ContentType = "text/plain; charset=utf-8";
if (context.Request.Path != "/favicon.ico")
{
using (var serviceScope = app.Services.CreateScope())
{
var services = serviceScope.ServiceProvider;
var svc = services.GetService<ILifetimeService>();//первое обращение к сервису
var elapsed_1 = svc?.GetTime();
var svc2 = services.GetService<ILifetimeService>();//второе обращение к сервису
var elapsed_2 = svc2?.GetTime();
await context.Response.WriteAsync($"Первое обращение {elapsed_1} \r\n Второе обращение {elapsed_2} \r\n");
}
using (var serviceScope = app.Services.CreateScope())
{
var services = serviceScope.ServiceProvider;
var svc = services.GetService<ILifetimeService>();//третье обращение к сервису
var elapsed_1 = svc?.GetTime();
var svc2 = services.GetService<ILifetimeService>();//четвертое обращение к сервису
var elapsed_2 = svc2?.GetTime();
await context.Response.WriteAsync($"Третье обращение {elapsed_1} \r\n Четвертое обращение {elapsed_2}");
}
}
});
app.Run();
}
}
}
Здесь за один запрос от пользователя сервис запрашивается четыре раза. Следуя ошибочной логике о том, что scope-сервис живёт от запроса до запроса, мы должны были би увидеть в консоли один вызов конструктора, а счётчик вызовов метода GetTime() в результате должен быть равен 4 сразу после загрузки приложения. Посмотрим на результат:
Объект создан два раза и, следовательно, при каждом создании счётчик вызовов метода обнулялся. Почему так произошло — разберемся ниже. Во-первых, этот пример отличается от предыдущих следующими строками кода:
using (var serviceScope = app.Services.CreateScope())
{
var services = serviceScope.ServiceProvider;
}
Здесь мы создаем область действия нашего сервиса, вызывая метод IServiceProvider.CreateScope() и уже в этой области обращаемся к сервису и работаем с ним:
using (var serviceScope = app.Services.CreateScope())
{
var services = serviceScope.ServiceProvider;
var svc = services.GetService<ILifetimeService>();//первое обращение к сервису
var elapsed_1 = svc?.GetTime();
var svc2 = services.GetService<ILifetimeService>();//второе обращение к сервису
var elapsed_2 = svc2?.GetTime();
await context.Response.WriteAsync($"Первое обращение {elapsed_1} \r\n Второе обращение {elapsed_2} \r\n");
}
Таким образом, если теперь снова посмотреть на пример, то можно увидеть, что в рамках всего одного компонента middleware мы создали две области — отсюда и вывод в консоли (объект создался дважды). Если переписать пример так:
app.Run(async context =>
{
context.Response.ContentType = "text/plain; charset=utf-8";
if (context.Request.Path != "/favicon.ico")
{
using (var serviceScope = app.Services.CreateScope())
{
var services = serviceScope.ServiceProvider;
var svc = services.GetService<ILifetimeService>();//первое обращение к сервису
var elapsed_1 = svc?.GetTime();
var svc2 = services.GetService<ILifetimeService>();//второе обращение к сервису
var elapsed_2 = svc2?.GetTime();
await context.Response.WriteAsync($"Первое обращение {elapsed_1} \r\n Второе обращение {elapsed_2} \r\n");
var svc3 = services.GetService<ILifetimeService>();//третье обращение к сервису
var elapsed_3 = svc3?.GetTime();
var svc4 = services.GetService<ILifetimeService>();//четвертое обращение к сервису
var elapsed_4 = svc4?.GetTime();
await context.Response.WriteAsync($"Третье обращение {elapsed_3} \r\n Четвертое обращение {elapsed_4}");
}
}
});
То есть оставить одну область действия, то объект будет создаваться один раз. При этом, после обновления страницы — фактически отправки нового запроса, объект сервиса будет создаваться заново так как будут заново создаваться области действия (scope). Таким образом, можно выделить три момента работы со scoped-сервисами:
- для такого сервиса обязательно необходимо указывать область его действия
- объект такого сервиса «живет» только в своей области действия. Вышли из области действия — объект «убился».
- запрос — это НЕ область действия scoped-сервиса. Область действия сервиса может охватывать часть одного компонента middleware (см. пример выше), тогда как каждый запрос обрабатывается сразу всем конвейером (набором) компонентов middleware и в каждом компоненте могут быть свои вызовы scoped-сервиса, со своими областями действия и (см. пункт 1) объект сервиса будет создаваться заново для каждой области.
Итого
Сегодня мы разобрались с жизненным циклом сервисов в ASP.NET Core. Выделяются три вида сервисов — transient, scoped и singleton. Чтобы указать время жизни сервиса, при регистрации его в контейнере DI мы должны использовать методы AddTransient, AddScoped или AddSingleton. Для scoped-сервисов обязательно наличие области его действия, которая создается вызовом метода IServiceProvider.CreateScope().

