Singleton-объекты и scoped-сервисы

Данное руководство устарело. Актуальное руководство: Руководство по ASP.NET Core 7

Последнее обновление: 07.11.2019

Все объекты, которые используются в ASP.NET Core, имеет три варианта жизненного цикла. Singleton-объекты создаются один раз при запуске приложения, и при всех запросах к приложению оно использует один и тот же singleton-объект. К подобным singleton-объектам относятся, к примеру, компоненты middleware или сервисы, которые регистрируются с помощью метода AddSingleton().

Transient-объекты создаются каждый раз, когда нам требуется экземпляр определенного класса. А scoped-объекты создаются по одному на каждый запрос.

Одни объекты или сервисы с помощью встроенного механизма dependency injection можно передать в другие объекты. Наиболее распространенный способ внедрения объектов предсталяет инъекция через конструктор. Однако начиная с версии ASP.NET Core 2.0 мы не можем передавать scoped-сервисы в конструктор singleton-объектов.

Например, пусть будут опеделены следующие классы:

public interface ITimer
{
	string Time { get; }
}
public class Timer : ITimer
{
	public Timer()
	{
		Time = System.DateTime.Now.ToString("hh:mm:ss");
	}
	public string Time { get; }
}
public class TimeService
{
	private ITimer _timer;
	public TimeService(ITimer timer)
	{
		_timer = timer;
	}
	public string GetTime()
	{
		return _timer.Time;
	}
}

TimeService получает через конструктор сервис ITimer и использует его для получения текущего времени.

Также пусть будет определен компонент middleware TimerMiddleware:

public class TimerMiddleware
{
	private readonly RequestDelegate _next;
	TimeService _timeService;
	public TimerMiddleware(RequestDelegate next, TimeService timeService)
	{
		_next = next;
		_timeService = timeService;
	}

	public async Task Invoke(HttpContext context)
    {
		context.Response.ContentType = "text/html; charset=utf-8";
		await context.Response.WriteAsync($"Текущее время: {_timeService?.GetTime()}");
	}
}

Компонент TimerMiddleware получает сервис TimeService и отправляет в ответ клиенту информацию о текущем времени.

TimerMiddleware является singleton-объектом. И теперь в классе Startup зарегистрируем сервис TimeService как scoped-объект:

public class Startup
{
	public void ConfigureServices(IServiceCollection services)
	{
		services.AddTransient<ITimer, Timer>();
		services.AddScoped<TimeService>();
	}
	public void Configure(IApplicationBuilder app)
	{
		app.UseDeveloperExceptionPage();
        app.UseMiddleware<TimerMiddleware>();
	}
}

Если мы запустим приложение, то мы столкнемся с ошибкой типа "InvalidOperationException: Cannot resolve scoped service 'DIApp.TimeService' from root provider.":

Cannot resolve scoped service from root provider в ASP.NET Core 2

То есть на момент создания объекта TimerMiddleware scoped-сервис TimeService еще не установлен, соответственно он использоваться не может. А без создания объекта TimeService нельзя создать объект TimerMiddleware.

Аналогичная ситуация может возникнуть, если TimeService добавляется как Transient, а сервис ITimer определен как Scoped:

public void ConfigureServices(IServiceCollection services)
{
	services.AddScoped<ITimer, Timer>();
	services.AddTransient<TimeService>();
}

В этом случае для создания объекта TimeService надо получить сервис ITimer, но на момент вызова конструктора TimerMiddleware сервис ITimer еще неопределен:

Cannot resolve from root provider because it requires scoped service

Для выхода из этой ситуации ни TimeService, ни ITimer не должны иметь жизненный цикл Scoped. То есть это может быть Transient или Singleton.

Рассмотрим еще одну ситуацию, с которой можно столкнуться в любой части приложения, а не только в конструкторе middleware, когда сервис TimeService представляет singleton, а ITimer - scoped-объект:

public void ConfigureServices(IServiceCollection services)
{
	services.AddScoped<ITimer, Timer>();
	services.AddSingleton<TimeService>();
}

И, допустим, эти сервисы используются в TimerMiddleware непосредственно при обработке запроса в методе Invoke/InvokeAsync:

public class TimerMiddleware
{
	private readonly RequestDelegate _next;
	public TimerMiddleware(RequestDelegate next)
	{
		_next = next;
	}

	public async Task InvokeAsync(HttpContext context, TimeService timeService)
	{
		context.Response.ContentType = "text/html; charset=utf-8";
		await context.Response.WriteAsync($"Текущее время: {timeService?.GetTime()}");
	}
}

При запуске приложения мы опять же столкнемся с ошибкой, только немного другой "Cannot consume scoped service 'DIApp.ITimer' from singleton 'DIApp.TimeService'"

Cannot consume scoped service from singleton in ASP.NET Core

Но суть будет та же самая - мы не можем по умолчанию передавать в конструктор singleton-объекта scoped-сервис.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850