Что такое ленивое вычисление в LINQ и как оно влияет на поведение
Что проверяют
Понимаете ли вы, что переменная с запросом LINQ это рецепт, а не результат, и умеете ли предсказать, когда и сколько раз он выполнится. Дальше разговор почти всегда уходит в различие IEnumerable и IQueryable и в работу с ORM.
Как отвечать
Операторы вроде Where, Select, OrderBy, Take возвращают объект запроса и ничего не вычисляют. Выполнение начинается при переборе: foreach, ToList, ToArray, Count, Sum, First, Any. Первая группа называется отложенной, вторая немедленной.
var q = items.Where(x => x.Price > 100); // ничего не выполнилось
items.Add(new Item { Price = 500 });
var n = q.Count(); // новый элемент уже попал в результатДальше важна реализация. У IEnumerable цепочка это делегаты, они выполняются в памяти процесса поэлементно. У IQueryable это дерево выражений, которое провайдер переводит в один запрос к источнику при первом переборе. Поэтому фильтры и проекции, дописанные до материализации, уезжают в базу, а всё, что после ToList, считается уже в приложении.
Стоит отличать потоковые операторы (Where и Select отдают элементы по одному) от буферизующих (OrderBy и GroupBy вынуждены собрать весь вход, прежде чем вернуть первый элемент).
Где спотыкаются
Перебирают один и тот же запрос несколько раз и получают повторные обращения к базе на каждом переборе. Лечится одним вызовом материализации в нужном месте, но его же ставят слишком рано и утаскивают в память всю таблицу.
Отдают IQueryable наружу и переживают перебор за пределами жизни контекста. Запрос выполняется позже, контекст уже освобождён, и падение случается в месте, никак не связанном с написанным запросом. По той же причине ошибка данных всплывает на строке foreach, а не там, где запрос собран.
Считают, что любой код внутри Where доедет до SQL. Провайдер переводит не всё: непереводимый вызов заканчивается исключением либо тихой загрузкой лишних строк в память с фильтрацией уже там.