Skip to main content
    Courses/C#/Expression Trees

    Lesson 18 • Advanced Track

    Expression Trees

    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.

    What You'll Learn

    • Tell the difference between a compiled lambda (Func) and an expression tree (Expression<Func>)
    • Compile an expression tree into a runnable delegate with .Compile()
    • Build a tree by hand with Expression.Parameter, Expression.Add and Expression.Lambda
    • Inspect a node's structure via .Body and .NodeType
    • Understand why LINQ providers and ORMs need expressions to translate queries to SQL
    • Walk and rewrite a tree with ExpressionVisitor

    💡 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.

    Worked example: Func vs Expression, Compile & manual build

    Read every comment, run it, and check each output matches.

    Try it Yourself »
    C#
    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).
    ...

    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.

    🎯 Your turn: store an Expression, then Compile it

    Assign the lambda as a tree, compile it to a Func, and check the output.

    Try it Yourself »
    C#
    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 
    ...

    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.

    🎯 Your turn: build (x, y) => x - y manually

    Fill in the parameter type and the subtract factory method, then compile and run.

    Try it Yourself »
    C#
    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
            // 
    ...

    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.

    Worked example: a filter the provider can read

    See how an Expression filter exposes 'Price > 100' so a provider could translate it.

    Try it Yourself »
    C#
    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()
        {
            var products = new List<Product>
            {
                new() { Name = "Laptop",   Price = 999m },
                new() { Name = "Mouse",    Price = 25m  },
                new() { Name = "Keyboard", Price = 75m  },
                new() { Name = "Monitor",  
    ...

    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.

    Worked example: rewrite + into * with a visitor

    Override VisitBinary to transform a tree, then inspect a node by hand.

    Try it Yourself »
    C#
    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 un
    ...

    🔎 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

    • 💡 EF Core needs Expression, not Func: on an IQueryable<T>, .Where(expr) translates to SQL. Pass a Func and you fall back to IEnumerable, pulling every row into memory first.
    • 💡 Compile once, reuse: .Compile() generates IL at runtime and is expensive. Cache the resulting Func rather than recompiling.
    • 💡 Trees are immutable: you never modify a node — you build a new tree. ExpressionVisitor.Visit returns the new one.
    • 💡 Expression lambdas are single-expression only: n => n > 10 is fine; a statement body with { braces, if, or a local variable cannot be assigned to Expression<Func<>>.
    • 💡 Libraries make composition easy: LINQKit's PredicateBuilder combines Expression<Func<T,bool>> predicates with AND/OR far more cleanly than hand-building AndAlso nodes.

    Common Errors (and the fix)

    • Calling the tree directly: addExpr(3, 5) won't compile — an Expression isn't callable. Fix: addExpr.Compile()(3, 5), or compile once and call the delegate.
    • "CS0834: A lambda expression with a statement body cannot be converted to an expression tree": you wrote n => { return n > 10; }. Expression lambdas allow only a single expression. Fix: drop the braces — n => n > 10.
    • "CS1660 / cannot convert lambda to Func" when you meant a tree (or vice versa): you mixed up Expression<Func<int,bool>> and Func<int,bool>. They are different types — a tree is data, a Func is compiled code. Fix: match the variable's type to what you need (inspectable vs runnable).
    • "CS0103: The name 'Expression' does not exist in the current context": you're missing the namespace. Fix: add using System.Linq.Expressions; at the top of the file.
    • "InvalidOperationException: The LINQ expression could not be translated": EF Core couldn't turn part of your tree into SQL (e.g. a custom C# method). Fix: simplify the predicate, or call .AsEnumerable() first to finish that part in memory.

    📋 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.

    🎯 Mini-Challenge: build, compile & test a predicate

    Declare an Expression<Func<int,bool>>, compile it, and test it on 5, 10 and 11.

    Try it Yourself »
    C#
    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 
    ...

    🎉 Lesson Complete

    • Expression<Func<T>> stores a lambda as inspectable data; Func<T> stores it as compiled code
    • ✅ A tree is data — call .Compile() to turn it into a runnable delegate before invoking it
    • ✅ Build trees by hand with Expression.Parameter, Expression.Add and Expression.Lambda
    • ✅ Inspect any node through .Body and .NodeType
    • ✅ LINQ providers and ORMs need the tree so they can translate your query to SQL
    • ExpressionVisitor walks and rewrites trees, always returning a new (immutable) tree
    • Next lesson: Reflection — inspecting types, methods, and attributes at runtime

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service

    Install LearnCodingFast

    Learn faster with the app on your home screen.