C# Expression Trees

πŸ“– 8 min read

What Are Expression Trees

Expression trees turn code into data. Instead of executing a lambda, you can inspect its structure, transform it, or translate it to another language like SQL.

Expression trees represent code as data structures. Instead of executing immediately, the code is captured as a tree that can be analyzed, modified, or compiled.

using System.Linq.Expressions;

// Lambda expression - executes immediately
Func<int, int> doubleFunc = x => x * 2;
int result = doubleFunc(5);  // 10

// Expression tree - captured as data
Expression<Func<int, int>> doubleExpr = x => x * 2;
// Can inspect: Parameter "x", Multiply operation, Constant 2

// Compile to function when ready
Func<int, int> compiled = doubleExpr.Compile();
int result2 = compiled(5);  // 10

Why Expression Trees Matter

Where You Use Expression Trees

If you've written .Where(p => p.Age > 18) with Entity Framework, you've used expression trees. EF Core translates that lambda into SQL because it's an expression tree, not executable code.

Expression trees power:

  • LINQ to SQL/EF Core: Translates C# queries to SQL
  • Dynamic queries: Build queries at runtime
  • Serialization: Send code across boundaries
  • Code generation: Create optimized delegates
  • Mocking frameworks: Capture method calls
// EF Core translates this to SQL
var adults = dbContext.People
    .Where(p => p.Age >= 18)  // Expression<Func<Person, bool>>
    .OrderBy(p => p.Name)     // Expression<Func<Person, string>>
    .ToList();

// Generated SQL:
// SELECT * FROM People WHERE Age >= 18 ORDER BY Name

Expression Tree Structure

Anatomy of an Expression

Expression<Func<int, int, int>> addExpr = (a, b) => a + b;

// Tree structure:
// LambdaExpression
// β”œβ”€β”€ Parameters: [a, b]
// └── Body: BinaryExpression (Add)
//     β”œβ”€β”€ Left: ParameterExpression (a)
//     └── Right: ParameterExpression (b)

// Inspect the tree
var lambda = (LambdaExpression)addExpr;
Console.WriteLine($"Parameters: {string.Join(", ", lambda.Parameters)}");
Console.WriteLine($"Body: {lambda.Body}");
Console.WriteLine($"Body Type: {lambda.Body.NodeType}");  // Add

var binary = (BinaryExpression)lambda.Body;
Console.WriteLine($"Left: {binary.Left}");   // a
Console.WriteLine($"Right: {binary.Right}"); // b

Common Expression Types

Type Description Example
ConstantExpression Literal value 5, "hello"
ParameterExpression Parameter reference x in x => x + 1
BinaryExpression Two operands a + b, x > 5
UnaryExpression One operand -x, !flag
MemberExpression Property/field access person.Name
MethodCallExpression Method invocation str.ToUpper()
ConditionalExpression Ternary x > 0 ? x : -x
NewExpression Constructor call new Point(1, 2)
LambdaExpression Lambda definition x => x * 2

Building Expressions Manually

Simple Expressions

// Build: x => x * 2
var param = Expression.Parameter(typeof(int), "x");
var constant = Expression.Constant(2);
var multiply = Expression.Multiply(param, constant);
var lambda = Expression.Lambda<Func<int, int>>(multiply, param);

Func<int, int> compiled = lambda.Compile();
Console.WriteLine(compiled(5));  // 10

Property Access

public class Person
{
    public string Name { get; set; } = "";
    public int Age { get; set; }
}

// Build: p => p.Name
var param = Expression.Parameter(typeof(Person), "p");
var property = Expression.Property(param, "Name");
var lambda = Expression.Lambda<Func<Person, string>>(property, param);

var getName = lambda.Compile();
var person = new Person { Name = "Alice" };
Console.WriteLine(getName(person));  // Alice

Method Calls

// Build: s => s.ToUpper()
var param = Expression.Parameter(typeof(string), "s");
var method = typeof(string).GetMethod("ToUpper", Type.EmptyTypes)!;
var call = Expression.Call(param, method);
var lambda = Expression.Lambda<Func<string, string>>(call, param);

var toUpper = lambda.Compile();
Console.WriteLine(toUpper("hello"));  // HELLO

Complex Expressions

// Build: (a, b) => a > 0 ? a + b : a - b
var a = Expression.Parameter(typeof(int), "a");
var b = Expression.Parameter(typeof(int), "b");

var condition = Expression.GreaterThan(a, Expression.Constant(0));
var ifTrue = Expression.Add(a, b);
var ifFalse = Expression.Subtract(a, b);
var conditional = Expression.Condition(condition, ifTrue, ifFalse);

var lambda = Expression.Lambda<Func<int, int, int>>(conditional, a, b);
var func = lambda.Compile();

Console.WriteLine(func(5, 3));   // 8
Console.WriteLine(func(-5, 3)); // -8

Dynamic Query Building

Building Where Clauses

public static class QueryBuilder
{
    public static Expression<Func<T, bool>> BuildPredicate<T>(
        string propertyName,
        object value)
    {
        var param = Expression.Parameter(typeof(T), "x");
        var property = Expression.Property(param, propertyName);
        var constant = Expression.Constant(value);
        var equals = Expression.Equal(property, constant);

        return Expression.Lambda<Func<T, bool>>(equals, param);
    }
}

// Usage
var predicate = QueryBuilder.BuildPredicate<Person>("Name", "Alice");
var results = dbContext.People.Where(predicate).ToList();

Combining Predicates

public static class PredicateBuilder
{
    public static Expression<Func<T, bool>> And<T>(
        Expression<Func<T, bool>> left,
        Expression<Func<T, bool>> right)
    {
        var param = Expression.Parameter(typeof(T), "x");

        var leftBody = ReplaceParameter(left.Body, left.Parameters[0], param);
        var rightBody = ReplaceParameter(right.Body, right.Parameters[0], param);

        var combined = Expression.AndAlso(leftBody, rightBody);
        return Expression.Lambda<Func<T, bool>>(combined, param);
    }

    public static Expression<Func<T, bool>> Or<T>(
        Expression<Func<T, bool>> left,
        Expression<Func<T, bool>> right)
    {
        var param = Expression.Parameter(typeof(T), "x");

        var leftBody = ReplaceParameter(left.Body, left.Parameters[0], param);
        var rightBody = ReplaceParameter(right.Body, right.Parameters[0], param);

        var combined = Expression.OrElse(leftBody, rightBody);
        return Expression.Lambda<Func<T, bool>>(combined, param);
    }

    private static Expression ReplaceParameter(
        Expression expression,
        ParameterExpression oldParam,
        ParameterExpression newParam)
    {
        return new ParameterReplacer(oldParam, newParam).Visit(expression);
    }
}

// Usage
Expression<Func<Person, bool>> isAdult = p => p.Age >= 18;
Expression<Func<Person, bool>> hasEmail = p => p.Email != null;

var combined = PredicateBuilder.And(isAdult, hasEmail);
var results = dbContext.People.Where(combined).ToList();

Dynamic OrderBy

public static IQueryable<T> OrderByProperty<T>(
    this IQueryable<T> source,
    string propertyName,
    bool descending = false)
{
    var param = Expression.Parameter(typeof(T), "x");
    var property = Expression.Property(param, propertyName);
    var lambda = Expression.Lambda(property, param);

    string methodName = descending ? "OrderByDescending" : "OrderBy";

    var method = typeof(Queryable).GetMethods()
        .First(m => m.Name == methodName && m.GetParameters().Length == 2)
        .MakeGenericMethod(typeof(T), property.Type);

    return (IQueryable<T>)method.Invoke(null, new object[] { source, lambda })!;
}

// Usage
var sorted = dbContext.People
    .OrderByProperty("Name")
    .ToList();

Expression Visitors

Traversing and Modifying Trees

public class ParameterReplacer : ExpressionVisitor
{
    private readonly ParameterExpression _oldParam;
    private readonly ParameterExpression _newParam;

    public ParameterReplacer(ParameterExpression oldParam, ParameterExpression newParam)
    {
        _oldParam = oldParam;
        _newParam = newParam;
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
        return node == _oldParam ? _newParam : base.VisitParameter(node);
    }
}

Query Analysis

public class MemberAccessVisitor : ExpressionVisitor
{
    public List<string> AccessedMembers { get; } = new();

    protected override Expression VisitMember(MemberExpression node)
    {
        AccessedMembers.Add(node.Member.Name);
        return base.VisitMember(node);
    }
}

// Usage - find all properties accessed in a query
Expression<Func<Person, bool>> expr = p => p.Age > 18 && p.Name.StartsWith("A");
var visitor = new MemberAccessVisitor();
visitor.Visit(expr);
// AccessedMembers: ["Age", "Name"]

Expression Transformation

// Convert property access to dictionary lookup
public class PropertyToDictVisitor : ExpressionVisitor
{
    private readonly ParameterExpression _dictParam;

    public PropertyToDictVisitor(ParameterExpression dictParam)
    {
        _dictParam = dictParam;
    }

    protected override Expression VisitMember(MemberExpression node)
    {
        if (node.Expression is ParameterExpression)
        {
            // Convert p.Name to dict["Name"]
            var key = Expression.Constant(node.Member.Name);
            var indexer = typeof(Dictionary<string, object>)
                .GetProperty("Item")!;
            return Expression.MakeIndex(_dictParam, indexer, new[] { key });
        }
        return base.VisitMember(node);
    }
}

Compiled Expressions for Performance

Caching Compiled Delegates

Compile() Is Expensive

Compiling an expression tree to a delegate is slow, much slower than reflection. Always cache the compiled result when the same expression will be used repeatedly.

public static class PropertyAccessor<T>
{
    private static readonly ConcurrentDictionary<string, Func<T, object?>> Getters = new();

    public static Func<T, object?> GetGetter(string propertyName)
    {
        return Getters.GetOrAdd(propertyName, CreateGetter);
    }

    private static Func<T, object?> CreateGetter(string propertyName)
    {
        var param = Expression.Parameter(typeof(T), "x");
        var property = Expression.Property(param, propertyName);
        var converted = Expression.Convert(property, typeof(object));
        var lambda = Expression.Lambda<Func<T, object?>>(converted, param);
        return lambda.Compile();
    }
}

// Usage - much faster than reflection after first call
var getter = PropertyAccessor<Person>.GetGetter("Name");
var name = getter(person);

Fast Property Setters

public static class PropertySetter<T>
{
    private static readonly ConcurrentDictionary<string, Action<T, object?>> Setters = new();

    public static Action<T, object?> GetSetter(string propertyName)
    {
        return Setters.GetOrAdd(propertyName, CreateSetter);
    }

    private static Action<T, object?> CreateSetter(string propertyName)
    {
        var param = Expression.Parameter(typeof(T), "x");
        var valueParam = Expression.Parameter(typeof(object), "value");
        var property = Expression.Property(param, propertyName);
        var converted = Expression.Convert(valueParam, property.Type);
        var assign = Expression.Assign(property, converted);
        var lambda = Expression.Lambda<Action<T, object?>>(assign, param, valueParam);
        return lambda.Compile();
    }
}

Practical Examples

Generic Mapper

public static class Mapper<TSource, TDest> where TDest : new()
{
    private static readonly Func<TSource, TDest> MapFunc = CreateMapper();

    private static Func<TSource, TDest> CreateMapper()
    {
        var sourceParam = Expression.Parameter(typeof(TSource), "src");
        var bindings = new List<MemberBinding>();

        foreach (var destProp in typeof(TDest).GetProperties())
        {
            var sourceProp = typeof(TSource).GetProperty(destProp.Name);
            if (sourceProp != null && sourceProp.PropertyType == destProp.PropertyType)
            {
                var sourceAccess = Expression.Property(sourceParam, sourceProp);
                bindings.Add(Expression.Bind(destProp, sourceAccess));
            }
        }

        var newExpr = Expression.New(typeof(TDest));
        var memberInit = Expression.MemberInit(newExpr, bindings);
        var lambda = Expression.Lambda<Func<TSource, TDest>>(memberInit, sourceParam);

        return lambda.Compile();
    }

    public static TDest Map(TSource source) => MapFunc(source);
}

// Usage
var dto = Mapper<Person, PersonDto>.Map(person);

Specification Pattern

public abstract class Specification<T>
{
    public abstract Expression<Func<T, bool>> ToExpression();

    public bool IsSatisfiedBy(T entity)
    {
        return ToExpression().Compile()(entity);
    }

    public Specification<T> And(Specification<T> other)
    {
        return new AndSpecification<T>(this, other);
    }

    public Specification<T> Or(Specification<T> other)
    {
        return new OrSpecification<T>(this, other);
    }
}

public class AndSpecification<T> : Specification<T>
{
    private readonly Specification<T> _left;
    private readonly Specification<T> _right;

    public AndSpecification(Specification<T> left, Specification<T> right)
    {
        _left = left;
        _right = right;
    }

    public override Expression<Func<T, bool>> ToExpression()
    {
        return PredicateBuilder.And(_left.ToExpression(), _right.ToExpression());
    }
}

// Concrete specifications
public class AdultSpecification : Specification<Person>
{
    public override Expression<Func<T, bool>> ToExpression()
        => p => p.Age >= 18;
}

// Usage
var spec = new AdultSpecification()
    .And(new HasEmailSpecification());
var results = dbContext.People.Where(spec.ToExpression()).ToList();

Limitations and Considerations

What Can’t Be in Expression Trees

// These compile as Func, not Expression:

// Dynamic
Expression<Func<dynamic, int>> bad1 = d => d.Value;  // Error

// Out/ref parameters
Expression<Action<out int>> bad2 = (out int x) => x = 1;  // Error

// Named/optional parameters
Expression<Func<int>> bad3 = () => Method(name: "test");  // Error in some cases

// Async lambdas
Expression<Func<Task<int>>> bad4 = async () => await Task.FromResult(1);  // Error

// Statements (only expressions allowed)
Expression<Action> bad5 = () => { var x = 1; };  // Error

Performance Notes

// Compile() is expensive - cache results
// BAD: Compiles every call
public bool Check(Expression<Func<Person, bool>> expr, Person p)
{
    return expr.Compile()(p);  // Slow!
}

// GOOD: Cache compiled delegate
private readonly ConcurrentDictionary<Expression, Delegate> _cache = new();

public bool CheckCached(Expression<Func<Person, bool>> expr, Person p)
{
    var compiled = (Func<Person, bool>)_cache.GetOrAdd(
        expr,
        e => ((Expression<Func<Person, bool>>)e).Compile());
    return compiled(p);
}

Version History

Feature Version Significance
Expression Trees C# 3.0 LINQ foundation
Expression Visitor .NET 4.0 Tree traversal/modification
Improved debugging .NET 4.0 DebugView property
Block expressions .NET 4.0 Statement-like expressions
Index expressions .NET 4.0 Indexer access

Key Takeaways

Code as data: Expression trees let you inspect and manipulate code structure at runtime.

LINQ translation: They’re how EF Core and other providers translate C# to SQL.

Cache compiled delegates: Compile() is expensive. Cache results for repeated use.

Use ExpressionVisitor: The standard way to traverse and transform expression trees.

Prefer lambdas when possible: Manual tree building is verbose. Use Expression<Func<>> from lambdas when the expression is known at compile time.

Limitations exist: Async, out parameters, statements, and some language features aren’t supported.

Found this guide helpful? Share it with your team:

Share on LinkedIn