.NET Tutorial
Junior5 мин чтенияВопрос 7 из 25

Что такое IEnumerable vs IQueryable? Где используется каждый?

Одинаковый код — разное выполнение

C#
// IQueryable: фильтр уйдёт в SQL
IQueryable<Order> q = db.Orders.Where(o => o.Total > 1000);
var list1 = q.ToList();
// SQL: SELECT ... FROM Orders WHERE Total > 1000

// IEnumerable: вся таблица загрузится в память, фильтр — в C#
IEnumerable<Order> e = db.Orders.AsEnumerable().Where(o => o.Total > 1000);
var list2 = e.ToList();
// SQL: SELECT ... FROM Orders   (без WHERE!)

IQueryable<Order>

Where(o => o.Total > 1000)

Expression tree

EF Core: трансляция в SQL

БД: WHERE Total > 1000
вернула 50 строк

IEnumerable<Order>

БД: SELECT *
вернула 1 000 000 строк

Материализация
1 000 000 объектов

Where в памяти
осталось 50

IQueryable<Order>

Where(o => o.Total > 1000)

Expression tree

EF Core: трансляция в SQL

БД: WHERE Total > 1000
вернула 50 строк

IEnumerable<Order>

БД: SELECT *
вернула 1 000 000 строк

Материализация
1 000 000 объектов

Where в памяти
осталось 50

Где выполняется фильтрация

Как устроены интерфейсы

C#
public interface IEnumerable<out T> : IEnumerable
{
    IEnumerator<T> GetEnumerator();          // только «дай следующий элемент»
}

public interface IQueryable<out T> : IEnumerable<T>, IQueryable
{
    Type ElementType { get; }
    Expression Expression { get; }           // дерево выражений — «что хотим получить»
    IQueryProvider Provider { get; }         // кто умеет его выполнить (EF Core, OData...)
}

Методы-расширения различаются сигнатурами:

C#
// System.Linq.Enumerable — для IEnumerable
public static IEnumerable<T> Where<T>(this IEnumerable<T> source, Func<T, bool> predicate);

// System.Linq.Queryable — для IQueryable
public static IQueryable<T> Where<T>(this IQueryable<T> source, Expression<Func<T, bool>> predicate);

Лямбда o => o.Total > 1000 компилируется по-разному в зависимости от типа параметра:

  • в Func<Order,bool> — в обычный IL-код, который можно только вызвать;
  • в Expression<Func<Order,bool>> — в объектную модель выражения (узлы BinaryExpression, MemberExpression, ConstantExpression), которую можно проанализировать и перевести в SQL.

LambdaExpression
параметр: o

BinaryExpression
GreaterThan

MemberExpression
o.Total

ConstantExpression
1000

ParameterExpression
o : Order

LambdaExpression
параметр: o

BinaryExpression
GreaterThan

MemberExpression
o.Total

ConstantExpression
1000

ParameterExpression
o : Order

Дерево выражения для o => o.Total > 1000

Сравнение

IEnumerable<T> IQueryable<T>
Пространство имён методов System.Linq.Enumerable System.Linq.Queryable
Аргументы Func<> — делегаты Expression<Func<>> — деревья выражений
Где выполняется В памяти процесса В источнике данных (БД, API)
Какие методы можно вызвать в лямбде Любые C#-методы Только те, что провайдер умеет транслировать
Типичный источник List, массивы, yield return DbSet<T>, OData, MongoDB LINQ
Отложенное выполнение Да Да

Граница между мирами

Когда LINQ-цепочка переходит из IQueryable в IEnumerable, всё, что после перехода, выполняется в памяти:

C#
var result = db.Orders
    .Where(o => o.CreatedAt >= from)          // SQL
    .OrderByDescending(o => o.Total)          // SQL
    .Select(o => new { o.Id, o.Total, o.Note }) // SQL: только нужные столбцы
    .AsEnumerable()                            // ← граница
    .Where(o => MyComplexCheck(o.Note))        // C#: метод, который нельзя перевести в SQL
    .Take(10)                                  // C#
    .ToList();

Где что использовать

  • Репозиторий / запрос к БД — строим IQueryable, добавляем фильтры, пагинацию, проекцию, и материализуем (ToListAsync) внутри слоя данных.
  • Наружу из репозитория — List<T>, IReadOnlyList<T> или DTO. Возврат IQueryable из репозитория — спорная практика: вызывающий код может сгенерировать неэффективный запрос или обратиться к БД после закрытия контекста.
  • Коллекции в памяти, генераторы (yield) — IEnumerable<T>.
  • Динамические фильтры (поиск по опциональным полям) — удобно наращивать IQueryable:
C#
IQueryable<Product> query = db.Products.AsNoTracking();
if (filter.MinPrice is not null) query = query.Where(p => p.Price >= filter.MinPrice);
if (!string.IsNullOrEmpty(filter.Name)) query = query.Where(p => p.Name.Contains(filter.Name));
var page = await query.OrderBy(p => p.Id).Skip(skip).Take(take).ToListAsync(ct);

Вопросы на засыпку

Что будет, если в Where для IQueryable вызвать собственный C#-метод?

EF Core не сможет его перевести. С EF Core 3.0 это исключение InvalidOperationException: The LINQ expression could not be translated (до 3.0 был тихий client evaluation). Исключение: метод в финальном Select — там EF Core может выполнить его на клиенте.

Чем опасен многократный перебор IEnumerable?

Каждый foreach/Count() заново выполняет всю цепочку — для IQueryable это новый SQL-запрос, для генератора — повторное вычисление. Материализуйте один раз через ToList(). Анализаторы подсвечивают это как «Possible multiple enumeration».

IQueryable наследуется от IEnumerable — значит, можно везде передавать как IEnumerable?

Можно, но выполнение переключится на LINQ to Objects с момента, когда статический тип стал IEnumerable. Все последующие операторы выполняются в памяти.

Что такое IAsyncEnumerable и связь с EF Core?

AsAsyncEnumerable() позволяет получать строки из БД по одной через await foreach, не загружая всё в список. Это полезно для стриминга больших выборок.

Итог

IEnumerable — «перебери в памяти», IQueryable — «опиши запрос, который выполнит кто-то другой». Держите запрос в IQueryable как можно дольше, чтобы фильтрация, сортировка, пагинация и проекция ушли в базу, и материализуйте результат один раз.