LINQ глубоко: отложенное выполнение, IEnumerable и IQueryable, expression trees
↑ 2.1 C♯: продвинутый язык · ← Предыдущая · → Следующая
Зачем это на собесе
senior-уровень LINQ: понимать отложенное выполнение, разницу
FuncиExpression<Func>, как EF Core превращает C# в SQL и почему иногда грузит всю таблицу. Это напрямую влияет на производительность сервисов.
Объяснение
Отложенное выполнение изнутри
LINQ to Objects построен на итераторах (yield return). Конвейер Where → Select не создаёт промежуточных списков: элементы проходят по одному при перечислении.
static IEnumerable<T> MyWhere<T>(IEnumerable<T> source, Func<T, bool> predicate)
{
foreach (var item in source)
if (predicate(item)) yield return item; // ленивая выдача
}
var query = patients.Where(p => { Console.Write("W "); return p.Age > 18; })
.Select(p => { Console.Write("S "); return p.Name; });
// ничего не выведено
foreach (var n in query) { } // W S W W S ... — элементы идут по конвейеру по одному- Потоковые операторы (
Where,Select,Take) обрабатывают по одному элементу. - Буферизующие (
OrderBy,GroupBy,Reverse,Distinct) при первом запросе элемента читают весь источник.
Func против Expression<Func>
Func<Patient, bool> f = p => p.Age > 18; // скомпилированный делегат (IL-код)
Expression<Func<Patient, bool>> e = p => p.Age > 18; // ДЕРЕВО ВЫРАЖЕНИЯ — данные, описывающие кодIEnumerable.Where(Func)— выполняет делегат в памяти.IQueryable.Where(Expression)— передаёт дерево провайдеру, который транслирует его (EF Core → SQL, Hot Chocolate → запрос к источнику).
// ОШИБКА: метод принимает Func — EF не видит условие, тянет всю таблицу и фильтрует в памяти
IEnumerable<Patient> Filter(Func<Patient, bool> pred) => db.Patients.Where(pred);
// ПРАВИЛЬНО: Expression — условие уходит в SQL WHERE
IQueryable<Patient> Filter(Expression<Func<Patient, bool>> pred) => db.Patients.Where(pred);Expression trees
Код как структура данных: можно анализировать, строить и компилировать в рантайме.
Expression<Func<Patient, bool>> expr = p => p.City == "Минск";
var body = (BinaryExpression)expr.Body; // Equal(Member(p.City), Constant("Минск"))
// динамический фильтр (например, из параметров GraphQL или UI-таблицы)
static Expression<Func<T, bool>> PropertyEquals<T>(string prop, object value)
{
var p = Expression.Parameter(typeof(T), "x");
var body = Expression.Equal(Expression.PropertyOrField(p, prop), Expression.Constant(value));
return Expression.Lambda<Func<T, bool>>(body, p);
}
var q = db.Patients.Where(PropertyEquals<Patient>("City", "Минск"));
var compiled = expr.Compile(); // превратить в делегат (дорого — кэшируйте)Где используются: ORM (EF Core, linq2db), GraphQL-фильтры и проекции (Hot Chocolate [UseFiltering]), маппинг (AutoMapper ProjectTo), спецификации, валидаторы (RuleFor(x => x.Name) в FluentValidation берёт имя свойства из выражения).
Трансляция EF Core
- Только известные провайдеру конструкции: операторы,
Contains,StartsWith,EF.Functions.ILike, агрегаты, навигационные свойства. - Вызов вашего C#-метода внутри
Whereне транслируется → исключение (EF Core 3+). - Финальный
Selectможет выполнить часть на клиенте (проекция), но фильтры и сортировки должны быть транслируемы.
Нюансы и подводные камни
AsEnumerable()посреди запроса к EF переводит всё дальнейшее в память. Ставьте его осознанно, после фильтрации.ToList()внутри цикла или в проекции — N+1 запросов.Compile()дорогой (генерация IL): кэшируйте скомпилированные выражения.- Захваченная в выражение переменная становится параметром SQL (хорошо для плана запроса). Константа в выражении — литералом.
- LINQ-запрос над изменяемой коллекцией, выполненный позже, видит изменения, внесённые после его создания.
Тестирование
[Fact]
public void Dynamic_filter_matches_property()
{
var data = new[] { new Patient { City = "Минск" }, new Patient { City = "Брест" } }.AsQueryable();
data.Where(PropertyEquals<Patient>("City", "Минск")).Should().ContainSingle();
}Трансляцию запросов проверяйте интеграционными тестами на реальной БД (Testcontainers) и ToQueryString():
var sql = db.Patients.Where(p => p.City == "Минск").ToQueryString();Вопросы с ответами
Чем Func и Expression<Func> отличаются?
Func — скомпилированный делегат, который можно только выполнить. Expression — дерево выражения, описывающее код как данные: его можно анализировать и транслировать в другой язык, например в SQL.
Почему запрос через EF Core может загрузить всю таблицу в память?
Если в цепочке появился IEnumerable (AsEnumerable, метод с параметром Func вместо Expression, ToList до фильтра), дальнейшие операторы выполняются в памяти.
Какие LINQ-операторы потоковые, а какие буферизующие?
Потоковые (Where, Select, Take, Skip) обрабатывают элементы по одному. Буферизующие (OrderBy, GroupBy, Distinct, Reverse) сначала читают весь источник.
Где применяются expression trees?
ORM (EF Core), динамические фильтры и сортировки, GraphQL-фильтрация, AutoMapper ProjectTo, FluentValidation, построение быстрых геттеров вместо рефлексии.
Как увидеть SQL, который сгенерирует EF Core?
ToQueryString(), логирование команд (LogTo, категория Microsoft.EntityFrameworkCore.Database.Command) или трассировка в OpenTelemetry.
Связанные темы
- Предыдущая: [[be-2-1-2-sobytiya-i-pattern-observer|События и паттерн Observer в C#]] · Следующая: yield, итераторы, IAsyncEnumerable
- LINQ основы: LINQ: основные операторы
- IEnumerable и IQueryable: Интерфейсы коллекций: IEnumerable, ICollection, IList, IReadOnly
- Производительность EF Core: Производительность EF Core и логирование SQL
- Hot Chocolate: фильтрация: Hot Chocolate: queries, mutations, фильтрация, пагинация