Expression trees and IQueryable internals
An IQueryable is a description of a query, and the difference between a delegate and an expression tree is the whole reason providers can translate it.
Delegate versus expression tree
using System.Linq.Expressions;
// A delegate is compiled code. A provider cannot inspect it.
Func<Order, bool> asDelegate = o => o.Total > 100;
// An expression tree is data describing the code. A provider can walk it.
Expression<Func<Order, bool>> asExpression = o => o.Total > 100;
Console.WriteLine(asExpression.Body.NodeType); // GreaterThan
Console.WriteLine(asExpression.Parameters[0].Name); // o
// IEnumerable uses Func; IQueryable uses Expression<Func>
// IEnumerable<T>.Where(Func<T, bool>)
// IQueryable<T>.Where(Expression<Func<T, bool>>)
// That single difference is why this compiles and produces terrible SQL
var bad = db.Orders
.Where(o => IsExpensive(o)); // no expression overload available
public static bool IsExpensive(Order o) => o.Total > 100;
// ...while this is translated
Expression<Func<Order, bool>> predicate = o => o.Total > 100;
var good = db.Orders.Where(predicate);Composing queries and building predicates
// A reusable fragment: a method that takes and returns IQueryable<T>
public static IQueryable<Order> PlacedSince(this IQueryable<Order> query, DateTimeOffset since) =>
query.Where(o => o.PlacedAt >= since);
public static IQueryable<Order> Open(this IQueryable<Order> query) =>
query.Where(o => o.Status == OrderStatus.Open);
// Composed at the call site, and the whole thing becomes one SQL statement
var recent = db.Orders
.Open()
.PlacedSince(DateTimeOffset.UtcNow.AddDays(-7))
.OrderBy(o => o.Id);
// Building a predicate dynamically with an expression tree
public static Expression<Func<T, bool>> BuildOr<T>(
IReadOnlyList<Expression<Func<T, bool>>> predicates)
{
if (predicates.Count == 0)
return _ => true;
var parameter = Expression.Parameter(typeof(T), "x");
Expression? body = null;
foreach (var p in predicates)
{
var replaced = new ParameterReplacer(p.Parameters[0], parameter)
.Visit(p.Body);
body = body is null ? replaced : Expression.OrElse(body, replaced);
}
return Expression.Lambda<Func<T, bool>>(body!, parameter);
}
internal sealed class ParameterReplacer : ExpressionVisitor
{
private readonly ParameterExpression _from;
private readonly ParameterExpression _to;
public ParameterReplacer(ParameterExpression from, ParameterExpression to)
=> (_from, _to) = (from, to);
protected override Expression VisitParameter(ParameterExpression node) =>
node == _from ? _to : base.VisitParameter(node);
}
// Usage: any of these statuses, generated from a runtime list
var statuses = new[] { OrderStatus.Open, OrderStatus.Pending };
var anyStatus = BuildOr<Order>(statuses
.Select(s => (Expression<Func<Order, bool>>)(o => o.Status == s))
.ToList());
var results = await db.Orders.Where(anyStatus).ToListAsync(ct);| Asked of the query | Translatable | Workaround |
|---|---|---|
| A comparison and a boolean operator | Yes | None needed |
string.StartsWith with a constant | Yes | EF.Functions.Like for patterns |
string.Contains with a local variable | Usually | Depends on the provider and collation |
| A method call on your own type | No | Inline the expression or map to a database function |
| A local function or a delegate variable | No | Accept an expression parameter instead |
| DateTime formatting | Provider specific | Do the formatting after materialising |
| A custom comparer | No | Compare on a normalised column |
⚠️
Calling a delegate from inside a queryable expression forces the provider to fetch the data and evaluate locally, which is invisible in a unit test over a list. If a query is unexpectedly slow, look for a method call that is not a known-translatable one.
FAQ
Can I inspect the expression tree at runtime?
Yes. Walk the body with an ExpressionVisitor, or just log it: an expression's ToString gives a readable form that shows exactly what the provider received.
Why can I not call my own method in a Where clause?
Because the provider has no idea how to translate it to SQL. Expose it as an Expression> or inline the logic so the tree contains only translatable nodes.
Related
Deferred execution and querying a database Debugging and testing LINQ queries
Last refreshed 2026-09-18.