LINQ глубоко: отложенное выполнение, IEnumerable и IQueryable, expression trees

↑ 2.1 C♯: продвинутый язык · ← Предыдущая · → Следующая

Обязательно~6 мин чтенияУровень: middle

Зачем это на собесе

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();

Вопросы с ответами

Связанные темы