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 queryTranslatableWorkaround
A comparison and a boolean operatorYesNone needed
string.StartsWith with a constantYesEF.Functions.Like for patterns
string.Contains with a local variableUsuallyDepends on the provider and collation
A method call on your own typeNoInline the expression or map to a database function
A local function or a delegate variableNoAccept an expression parameter instead
DateTime formattingProvider specificDo the formatting after materialising
A custom comparerNoCompare 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.

Deferred execution and querying a database Debugging and testing LINQ queries

Last refreshed 2026-09-18.