Содержание
Понимание жизненного цикла зависимостей в 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()
.