Введение
Понимание жизненного цикла внедряемых зависимостей в приложениях ASP.Net Core очень важно. Как мы знаем, внедрение зависимостей (DI - Dependency Injection) - это метод достижения слабой связанности между объектами и их коллабораторами, или зависимостями. Чаще всего классы объявляют свои зависимости через конструктор, в рамках реализации принципа явных зависимостей (Explicit Dependencies Principle). Этот подход известен как «constructor injection».
Чтобы реализовать внедрение зависимостей, нам нужно настроить DI-контейнер с классами, которые участвуют во внедрении зависимостей. DI-контейнер должен решать, возвращать ли новый инстанс сервиса или предоставить уже существующий. Мы выполняем это действие с помощью метода ConfigureServices в классе startup.
Жизненный цикл сервиса зависит от того, когда создается инстанс зависимости и как долго он существует, а также от того, как мы зарегистрировали этот сервис.
Жизненный цикл сервиса определяют следующие три метода:
AddTransient
Transient подразумевает, что сервис создается каждый раз, когда его запрашивают. Этот жизненный цикл лучше всего подходит для легковесных, не фиксирующих состояние, сервисов.AddScoped
Scoped - сервис создаются единожды для каждого запроса.AddSingleton
Singleton - сервис создается при первом запросе (или при запуске ConfigureServices, если вы указываете инстанс там), а затем каждый последующий запрос будет использовать этот же инстанс.
Жизненные циклы внедренных зависимостей на примере
Давайте разберемся с жизненными циклами внедренных зависисмостей на примере ASP.NET Core приложения.
В своем примере я создал три интерфейса с именами ItransientService
, IScopedService
и ISingletonService
- по одному на каждый из типов жизненных циклов внедренных зависимостей. Все эти интерфейсы содержат один единственный метод, именуемый GetOperationID()
, который возвращает уникальный Guid
.
using System;
namespace TransientScopedSingleton {
publicinterfaceITransientService {
Guid GetOperationID();
}
}
using System;
namespace TransientScopedSingleton {
publicinterfaceIScopedService {
Guid GetOperationID();
}
}
using System;
namespace TransientScopedSingleton {
publicinterfaceISingletonService {
Guid GetOperationID();
}
}
Давайте реализуем эти три интерфейса в сервисе OperationService
.
using System;
namespace TransientScopedSingleton {
publicclassOperationService: ITransientService,
IScopedService,
ISingletonService {
Guid id;
publicOperationService() {
id = Guid.NewGuid();
}
public Guid GetOperationID() {
return id;
}
}
}
Теперь зарегистрируем OperationService
через эти три интерфейса, как показано ниже, в методе ConfigureServices
класса startup.
services.AddTransient<ITransientService, OperationService>();
services.AddScoped<IScopedService, OperationService>();
services.AddSingleton<ISingletonService, OperationService>();
Потрясающе! Теперь мы готовы внедрить эти сервисы в контроллер. Для большей наглядности мы внедрим по два экземпляра каждого сервиса через конструктор HomeController.
private readonly ILogger<HomeController> _logger;
private readonly ITransientService _transientService1;
private readonly ITransientService _transientService2;
private readonly IScopedService _scopedService1;
private readonly IScopedService _scopedService2;
private readonly ISingletonService _singletonService1;
private readonly ISingletonService _singletonService2;
public HomeController(ILogger<HomeController> logger,
ITransientService transientService1,
ITransientService transientService2,
IScopedService scopedService1,
IScopedService scopedService2,
ISingletonService singletonService1,
ISingletonService singletonService2)
{
_logger = logger;
_transientService1 = transientService1;
_transientService2 = transientService2;
_scopedService1 = scopedService1;
_scopedService2 = scopedService2;
_singletonService1 = singletonService1;
_singletonService2 = singletonService2;
}
Теперь мы вызовем метод GetOperationID
инстанса каждого сервиса и назначим его viewbag, чтобы мы могли наблюдать эти значения в пользовательском интерфейсе.
public IActionResult Index()
{
ViewBag.transient1 = _transientService1.GetOperationID().ToString();
ViewBag.transient2 = _transientService2.GetOperationID().ToString();
ViewBag.scoped1 = _scopedService1.GetOperationID().ToString();
ViewBag.scoped2 = _scopedService2.GetOperationID().ToString();
ViewBag.singleton1 = _singletonService1.GetOperationID().ToString();
ViewBag.singleton2 = _singletonService2.GetOperationID().ToString();
return View();
}
Изменим представление для отображения идентификаторов соответствующих типов сервисов.
<div class="text-center">
<h2 class="display-4">Dependency Injection Lifetime
</h2>
</div>
<table class="table table-bordered">
<thead>
<tr>
<th>Service Type</th>
<th>First Instance Operation ID</th>
<th>Second Instance Operation ID</th>
</tr>
</thead>
<tbody>
<tr>
<tdstyle="background-color: darksalmon">Transient
</td>
<tdstyle="background-color: darksalmon">@ViewBag.transient1
</td>
<tdstyle="background-color: darksalmon">@ViewBag.transient2
</td>
</tr>
<tr>
<td>Scoped</td>
<td>@ViewBag.scoped1</td>
<td>@ViewBag.scoped2</td>
</tr>
<tr>
<tdstyle="background-color: aquamarine">Singleton
</td>
<tdstyle="background-color: aquamarine">@ViewBag.singleton1
</td>
<tdstyle="background-color: aquamarine">@ViewBag.singleton2
</td>
</tr>
</tbody>
</table>
После того, как мы запустим приложение, мы увидим, что для соответствующих типов сервисов отображаются два отдельных Guid. Теперь запустим два инстанса пользовательского интерфейса в двух разных вкладках браузера, чтобы можно было отправить два независимых запроса.
Запрос 1
Запрос 2
Наблюдения
Transient-сервис всегда возвращает новый инстанс, даже в рамках одного и того же запроса, поэтому идентификаторы операций для первого и второго инстанса различаются у обоих запросов (Запрос 1 и Запрос 2).
В случае Scoped-сервиса на каждый запрос создается по одному инстансу, который затем используется в рамках всего запроса. Вот почему идентификаторы операций одинаковы для первого и второго инстанса Запроса 1. Но если мы обновим страницу или загрузим пользовательский интерфейс в другой вкладке браузера (что представляет собой не что иное, как Запрос 2), мы увидим новый идентификатор.
В случае Singleton-сервиса создается только один инстанс, который совместно используется всеми компонентами приложениями. Если мы обновим страницу или загрузим пользовательский интерфейс на другой вкладке браузера (опять же Запрос 2), мы увидим те же идентификаторы.
Service Тип сервиса | В рамках одного http-запроса | Для двух разных http-запросов |
Transient | Новый инстанс | Новый инстанс |
Scoped | Уже существующий инстанс | Новый инстанс |
Singleton | Уже существующий инстанс | Уже существующий инстанс |
Заключение
Давайте подытожим то, что мы с вами обсудили.
Для transient-сервиса предоставляется новый инстанс каждый раз, когда он запрашивается, независимо от того, происходит ли это в рамках одного и того же HTTP-запроса или в разных.
Для scoped-сервиса мы получаем один и тот же инстанс в рамках одного HTTP-запроса, и разные для разных HTTP-запросов.
Singleton-служба предполагает только один инстанс. Инстанс создается при первом запросе сервиса, и этот единственный инстанс будет использоваться для всех последующих HTTP-запросов во всем приложении.
Спасибо за внимание!
Прямо сейчас в OTUS открыт набор на новый поток курса "C# ASP.NET Core разработчик". Всех желающих приглашаем записаться на demo day курса, в рамках которого вы сможете подробно узнать о программе обучения, а также задать преподавателям вопросы, которые вас интересуют.
AgentFire
Можно было бы убрать ASP.NET, ведь он нужен только ради UI+http-запроса, и показать более абстрактно, как работает
AddScoped
.