#c# #asp.net-core #dependency-injection #repository-pattern
#c# #asp.net-core #внедрение зависимостей #репозиторий-шаблон
Вопрос:
У меня есть служба временного репозитория, и мне нужно создавать область служб каждый раз, когда я ее вызываю.
Я попытался создать эту область в конструкторе репозитория следующим образом:
public class ServiceRepository : IServiceRepository
{
private IServiceScopeFactory _serviceScopeFactory;
private IServiceScope _scope;
private IServiceProvider _serviceContainer;
private DataBaseContext _db;
public ServiceRepository(DataBaseContext context, IServiceScopeFactory serviceScopeFactory)
{
_db = context;
_serviceScopeFactory = serviceScopeFactory;
_scope = _serviceScopeFactory.CreateScope();
_serviceContainer = _scope.ServiceProvider;
}
и после этого я попытался вызвать свою службу репозитория от поставщика услуг:
var serviceRepository = _serviceProvider.GetRequiredService<IServiceRepository>();
Я ожидаю, что каждый раз, когда я вызываю эту службу таким образом, будет создаваться область служб, которую я объявил в конструкторе репозитория. Но при доступе к службе я получаю сообщение об ошибке:
System.InvalidOperationException: 'Cannot resolve 'Data_Access_Layer.Interfaces.IServiceRepository' from root provider because it requires scoped service 'Data_Access_Layer.EF.DataBaseContext'.'
Что я делаю не так? раньше я устанавливал область следующим образом, и это работало:
var scopeFactory = _serviceProvider.GetService<IServiceScopeFactory>();
var scope = scopeFactory.CreateScope();
var scopedContainer = scope.ServiceProvider;
Но в этом случае мне нужно объявлять область действия каждый раз, прежде чем я вызываю IServiceRepository. Вот почему я хочу объявить область в конструкторе IServiceRepository.
Ответ №1:
Вы используете все это неправильно. Во-первых, объекты с временным сроком службы могут быть введены с помощью служб с ограниченным сроком службы напрямую. Вы должны не вводить IServiceProvider
or IServiceScopeFactory
и т.д., А, скорее, ваши фактические зависимости. Вы уже вводите свой контекст напрямую (который является службой с ограниченной областью действия), поэтому я не уверен, почему вы пытаетесь обрабатывать что-либо еще другим способом.
Вы должны вводить IServiceProvider
(ничего другого) только тогда, когда ваш объект имеет время жизни одного элемента и нуждается в службах с ограниченной областью. Это называется антишаблоном service locator, и это антишаблон по определенной причине: вам следует избегать необходимости делать это, насколько это возможно. В общем, большинство из того, что, по мнению людей, должно быть одиночными, на самом деле не должно быть одиночными. Существует лишь несколько случаев, когда вам действительно нужно время жизни одного элемента. Во всех других сценариях «scoped» должно быть вашим текущим сроком службы. Кроме того, если вашему синглтону действительно нужны службы с ограниченной областью, это сильный аргумент в пользу того, что он сам должен быть ограничен.
Однако, если вы действительно окажетесь в ситуации, когда вам действительно нужно время жизни одного элемента и вам все еще нужны службы с ограниченной областью действия, то правильный способ сделать это следующий:
public class MySingletonService
{
private readonly IServiceProvider _provider;
public MySingletonService(IServiceProvider provider)
{
_provider = provider;
}
...
}
И это все. Вы не создаете область внутри конструктора. Любая служба, извлеченная из области, существует только в пределах этой области, и когда область исчезает, служба тоже. Таким образом, вы не можете сохранить службы с ограниченной областью в ivar на синглтоне. Вместо этого внутри каждого отдельного метода, которому требуется такая служба, вам нужно сделать:
using (var scope = _provider.CreateScope())
{
var myScopedService = scope.ServiceProvider.GetRequiredService<MyScopedService>();
// do something with scoped service
}
Это еще одна причина, по которой service locator является антишаблоном: это приводит к большому количеству тупого и повторяющегося кода. Иногда у вас нет выбора, но в большинстве случаев у вас есть.
Комментарии:
1. Хотя это совершенно правильно с точки зрения дизайна DI, это, к сожалению, тот случай, когда «оптимизация производительности часто противоречит абстракциям кода» <= При написании High Performance .NET. «Существует лишь несколько случаев, когда вам действительно требуется время жизни синглтона». <= Вместо этого вы должны рассматривать каждую из ваших служб с ограниченной областью как накладные расходы на сборку мусора и пытаться переместить как можно больше служб с ограниченной областью в Singleton. Если, конечно, вы не работаете на Cray. Тогда все будет хорошо.
2. Полагаю, я должен добавить — как только вы увидите, что на сборку мусора Gen 0 в Azure Production потрачено 3,8 секунды, тогда вы начнете переоценку выделений. Даже недолговечные.
3. Синглтоны имеют / должны иметь специальное назначение и создаваться с таким сроком службы именно потому, что они требуют такого срока службы, то есть поддержания состояния по всей поверхности приложения. Конечно, могут возникнуть затраты на выделение для использования scoped, но это в значительной степени окупается снижением сложности кода и энтропии. Если ваша цель — использовать все по одному элементу, то на самом деле вам даже не следует использовать внедрение зависимостей в первую очередь, и это в любом случае обеспечит вам большую производительность, чем любое попадание из GC.
4. Как можно окупить затраты на 3,8-секундный TTFB? Вы этого не делаете. Это приводит к потере продаж в целом ряде случаев, в том числе — К увеличению показателя отказов — Уменьшению размеров корзины — Оставленным корзинам и т.д. Существует много способов, которыми вы можете провести рефакторинг кода, чтобы перенести то, что в противном случае было бы ограничено DI, в Singleton. Это должно быть таким же важным фактором, если не больше, чем шаблоны проектирования ради шаблонов. «Одноэлементное все» — это просто соломенный человечек. Всегда необходимо интеллектуальное распознавание компромиссов. Но «Охватить все», безусловно, не лучший подход.
5. Если вы думаете, что я ошибаюсь, я попрошу вас прислать мне профилировщик вашего кода, который вы написали таким образом в рабочей среде, и я покажу вам, как ваше предложение отразится на вашем рабочем коде прямо сейчас. Напишите мне для получения более подробной информации. С уважением, Джефф
Ответ №2:
Хотя ответ Криса Пратта дает ответ на вопрос «как мне преобразовать службы с ограниченной областью в синглтон», это не ответ на то, почему вам говорят, что ваш репозиторий не может быть разрешен (по крайней мере, когда проверка области все еще включена).
Что такое корневой провайдер?
Корневой поставщик — это одноэлементный поставщик, который среда выполнения использует для создания одноэлементных служб и всех других служб через области, которые она создает. Его время жизни напрямую связано со временем жизни приложения. Если вы пишете веб-api, корневой провайдер будет существовать до тех пор, пока ваше приложение работает.
Корневой поставщик услуг создается при вызове BuildServiceProvider. Время жизни поставщика корневых служб соответствует времени жизни приложения / сервера, когда поставщик запускается с приложением, и удаляется при завершении работы приложения
Что такое поставщики с ограниченной областью?
Для создания используются поставщики с ограниченной областью… как вы уже догадались, службы с ограниченной областью. Время жизни службы с ограниченной областью привязано к контейнеру, который ее создал.
Службы с ограниченной областью действия удаляются создавшим их контейнером
Область предоставляет вам, разработчику, способ определить время жизни конкретной службы. В веб-проектах создание области обычно обрабатывается конвейером запросов, и это все, что требуется в большинстве сценариев. Фреймворк создает для вас область, когда начинает обрабатывать запрос, и использует поставщика из этой области для внедрения служб. Когда запрос завершен, область удаляется вместе со службами, которыми она управляет. Ручная версия этого, которая существует в большей части документации msdn, была бы следующей:
public void DoScopedWork(IServiceProvider serviceProvider)
{
using (var scope = serviceProvider.CreateScope())
{
var scopedProvider = scope.ServiceProvider;
var myService = scopedProvider.GetService<IMyService>();
myService.DoWork();
}
}
Службы с ограниченной областью от корневого поставщика?
Проверка области включена в средах разработки по умолчанию по какой-то причине, и это одна из тех функций, если-вы-читаете-это, — вы-сделали-что-то-неправильно.
Когда ValidateScopes имеет значение true, поставщик услуг по умолчанию выполняет проверки, чтобы убедиться, что:
- Службы с ограниченной областью не разрешаются прямо или косвенно от корневого поставщика услуг
- Службы с ограниченной областью действия прямо или косвенно не внедряются в синглтоны
Поскольку службы с ограниченной областью действия удаляются создавшим их поставщиком, когда этот поставщик удаляется (выпадает из … области действия?), создание службы с ограниченной областью действия от корневого поставщика эффективно создает синглтон.
Если служба с ограниченной областью создается в корневом контейнере, срок службы службы фактически увеличивается до одноэлементного, поскольку корневой контейнер удаляет ее только при завершении работы приложения / сервера. Проверка областей служб выявляет эти ситуации при вызове BuildServiceProvider.
Проверка существует для этих случаев, потому что при регистрации службы было явно указано, что она должна быть ограничена. Итак, указание фреймворку IRepositoryService
на то, что область действия должна быть ограничена, а затем указание ему также разрешить эту службу с ограниченной областью действия от корневого поставщика, является противоречием в терминах. Конечно, корневой провайдер МОЖЕТ создать эту службу, поэтому опцию проверки областей МОЖНО отключить, но действительно лучше понять, что это может делать с приложением, прежде чем решать, что это правильно.
Но временный, верно?
Нет. Упоминалось, что репозиторий должен быть временным, но это не означает, что мы можем игнорировать область. Временные службы являются новыми каждый раз, когда они запрашиваются у поставщика. Конечно, временная служба, запрошенная у корневого поставщика, НЕ будет одноэлементной, но она будет как новой, так и бессмертной (если вы не избавитесь от нее вручную). Мы можем представить, что произойдет, когда временный репозиторий запрашивается n раз у корневого поставщика, верно?
Почему это проблема для OP
var serviceRepository = _serviceProvider.GetRequiredService<IServiceRepository>();
@Giacomo, это проблема для вас, которая коренится в том, где вы пытаетесь использовать репозиторий. Где бы в вашем коде вы ни пытались разрешить, в вашем репозитории есть место, где для вас не был создан поставщик с ограниченной областью. Вы используете корневого провайдера. Без дополнительного контекста о том, что вы на самом деле делаете с этим репозиторием, или когда в течение срока службы вы это делаете, что я могу сказать, вам, вероятно, сначала нужно создать область using (var scope = _serviceProvider.CreateScope()) { ... }
и использовать поставщика услуг с ограниченной областью для создания вашего репозитория.
Правильно, но не совсем
Технически ваш ответ на ваш вопрос можно считать правильным, но он по-прежнему не объясняет, что происходит. Вместо устранения реальной проблемы он игнорирует предупреждение и позволяет службе с ограниченной областью ( DataBaseContext
) быть переведенной в эффективный синглтон, поскольку она создается корневым поставщиком для внедрения в ваш репозиторий (также эффективный синглтон). Самое большее, что я мог бы сказать в пользу использования options.ValidateScopes = false
в вашем случае, это то, что это обходной путь.
боковое примечание: не рекомендуется разрешать вашему контексту db существовать как одноэлементному
Вы упоминаете, что хотите передать область, в которой должен находиться ваш репозиторий, вашему репозиторию. Вы не можете получить свой пирог и съесть его тоже. Это сценарий типа «куриное яйцо». Создание области, которая должна создавать вашу службу, — это не то, что вы можете сделать в своем конструкторе служб. Вы говорите, что не хотите создавать новую область каждый раз, когда вам нужно использовать свой репозиторий. Для меня это звучит так, как будто ваше приложение было построено вокруг анти шаблона service locator, а не с учетом инверсии управления (что облегчает DI). Если вы настаиваете на продолжении использования service locator, вам следует
- привыкайте использовать
using (var s = _s.CreateScope()){...}
- взломайте свой репозиторий для реализации статического средства доступа, которое создаст для вас область и выполнит ваши действия в ее пределах
(кстати: это такой же антишаблон, как и service locator) - объявляйте все одноэлементным или переходным и ожидайте повсеместных утечек памяти
- используйте
options.ValidateScopes = false
и продолжайте игнорировать предупреждающие знаки
(неявно так же, как вариант 3)
Цитируемая документация
Ответ №3:
I помогает для меня просто добавить .UseDefaultServiceProvider(options => options.ValidateScopes = false)
к BuildWebHos
t в Program.cs
вот так:
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.UseDefaultServiceProvider(options => options.ValidateScopes = false)
.Build();
Я надеюсь, что это тоже будет кому-то полезно