Expression Trees

Reviewed & published by Brayan K

By the end of this lesson you'll be able to treat a piece of C# logic as data you can read, build, and rewrite at runtime — then compile it back into runnable code. This is the machinery behind LINQ providers and ORMs like Entity Framework, which read your queries and translate them into SQL.

Part of the free C# course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

💡 Real-World Analogy

A compiled lambda (a Func) is like a cooked meal — you can eat it, but you can't see the recipe or change the ingredients. An expression tree is the recipe written down as a list of steps: because it's just data, you can read each step, swap an ingredient, translate it into another language, or finally cook it (that's Compile()) whenever you're ready. Entity Framework needs the recipe, not the meal — it reads your p => p.Price > 100 recipe and rewrites it as WHERE Price > 100 SQL before any data is fetched.

📊 The Expression API at a Glance

MemberWhat it doesExampleReturns
Expression<T>A lambda stored as dataExpression<Func<int,int>>a tree
.Compile()Turn the tree into codeexpr.Compile()a delegate
.Body / .NodeTypeInspect the treeexpr.Body.NodeTypea node / kind
Expression.ParameterMake a parameter nodeParameter(typeof(int),"a")a node
Expression.AddMake a + nodeAdd(a, b)a node
Expression.LambdaWrap a body + paramsLambda<Func<...>>(body, a)a tree
ExpressionVisitorWalk / rewrite a treevisitor.Visit(expr)a new tree

Everything lives in System.Linq.Expressions. Forget that using and none of the Expression.* factory methods will be in scope.

Running C# locally: install the .NET SDK or use dotnetfiddle.net. Every example below needs using System.Linq.Expressions; at the top.

1. Expression<Func> vs a Plain Func

Write (a, b) => a + b and what you get depends on the type you store it in. Store it in a Func<int, int, int> and the compiler turns it into IL — runnable machine code you can call but never look inside. Store the identical lambda in an Expression<Func<int, int, int>> and the compiler instead builds a tree of objects describing the code: a Lambda node, with a parameter list and an Add node inside it. The tree is data, so you can read it, take it apart, and only later compile it into a delegate. Read this worked example, run it, then you'll write your own.

using System;
using System.Linq.Expressions;        // 👈 needed for Expression<T> and the factory methods

class Program
{
    static void Main()
    {
        // A lambda stored as a DELEGATE is compiled IL — runnable code.
        // You can CALL it, but you can't look inside it.
        Func<int, int, int> addFunc = (a, b) => a + b;
        Console.WriteLine($"Func result: {addFunc(3, 5)}");   // Func result: 8

        // The SAME lambda stored as an EXPRESSION is a data structure (a tree).
        // The compiler does NOT turn it into IL — it builds an object you can read.
        Expression<Func<int, int, int>> addExpr = (a, b) => a + b;

        // Because it's data, you can inspect its parts:
        Console.WriteLine($"NodeType:   {addExpr.NodeType}");    // Lambda
        Console.WriteLine($"Body:       {addExpr.Body}");        // (a + b)
        Console.WriteLine($"Body kind:  {addExpr.Body.NodeType}");// Add
        Console.WriteLine($"Params:     {string.Join(", ", addExpr.Parameters)}"); // a, b

        // To RUN an expression tree you must Compile() it into a delegate first.
        Func<int, int, int> compiled = addExpr.Compile();
        Console.WriteLine($"Compiled:   {compiled(3, 5)}");      // Compiled:   8

        // You can also BUILD a tree by hand with the Expression factory methods.
        ParameterExpression a2 = Expression.Parameter(typeof(int), "a"); // the 'a' node
        ParameterExpression b2 = Expression.Parameter(typeof(int), "b"); // the 'b' node
        BinaryExpression multiply = Expression.Multiply(a2, b2);          // a * b
        var lambda = Expression.Lambda<Func<int, int, int>>(multiply, a2, b2);

        Console.WriteLine($"Manual:     {lambda}");             // (a, b) => (a * b)
        Func<int, int, int> mul = lambda.Compile();
        Console.WriteLine($"4 * 7 =     {mul(4, 7)}");          // 4 * 7 =     28
    }
}

// ✅ Expected output:
//    Func result: 8
//    NodeType:   Lambda
//    Body:       (a + b)
//    Body kind:  Add
//    Params:     a, b
//    Compiled:   8
//    Manual:     (a, b) => (a * b)
//    4 * 7 =     28

Your turn. The program below is almost complete — store a lambda as an expression tree, then compile it so you can call it. Fill in the two blanks marked ___ using the hints, then run it.

using System;
using System.Linq.Expressions;

class Program
{
    static void Main()
    {
        // 🎯 YOUR TURN — fill in the blanks marked with ___, then run it.

        // 1) Store the lambda  (a, b) => a + b  as an EXPRESSION TREE,
        //    NOT as a Func. The type on the left already says Expression<...>.
        Expression<Func<int, int, int>> addExpr = ___;   // 👉 (a, b) => a + b

        // 2) Turn the tree into a runnable delegate.
        //    An expression is DATA — you must compile it before you can call it.
        Func<int, int, int> add = addExpr.___;           // 👉 the method is Compile()

        // These lines already work once your two blanks are correct:
        Console.WriteLine($"Body: {addExpr.Body}");      // (a + b)
        Console.WriteLine($"2 + 40 = {add(2, 40)}");

        // ✅ Expected output:
        //    Body: (a + b)
        //    2 + 40 = 42
    }
}

2. Building a Tree by Hand

The compiler builds a tree for you from a lambda, but you can also assemble one node by node with the Expression factory methods — that's how dynamic code does it when the logic isn't known until runtime. The pattern is always the same: make the parameter nodes with Expression.Parameter, combine them into a body (Expression.Add, Expression.Subtract, Expression.Multiply, …), then wrap the body and its parameters in an Expression.Lambda<T>. Compile, and you have a delegate. Now you try: build (x, y) => x - y by hand.

using System;
using System.Linq.Expressions;

class Program
{
    static void Main()
    {
        // 🎯 YOUR TURN — build the expression  (x, y) => x - y  by hand.
        // Fill in the blanks, then run it.

        // 1) Two parameter nodes named "x" and "y", both of type int.
        ParameterExpression x = Expression.Parameter(typeof(int), "x");
        ParameterExpression y = Expression.Parameter(typeof(___), "y");  // 👉 the type:  int

        // 2) A subtraction node:  x - y
        //    The factory method for subtraction is Expression.Subtract(...).
        BinaryExpression body = Expression.___(x, y);    // 👉 Subtract(x, y)

        // 3) Wrap it in a lambda whose parameters are x and y, then compile.
        var lambda = Expression.Lambda<Func<int, int, int>>(body, x, y);
        Func<int, int, int> subtract = lambda.Compile();

        Console.WriteLine($"Tree:       {lambda}");      // (x, y) => (x - y)
        Console.WriteLine($"Body kind:  {body.NodeType}");// Subtract
        Console.WriteLine($"10 - 4 =    {subtract(10, 4)}");

        // ✅ Expected output:
        //    Tree:       (x, y) => (x - y)
        //    Body kind:  Subtract
        //    10 - 4 =    6
    }
}

3. Why LINQ Providers & ORMs Need Trees

This is the reason expression trees exist. When you write dbContext.Products.Where(p => p.Price > 100), Entity Framework Core doesn't run that lambda in C#. It can't — the data is in a database. Instead, Where on an IQueryable<T> takes an Expression, so EF receives the tree, walks it, and translates p.Price > 100 into WHERE [Price] > 100 SQL. A plain Func would be a sealed black box it could only run in memory — meaning it would download every row first. The tree is glass: the provider can see through it.

using System.Globalization;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;

class Product
{
    public string Name { get; set; } = "";
    public decimal Price { get; set; }
}

class Program
{
    static void Main()
    {
        // Currency formatting follows the thread's culture: {value:C} prints
        // £ in London, $ in Boston and € in Paris. Pin it when the output has
        // to be the same everywhere — as it does on a page that shows you the
        // result.
        CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("en-GB");

        var products = new List<Product>
        {
            new() { Name = "Laptop",   Price = 999m },
            new() { Name = "Mouse",    Price = 25m  },
            new() { Name = "Keyboard", Price = 75m  },
            new() { Name = "Monitor",  Price = 250m },
        };

        // A LINQ provider (like EF Core) receives the filter as an Expression,
        // not as compiled code, so it can READ the tree and translate it.
        // Here we use AsQueryable() so .Where takes Expression<Func<...>>.
        Expression<Func<Product, bool>> filter = p => p.Price > 100;

        // The provider can inspect the tree to build (for EF Core) SQL like:
        //   WHERE [p].[Price] > 100
        Console.WriteLine($"Filter tree: {filter.Body}");   // (p.Price > 100)

        var pricey = products.AsQueryable().Where(filter).ToList();
        Console.WriteLine("Over £100:");
        foreach (var p in pricey)
            Console.WriteLine($"  {p.Name}: {p.Price:C}");
        // Laptop: £999.00 / Monitor: £250.00

        // The key idea: a Func is a black box you can only RUN.
        // An Expression is glass — a provider can SEE 'Price > 100' and
        // turn it into SQL, a remote API call, or another query language.
    }
}

// ✅ Expected output:
//    Filter tree: (p.Price > 100)
//    Over £100:
//      Laptop: £999.00
//      Monitor: £250.00

4. Inspecting & Rewriting with ExpressionVisitor

Once you have a tree you'll often want to walk it — to inspect it, optimise it, or rewrite parts of it. ExpressionVisitor is the built-in walker: subclass it and override a Visit* method (like VisitBinary for +, -, > nodes) to intercept and replace nodes. Trees are immutable — you never edit one in place, you return a new tree. This example swaps every addition for a multiplication, then inspects a node's parts directly.

using System;
using System.Linq.Expressions;

// An ExpressionVisitor walks a tree node by node. Override a Visit method
// to inspect or REPLACE nodes — here, swap every + for a *.
class AddToMultiplyVisitor : ExpressionVisitor
{
    protected override Expression VisitBinary(BinaryExpression node)
    {
        if (node.NodeType == ExpressionType.Add)
            return Expression.Multiply(Visit(node.Left), Visit(node.Right));
        return base.VisitBinary(node);   // leave other operators untouched
    }
}

class Program
{
    static void Main()
    {
        // Original tree:  (a, b) => a + b
        Expression<Func<int, int, int>> original = (a, b) => a + b;
        Console.WriteLine($"Original body: {original.Body}");  // (a + b)
        Console.WriteLine($"Original(3,4): {original.Compile()(3, 4)}"); // 7

        // Transform it: a NEW tree with * instead of + (trees are immutable).
        var rewritten = (Expression<Func<int, int, int>>)
            new AddToMultiplyVisitor().Visit(original);
        Console.WriteLine($"Rewritten body: {rewritten.Body}"); // (a * b)
        Console.WriteLine($"Rewritten(3,4): {rewritten.Compile()(3, 4)}"); // 12

        // Inspecting a tree by hand: each node knows its NodeType and parts.
        if (original.Body is BinaryExpression bin)
        {
            Console.WriteLine($"Op: {bin.NodeType}, Left: {bin.Left}, Right: {bin.Right}");
            // Op: Add, Left: a, Right: b
        }
    }
}

// ✅ Expected output:
//    Original body: (a + b)
//    Original(3,4): 7
//    Rewritten body: (a * b)
//    Rewritten(3,4): 12
//    Op: Add, Left: a, Right: b

🔎 Deep Dive: Compile() is not free

Storing a lambda as an Expression and calling .Compile() does real work at runtime — it generates IL on the fly. That's far slower than a lambda the C# compiler turned into a Func ahead of time, and slower still if you do it inside a loop.

// ❌ Recompiles the SAME tree on every iteration — wasteful.
foreach (var n in items)
    use(expr.Compile()(n));

// ✅ Compile ONCE, reuse the delegate.
var fn = expr.Compile();
foreach (var n in items)
    use(fn(n));

Rule of thumb: only reach for expression trees when you genuinely need code-as-data (a query provider, a dynamic rule engine, a mapper). For everyday logic a normal lambda is simpler and faster — compile once and cache the result if you must compile at all.

Pro Tips

Common Errors (and the fix)

📋 Quick Reference

TaskCodeResult
Store as a treeExpression<Func<int,int>> e = n => n*2;a tree
Run a treevar f = e.Compile(); f(5);10
Inspect a nodee.Body.NodeTypeMultiply
Parameter nodeExpression.Parameter(typeof(int),"a")a node
Add nodeExpression.Add(a, b)a node
Wrap a lambdaExpression.Lambda<Func<...>>(body, a, b)a tree
Rewrite a treenew MyVisitor().Visit(e)a new tree

Frequently Asked Questions

Q: What's the actual difference between Func<int,bool> and Expression<Func<int,bool>>?

A Func is compiled, runnable code — a black box you can only call. An Expression is a data structure describing that same code, which you can read, modify, or translate before (optionally) compiling it into a Func. Same lambda syntax, completely different capability.

Q: Why won't my expression lambda accept an if or curly braces?

C#'s lambda-to-tree conversion only supports a single expression (like n => n > 10), not a statement body with { braces, loops, or local variables. If you need statements you must build the tree explicitly with the factory methods, or just use a Func.

Q: How does Entity Framework turn my C# into SQL?

Your Where(p => ...) lands on an IQueryable<T> whose Where takes an Expression. EF walks that tree, recognises nodes like a property access and a > comparison, and emits the matching SQL — all without ever running your lambda in C#.

Q: Do I need expression trees for everyday code?

Rarely. Reach for them when you need code-as-data: a query provider, a dynamic rule/filter builder, an object mapper, or a mocking framework. For ordinary logic a normal lambda is simpler, faster, and clearer — don't add a tree where a Func will do.

Mini-Challenge: A Compiled Predicate

No blanks this time — just a brief and an outline to keep you on track. Build a predicate expression n => n > 10, compile it into a Func<int, bool>, and test it on a few values. Print the tree's body too, so you can see the code as data. Run it and check your output against the expected lines in the comments.

using System;
using System.Linq.Expressions;

class Program
{
    static void Main()
    {
        // 🎯 MINI-CHALLENGE: a compiled predicate
        // 1. Declare  Expression<Func<int, bool>> isBig = n => n > 10;
        //    (a predicate is just an expression that returns a bool).
        // 2. Compile it into a Func<int, bool> with .Compile().
        // 3. Test it on 5, 10 and 11 and print each result, e.g.
        //       Console.WriteLine($"5  -> {check(5)}");
        // 4. BONUS: print isBig.Body to see the tree (it shows "(n > 10)").
        //
        // ✅ Expected output:
        //    Body: (n > 10)
        //    5  -> False
        //    10 -> False
        //    11 -> True

        // your code here
    }
}

🎉 Lesson Complete

Practice quiz

What is the difference between Func<int,int> and Expression<Func<int,int>>?

  • They are identical
  • Expression is faster to call
  • Func is compiled runnable code; Expression is an inspectable data structure (a tree)
  • Func can be translated to SQL

Answer: Func is compiled runnable code; Expression is an inspectable data structure (a tree). A Func is compiled IL you can only run; an Expression is a tree of objects describing the code, which you can read, rewrite, then compile.

How do you run an expression tree?

  • Call .Compile() to turn it into a delegate first
  • Call it directly like a method
  • Use .Invoke() on the tree
  • Cast it to a Func

Answer: Call .Compile() to turn it into a delegate first. An Expression isn't callable. You must call .Compile() to produce a delegate, then invoke that delegate.

What does `addExpr(3, 5)` do if addExpr is an Expression<Func<int,int,int>>?

  • Returns 8
  • Returns the tree
  • Throws at runtime
  • Won't compile — an Expression isn't callable

Answer: Won't compile — an Expression isn't callable. Calling the tree directly won't compile; you need addExpr.Compile()(3, 5).

Which factory method makes a parameter node?

  • Expression.Lambda
  • Expression.Parameter
  • Expression.Add
  • Expression.Variable only

Answer: Expression.Parameter. Expression.Parameter(typeof(int), "a") creates a parameter node; you then combine nodes and wrap them in Expression.Lambda.

How do you inspect a node's kind (e.g. Add vs Subtract)?

  • .NodeType
  • .Compile()
  • .Invoke()
  • .GetType().Name only

Answer: .NodeType. Every node exposes its kind through .NodeType (e.g. Add, Subtract), and the lambda's body through .Body.

Why do LINQ providers and ORMs like EF Core need expressions, not Funcs?

  • Expressions run faster in memory
  • Funcs can't take parameters
  • They can read the tree and translate it into SQL
  • Expressions use less memory

Answer: They can read the tree and translate it into SQL. On an IQueryable<T>, Where takes an Expression so the provider can walk the tree and translate it into SQL — a Func would be a black box it could only run in memory.

What does ExpressionVisitor let you do?

  • Compile a tree to SQL directly
  • Walk a tree node by node to inspect or replace nodes
  • Run the tree on a remote server
  • Delete nodes from a tree in place

Answer: Walk a tree node by node to inspect or replace nodes. Subclass ExpressionVisitor and override a Visit method (like VisitBinary) to intercept and replace nodes as you walk the tree.

Are expression trees mutable?

  • Yes, you edit nodes in place
  • Only the root node is mutable
  • Only after Compile()
  • No — they are immutable; you return a new tree

Answer: No — they are immutable; you return a new tree. Trees are immutable; a visitor never edits one in place, it returns a brand-new tree (ExpressionVisitor.Visit returns the new one).

What kind of lambda body can be assigned to an Expression<Func<>>?

  • A statement body with braces and an if
  • Only a single expression (e.g. n => n > 10)
  • Any method body
  • Only a body with a return statement

Answer: Only a single expression (e.g. n => n > 10). Expression lambdas allow only a single expression. A statement body with { } braces, if, or local variables cannot be converted to an expression tree (CS0834).

What is the performance caveat with .Compile()?

  • It is free and instant
  • It only works once per tree
  • It generates IL at runtime and is expensive — compile once and reuse
  • It always runs on a background thread

Answer: It generates IL at runtime and is expensive — compile once and reuse. .Compile() generates IL on the fly at runtime, which is slow — especially in a loop. Compile once and cache the resulting delegate.

Continue this course

Related lessons