Избегайте Распространения DbContext или IQueryable в Приложениях. Окончание
Начало
Когда вы везде передаёте IQueryable, вы позволяете логике доступа к данным распространяться по всей кодовой базе. Нет никакой инкапсуляции логики и правил доступа к данным. Приложение не следует принципу разделения ответственности, поскольку позволяет логике данных «протекать» в каждую часть системы.
Более того, код за пределами уровня данных может даже не знать, что он имеет дело с IQueryable, потому что IQueryable наследует IEnumerable. Методы могут работать с IEnumerable, ожидая поведения только в памяти, но на самом деле манипулируя деревом выражений IQueryable. Это может привести к неожиданным исключениям во время выполнения, особенно если результаты фильтруются с использованием кода, который EF не может преобразовать в SQL (например, пользовательской функции). Страдает понимание кода, а также отладка, устранение неполадок и тестирование. Место IQueryable - на уровне данных. В репозитории допустимо использование IQueryable, при условии, что он не является частью абстракции или публичного интерфейса типа.
Поскольку DbContext или DbSet<T> можно использовать для получения IQueryable в любое время, передача их за пределы уровня данных имеет такое же (негативное) влияние на разделение ответственности и инкапсуляцию, что и передача IQueryable<T>.
Какая есть альтернатива?
Во-первых, вы можно передавать дерево выражений в сервис данных и использовать его:
public IOrderRepositoryВы создаёте фильтр заранее, он исполняется в сервисе данных, а возвращаемый результат – данные в памяти.
{
List<Order> List(
Expression<Func<Order,bool>> filter);
}
Ещё лучше использовать паттерн Спецификация. В этом случае вместо передачи LINQ-выражения вы передаёте экземпляр спецификации, а выражение строится в ней:
public IOrderRepositoryВ вызывающем коде вы определяете необходимую спецификацию как часть модели предметной области, а затем используете её в том месте, где вам нужны данные:
{
List<Order> List(
Specification<Order> spec);
}
public class ShippedOrdersForUserSpec :Таким образом запрос по-прежнему выполняется в базе данных, но IQueryable больше не используется нигде за пределами реализации репозитория. Вы можете посмотреть на использование этого паттерна в примере eShopOnWeb (см. папку /src/ApplicationCore/Specifications/).
Specification<Order>
{
public ShippedOrdersForUserSpec(
string username)
{
Query.Where(o => !o.Canceled &&
o.CreatedBy == username &&
o.Shipped);
}
}
// в контроллере/на странице
var spec = new ShippedOrdersForUserSpec(username);
var viewModel = await _repo.ListAsync(spec);
Итого
Распространение IQueryable по всему приложению позволяет расширять запросы к БД из любого места. Это и хорошо, и плохо, и соответствует определению антипаттерна, поскольку сначала кажется чем-то стоящим, но на практике приводит к проблемам. Решение состоит не в том, чтобы получать нефильтрованные данные и затем фильтровать их на сервере приложений, а в том, чтобы идентифицировать конкретные запросы, которые потребуются отдельным страницам или конечным точкам, и определять их как первоклассные абстракции в модели предметной области приложения. Паттерны Спецификация и Репозиторий прекрасно подходят для достижения такого дизайна и обеспечивают гораздо более удобный и тестируемый результат.
Источник: https://ardalis.com/avoid-dbcontext-iqueryable-proliferation/