Глубокое погружение во внедрение зависимостей ASP.NET Core
В этой статье мы глубоко погрузимся в внедрение зависимостей в ASP.NET Core и MVC Core. Мы рассмотрим почти все возможные варианты внедрения зависимостей в ваши компоненты.
Внедрение зависимостей лежит в основе ASP.NET Core. Оно позволяет компонентам вашего приложения иметь улучшенную тестируемость. Оно также делает ваши компоненты зависимыми только от некоторого компонента, который может предоставить необходимые сервисы.
В качестве примера приведем интерфейс и реализующий его класс:
public interface IDataService
{
IList<DataClass> GetAll();
}
public class DataService : IDataService
{
public IList<DataClass> GetAll()
{
//Get data...
return data;
}
}
Если другая служба зависит от DataService, они зависят от этой конкретной реализации. Тестирование такой службы может быть намного сложнее. Если же служба зависит от IDataService, их интересует только контракт, предоставляемый интерфейсом. Неважно, какая реализация задана. Это позволяет нам передавать фиктивную реализацию для тестирования поведения службы.
Срок службы
Прежде чем мы сможем поговорить о том, как инъекция выполняется на практике, важно понять , что такое service life . Когда компонент запрашивает другой компонент через инъекцию зависимости, то, является ли полученный им экземпляр уникальным для этого экземпляра компонента или нет, зависит от life. Таким образом, установка life определяет, сколько раз компонент будет инстанцирован, и является ли компонент общим.
Для этого есть 3 варианта с помощью встроенного контейнера DI в ASP.NET Core:
- Singleton
- Scoped
- Transient
Singleton означает, что будет создан только один экземпляр. Этот экземпляр является общим для всех компонентов, которым он необходим. Таким образом, всегда используется один и тот же экземпляр.
Scoped означает, что экземпляр создается один раз для каждой области . Область создается для каждого запроса к приложению, поэтому любые компоненты, зарегистрированные как Scoped, будут создаваться один раз для каждого запроса.
Transient означаеи, что компоненты создаются каждый раз, когда они запрашиваются, и никогда не используются совместно.
Важно понимать, что если вы регистрируете компонент A как синглтон, он не может зависеть от компонентов, зарегистрированных с Scoped или Transient life. Говоря более обобщенно:
Компонент не может зависеть от компонентов, срок службы которых меньше его собственного.
Последствия нарушения этого правила должны быть очевидны: компонент, от которого зависит, может быть утилизирован раньше зависимого.
Обычно вы хотите зарегистрировать компоненты, такие как контейнеры конфигурации всего приложения, как Singleton. Классы доступа к базе данных, такие как контексты Entity Framework, рекомендуется сделать Scoped, чтобы соединение можно было использовать повторно. Хотя, если вы хотите запустить что-либо параллельно, имейте в виду, что контексты Entity Framework не могут совместно использоваться двумя потоками. Если вам это нужно, лучше зарегистрировать контекст как Transient. Тогда каждый компонент получит свой собственный экземпляр контекста и сможет работать параллельно.
Регистрация услуг
Регистрация служб выполняется в ConfigureServices(IServiceCollection)методе вашего Startupкласса.
Вот пример регистрации услуги:
services.Add(new ServiceDescriptor(typeof(IDataService), typeof(DataService), ServiceLifetime.Transient));
Эта строка кода добавляется DataServiceв коллекцию служб. Тип службы установлен на , IDataServiceпоэтому если запрашивается экземпляр этого типа, они получают экземпляр DataService. Время жизни также установлено на Transient , поэтому каждый раз создается новый экземпляр.
ASP.NET Core предоставляет различные методы расширения, упрощающие регистрацию служб с различными сроками существования и другими настройками.
Вот более ранний пример использования метода расширения:
services.AddTransient<IDataService, DataService>();
Немного проще, да? Под крышкой он, конечно, вызывает более раннее, но это просто проще. Существуют похожие методы расширения для разных времен жизни с именами, которые вы, вероятно, можете угадать.
При желании вы также можете зарегистрироваться по одному типу (тип реализации = тип услуги):
services.AddTransient<DataService>();
Но тогда, конечно, компоненты должны зависеть от типа бетона, что может быть нежелательным.
Фабрики по реализации
В некоторых особых случаях вам может понадобиться взять на себя создание экземпляра какой-либо службы. В этом случае вы можете зарегистрировать фабрику реализации в дескрипторе службы. Вот пример:
services.AddTransient<IDataService, DataService>((ctx) =>
{
IOtherService svc = ctx.GetService<IOtherService>();
//IOtherService svc = ctx.GetRequiredService<IOtherService>();
return new DataService(svc);
});
Он создает экземпляр DataServiceс использованием другого компонента IOtherService. Вы можете получить зависимости, зарегистрированные в коллекции служб, с помощью GetService<T>()или GetRequiredService<T>().
Разница в том, что GetService<T>()возвращает , nullесли не может найти службу. GetRequiredService<T>()выдает , InvalidOperationExceptionесли не может найти ее.
Одиночные объекты зарегистрированы как константы
Если вы хотите создать экземпляр синглтона самостоятельно, вы можете сделать это:
services.AddSingleton<IDataService>(new DataService());
Это позволяет реализовать один очень интересный сценарий. Скажем, DataServiceреализует два интерфейса. Если мы сделаем так:
services.AddSingleton<IDataService, DataService>();
services.AddSingleton<ISomeInterface, DataService>();
Мы получаем два экземпляра. Один для обоих интерфейсов. Если мы хотим поделиться экземпляром, вот один из способов сделать это:
var dataService = new DataService();
services.AddSingleton<IDataService>(dataService);
services.AddSingleton<ISomeInterface>(dataService);
Если у компонента есть зависимости, вы можете создать поставщик услуг из коллекции услуг и получить из него необходимые зависимости:
IServiceProvider provider = services.BuildServiceProvider();
IOtherService otherService = provider.GetRequiredService<IOtherService>();
var dataService = new DataService(otherService);
services.AddSingleton<IDataService>(dataService);
services.AddSingleton<ISomeInterface>(dataService);
Обратите внимание, что это следует сделать в конце ConfigureServices, чтобы вы наверняка зарегистрировали все зависимости до этого.
Внедрения
Теперь, когда мы зарегистрировали наши компоненты, мы можем перейти к их фактическому использованию.
Типичный способ внедрения компонентов в ASP.NET Core — внедрение конструктора . Существуют и другие варианты для различных сценариев, но внедрение конструктора позволяет определить, что этот компонент не будет работать без этих других компонентов.
В качестве примера давайте создадим базовый компонент промежуточного программного обеспечения для ведения журнала:
public class LoggingMiddleware
{
private readonly RequestDelegate _next;
public LoggingMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext ctx)
{
Debug.WriteLine("Request starting");
await _next(ctx);
Debug.WriteLine("Request complete");
}
}
Существует три различных способа внедрения компонентов в промежуточное программное обеспечение:
- Конструктор
- Параметр вызова
- HttpContext.RequestServices
Давайте внедрим наш компонент, используя все три:
public class LoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly IDataService _svc;
public LoggingMiddleware(RequestDelegate next, IDataService svc)
{
_next = next;
_svc = svc;
}
public async Task Invoke(HttpContext ctx, IDataService svc2)
{
IDataService svc3 = ctx.RequestServices.GetService<IDataService>();
Debug.WriteLine("Request starting");
await _next(ctx);
Debug.WriteLine("Request complete");
}
}
Промежуточное программное обеспечение создается только один раз в течение жизненного цикла приложения, поэтому компонент, внедренный через конструктор, одинаков для всех проходящих через него запросов .
Компонент, внедренный в качестве параметра, Invokeабсолютно необходим промежуточному программному обеспечению, и оно выдаст исключение, InvalidOperationExceptionесли не сможет найти IDataServiceдля внедрения.
Третий использует RequestServicesсвойство для HttpContextполучения необязательной зависимости с помощью GetService<T>(). Свойство имеет тип IServiceProvider, поэтому оно работает точно так же, как поставщик в фабриках реализации. Если вы хотите потребовать компонент, вы можете использовать GetRequiredService<T>().
Если IDataServiceон был зарегистрирован как синглтон , то во всех них мы получим один и тот же экземпляр.
Если он был зарегистрирован как scoped , svc2то svc3это будет один и тот же экземпляр, но разные запросы получат разные экземпляры.
В случае переходных процессов все они всегда являются разными экземплярами.
Варианты использования каждого подхода:
Конструктор : компоненты Singleton, необходимые для всех запросов.
Параметр вызова : компоненты ограниченной области действия и временные компоненты, которые всегда необходимы в запросах.
RequestServices : компоненты, которые могут понадобиться или не понадобиться на основе информации времени выполнения
Я бы постарался избегать его использования RequestServices, если это возможно, и использовать его только в тех случаях, когда промежуточное программное обеспечение должно иметь возможность функционировать и без какого-либо компонента.
Стартап класс
В конструкторе класса Startup вы можете как минимум внедрить IHostingEnvironmentи ILoggerFactory. Это единственные два интерфейса, упомянутые в официальной документации . Могут быть и другие, но я о них не знаю.
public Startup(IHostingEnvironment env, ILoggerFactory loggerFactory)
{
}
Обычно используется IHostingEnvironmentдля настройки конфигурации приложения. С помощью ILoggerFactoryможно настроить ведение журнала.
Метод Configureпозволяет вводить любые зарегистрированные компоненты.
public void Configure(
IApplicationBuilder app,
IHostingEnvironment env,
ILoggerFactory loggerFactory,
IDataService dataSvc)
{
}
Таким образом, если вам понадобятся какие-либо компоненты во время настройки конвейера, вы можете просто потребовать их там.
Если вы используете app.Run()/ app.Use()/ app.UseWhen()/ app.Map()для регистрации простого промежуточного ПО на конвейере, вы не можете использовать инъекцию конструктора. Фактически, единственный способ получить необходимые вам компоненты — через ApplicationServices/ RequestServices.
Вот несколько примеров:
IDataService dataSvc2 = app.ApplicationServices.GetService<IDataService>();
app.Use((ctx, next) =>
{
IDataService svc = ctx.RequestServices.GetService<IDataService>();
return next();
});
app.Map("/test", subApp =>
{
IDataService svc1 = subApp.ApplicationServices.GetService<IDataService>();
subApp.Run((context =>
{
IDataService svc2 = context.RequestServices.GetService<IDataService>();
return context.Response.WriteAsync("Hello!");
}));
});
app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/test2"), subApp =>
{
IDataService svc1 = subApp.ApplicationServices.GetService<IDataService>();
subApp.Run(ctx =>
{
IDataService svc2 = ctx.RequestServices.GetService<IDataService>();
return ctx.Response.WriteAsync("Hello!");
});
});
Таким образом, вы можете запрашивать компоненты во время настройки через ApplicationServices, IApplicationBuilderа во время запроса RequestServicesчерез HttpContext.
Внедрение в MVC Core
Наиболее распространенным способом внедрения зависимостей в MVC является внедрение через конструктор .
Вы можете сделать это практически где угодно. В контроллерах у вас есть несколько вариантов:
public class HomeController : Controller
{
private readonly IDataService _dataService;
public HomeController(IDataService dataService)
{
_dataService = dataService;
}
[HttpGet]
public IActionResult Index([FromServices] IDataService dataService2)
{
IDataService dataService3 = HttpContext.RequestServices.GetService<IDataService>();
return View();
}
}
Если вы захотите получить зависимости позже на основе решений во время выполнения, вы можете снова использовать RequestServicesavailable для HttpContextсвойства Controllerбазового класса (ну, ControllerBaseтехнически).
Вы также можете внедрять службы, требуемые определенными действиями, добавляя их в качестве параметров и декорируя их с помощью FromServicesAttribute. Это указывает MVC Core получить его из коллекции служб вместо того, чтобы пытаться выполнить привязку модели к нему.
Просмотры бритвы
Вы также можете внедрять компоненты в представления Razor с помощью нового @injectключевого слова:
@using Microsoft.AspNetCore.Mvc.Localization
@inject IViewLocalizer Localizer
Здесь мы внедряем локализатор представления в _ViewImports.cshtml , чтобы он был доступен во всех представлениях как Localizer.
Не следует злоупотреблять этим механизмом для переноса данных в представления, которые должны поступать от контроллера.
Помощники тегов
Внедрение конструктора также работает в помощниках тегов :
[HtmlTargetElement("test")]
public class TestTagHelper : TagHelper
{
private readonly IDataService _dataService;
public TestTagHelper(IDataService dataService)
{
_dataService = dataService;
}
}
Посмотреть компоненты
То же самое с компонентами вида :
public class TestViewComponent : ViewComponent
{
private readonly IDataService _dataService;
public TestViewComponent(IDataService dataService)
{
_dataService = dataService;
}
public async Task<IViewComponentResult> InvokeAsync()
{
return View();
}
}
Компоненты View также имеют HttpContextдоступ к , и, таким образом, имеют доступ к RequestServices.
Фильтры
Фильтры MVC также поддерживают внедрение конструктора, а также имеют доступ к RequestServices:
public class TestActionFilter : ActionFilterAttribute
{
private readonly IDataService _dataService;
public TestActionFilter(IDataService dataService)
{
_dataService = dataService;
}
public override void OnActionExecuting(ActionExecutingContext context)
{
Debug.WriteLine("OnActionExecuting");
}
public override void OnActionExecuted(ActionExecutedContext context)
{
Debug.WriteLine("OnActionExecuted");
}
}
Однако мы не можем добавить атрибут как обычно в контроллер, поскольку он должен получить зависимости во время выполнения.
У нас есть два варианта добавления на уровне контроллера или действия:
[TypeFilter(typeof(TestActionFilter))]
public class HomeController : Controller
{
}
// or
[ServiceFilter(typeof(TestActionFilter))]
public class HomeController : Controller
{
}
Ключевое отличие в том, что он TypeFilterAttributeвыясняет, каковы зависимости фильтров, извлекает их через DI и создает фильтр. ServiceFilterAttributeС другой стороны, он пытается найти фильтр из коллекции сервисов!
Для [ServiceFilter(typeof(TestActionFilter))]работы нам понадобится немного больше настроек:
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<TestActionFilter>();
}
Теперь ServiceFilterAttributeможно найти фильтр.
Если вы хотите добавить фильтр глобально:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(mvc =>
{
mvc.Filters.Add(typeof(TestActionFilter));
});
}
На этот раз нет необходимости добавлять фильтр в коллекцию служб, он работает так, как если бы вы добавили фильтр TypeFilterAttributeна каждый контроллер.
HttpContext
Я HttpContextуже несколько раз упоминал. А что, если вы хотите получить доступ HttpContextза пределами контроллера/представления/компонента представления? Например, чтобы получить доступ к утверждениям текущего вошедшего в систему пользователя?
Вы можете просто ввести IHttpContextAccessor, как здесь:
public class DataService : IDataService
{
private readonly HttpContext _httpContext;
public DataService(IOtherService svc, IHttpContextAccessor contextAccessor)
{
_httpContext = contextAccessor.HttpContext;
}
//...
}
Это позволяет вашему сервисному слою получить доступ к данным, HttpContextне требуя от вас передачи их через каждый вызов метода.
Выводы
Несмотря на то, что контейнер внедрения зависимостей, предоставляемый ASP.NET Core, относительно прост по своим возможностям по сравнению с более крупными и старыми фреймворками DI, такими как Ninject или Autofac, он по-прежнему отлично подходит для большинства задач.
Только полноправные пользователи могут оставлять комментарии. Аутентифицируйтесь пожалуйста, используя сервисы.