Expression Trees
By the Syncfusion DevTutorials team · Last updated
Store code as data: Expression and lambda trees, building nodes by hand, compiling and executing, ExpressionVisitor, dynamic queries, IQueryable and how ORMs translate to SQL.
Introduction to Expression Trees
Think about two ways to share a recipe.
You can cook the dish and hand over the plate. The other person can eat it, but they cannot see how it was made.
Or you can write the recipe down. Now the other person can read each step, change an ingredient, translate it into another language, and then cook it.
A normal lambda like x => x > 5 is the plate. It is already compiled and ready to run. No one can look inside to see how it works.
An expression tree is the written recipe. It stores the same lambda as data: a tree of small pieces that explains "take the input x, take the number 5, compare them with greater-than." Your code can read that tree, change it, turn it into SQL, or compile it and run it.
In simple words: an expression tree is code saved as data, so other code can read and work with it.
Easy Way to Remember: a delegate is the cooked dish. An expression tree is the recipe.
What You Will Learn
In this lesson, you will learn:
- What an
Expressionis and how it works differently from a delegate. - How the compiler turns a lambda into an expression tree.
- How to build expression trees yourself.
- The main kinds of expression pieces (called nodes).
- How to compile and run an expression tree.
- How to read and change trees with
ExpressionVisitor. - How to build searches and filters at runtime.
- How LINQ and databases like Entity Framework use expression trees.
All runnable examples on this page run in your browser. Compiling expression trees works here too. Examples that need a real database, like Entity Framework Core, are shown as plain code. The runnable examples use in-memory data instead.
Start With a Working Example
Press Run.
The same lambda as a delegate and as data
using System;
using System.Linq.Expressions;
Func<int, bool> asCode = x => x > 5;
Expression<Func<int, bool>> asData = x => x > 5;
Console.WriteLine($"Delegate result: {asCode(8)}");
Console.WriteLine($"Delegate as text: {asCode}");
Console.WriteLine($"Tree as text: {asData}");
Console.WriteLine($"Tree body: {asData.Body}");
Console.WriteLine($"Body node type: {asData.Body.NodeType}");
var compare = (BinaryExpression)asData.Body;
Console.WriteLine($"Left side: {compare.Left}");
Console.WriteLine($"Right side: {compare.Right}");
Both lines have the same lambda: x => x > 5. The only difference is the declared type.
- As a
Func<int, bool>, it is compiled code. Printing it gives you no useful information. - As an
Expression<Func<int, bool>>, it is a tree. You can print it and look inside: left side, right side, and operator.
Try This
Change both lambdas to x => x % 2 == 0 and run again. Look at the Body node type line. It changes from GreaterThan to Equal.
Expression
System.Linq.Expressions.Expression is the base class for every piece of a tree. Each piece is called a node.
Every node has two key parts:
NodeTypetells you what the node does:Add,Constant,Parameter,Call,GreaterThanand so on.Typetells you what .NET type the node produces when it runs:int,bool,string.
Nodes hold other nodes inside. A BinaryExpression like a + b holds a Left node and a Right node. That is why it is called a tree: one top node with branches going down to the leaves.
Walk down the tree
using System;
using System.Linq.Expressions;
Expression<Func<int, int, int>> price = (qty, rate) => qty * rate + 50;
Show(price.Body, 0);
static void Show(Expression node, int depth)
{
string indent = new string(' ', depth * 2);
Console.WriteLine($"{indent}{node.NodeType} ({node.Type.Name}): {node}");
if (node is BinaryExpression b)
{
Show(b.Left, depth + 1);
Show(b.Right, depth + 1);
}
}
The top node is Add. Its left branch is the Multiply. Its right branch is the number 50. The bottom pieces are the inputs qty and rate, and the number.
Add
/ \
Multiply 50
/ \
qty rate
Lambda Expressions
The easiest way to get an expression tree is to let the compiler build it.
When you save a lambda to a variable of type Expression<TDelegate>, the compiler does not turn the lambda into code. Instead, it writes code that builds the tree.
The top of the tree is a LambdaExpression. It has:
Parameters: what the lambda takes as input,Body: the expression after the=>,ReturnType: what the lambda gives back.
Inside a LambdaExpression
using System;
using System.Linq.Expressions;
Expression<Func<string, int, bool>> rule = (name, age) => name.Length > 2 && age >= 18;
Console.WriteLine($"Whole lambda: {rule}");
Console.WriteLine($"Returns: {rule.ReturnType.Name}");
foreach (ParameterExpression p in rule.Parameters)
Console.WriteLine($"Parameter: {p.Name} : {p.Type.Name}");
Console.WriteLine($"Body: {rule.Body}");
Console.WriteLine($"Body type: {rule.Body.NodeType}");
What the Compiler Cannot Convert
Only expression lambdas (a single expression after =>) can become trees. A statement lambda with braces cannot.
Expression<Func<int, int>> ok = x => x * 2;
Expression<Func<int, int>> bad = x => { return x * 2; };
// error CS0834: A lambda expression with a statement body
// cannot be converted to an expression tree
A few other things are not allowed either, like await, assignments with =, and some newer syntax. The compiler tells you with an error when you use one.
Building Expression Trees
You can also build a tree by hand, using the static factory methods on the Expression class. This is how you create code whose shape you only learn while the program runs.
Each factory method makes one node:
| Method | Makes | Like writing |
|---|---|---|
Expression.Parameter(typeof(int), "x") | An input | x |
Expression.Constant(10) | A fixed number | 10 |
Expression.Add(a, b) | Addition | a + b |
Expression.GreaterThan(a, b) | Comparison | a > b |
Expression.Property(obj, "Name") | Property read | obj.Name |
Expression.Call(obj, method, args) | Method call | obj.Method(args) |
Expression.Lambda<T>(body, params) | The full lambda | x => body |
You build from the leaves up: first the inputs and numbers, then the operators that combine them, and finally the lambda.
Build x => x * 2 + 1 by hand
using System;
using System.Linq.Expressions;
// 1. Leaves
ParameterExpression x = Expression.Parameter(typeof(int), "x");
ConstantExpression two = Expression.Constant(2);
ConstantExpression one = Expression.Constant(1);
// 2. Branches
BinaryExpression times = Expression.Multiply(x, two);
BinaryExpression plus = Expression.Add(times, one);
// 3. Root
Expression<Func<int, int>> lambda = Expression.Lambda<Func<int, int>>(plus, x);
Console.WriteLine(lambda);
Func<int, int> run = lambda.Compile();
Console.WriteLine($"f(5) = {run(5)}");
The tree you build by hand prints just like a lambda you typed: x => ((x * 2) + 1).
Building With Properties and Method Calls
Real code works with objects. Expression.Property reads a property. Expression.Call calls a method.
Build a StartsWith filter by hand
using System;
using System.Linq.Expressions;
ParameterExpression p = Expression.Parameter(typeof(Person), "p");
MemberExpression name = Expression.Property(p, nameof(Person.Name));
var startsWith = typeof(string).GetMethod("StartsWith", new[] { typeof(string) })!;
MethodCallExpression call = Expression.Call(name, startsWith, Expression.Constant("A"));
var filter = Expression.Lambda<Func<Person, bool>>(call, p);
Console.WriteLine(filter);
var test = filter.Compile();
Console.WriteLine($"Asha: {test(new Person("Asha"))}");
Console.WriteLine($"Ravi: {test(new Person("Ravi"))}");
record Person(string Name);
Expression.Call needs a MethodInfo, so this is where reflection from lesson 13 comes back. Expression trees and reflection work together often.
Hand-built trees are checked when you build them, not when you write them. A wrong type, like adding an int to a string, throws an InvalidOperationException or ArgumentException from the factory method while the program runs. Build trees step by step and print them as you go.
Expression Nodes
There are about 85 node types, but a small number cover most real code.
| Node class | NodeType examples | Represents |
|---|---|---|
ParameterExpression | Parameter | An input, like x |
ConstantExpression | Constant | A fixed value, like 5 or "Pune" |
BinaryExpression | Add, Equal, AndAlso, GreaterThan | Two things and an operator |
UnaryExpression | Not, Negate, Convert | One thing |
MemberExpression | MemberAccess | A property or field, like p.Age |
MethodCallExpression | Call | A method call |
ConditionalExpression | Conditional | test ? a : b |
NewExpression | New | new Point(1, 2) |
LambdaExpression | Lambda | The full lambda |
You check what kind a node is with pattern matching, like in the next example.
Describe every node in a tree
using System;
using System.Linq.Expressions;
Expression<Func<Order, string>> label =
o => o.Total > 1000 ? "Big: " + o.Customer.ToUpper() : "Small";
Describe(label.Body, 0);
static void Describe(Expression node, int depth)
{
string pad = new string(' ', depth * 2);
switch (node)
{
case ConditionalExpression c:
Console.WriteLine($"{pad}Conditional");
Describe(c.Test, depth + 1);
Describe(c.IfTrue, depth + 1);
Describe(c.IfFalse, depth + 1);
break;
case BinaryExpression b:
Console.WriteLine($"{pad}Binary {b.NodeType}");
Describe(b.Left, depth + 1);
Describe(b.Right, depth + 1);
break;
case MethodCallExpression m:
Console.WriteLine($"{pad}Call {m.Method.Name}");
if (m.Object != null) Describe(m.Object, depth + 1);
break;
case MemberExpression m:
Console.WriteLine($"{pad}Member {m.Member.Name}");
break;
case ConstantExpression k:
Console.WriteLine($"{pad}Constant {k.Value}");
break;
default:
Console.WriteLine($"{pad}{node.NodeType}");
break;
}
}
class Order
{
public decimal Total { get; set; }
public string Customer { get; set; } = "";
}
Notice the 1000 constant. It shows as 1000 even though Total is a decimal, because the compiler stored it as a decimal constant. Trees keep the exact types the compiler used.
Compiling Expressions
A tree is data. To run it, you must turn it into a delegate. That is what Compile() does.
Compile() reads the tree and makes real code that can run. It gives back a delegate with the same type as the lambda: an Expression<Func<int, int>> compiles to a Func<int, int>.
Compile a tree into a delegate
using System;
using System.Linq.Expressions;
Expression<Func<decimal, decimal, decimal>> withTax = (price, rate) => price + price * rate / 100;
Func<decimal, decimal, decimal> calculate = withTax.Compile();
Console.WriteLine(withTax);
Console.WriteLine($"Rs. 1000 at 18% = {calculate(1000m, 18m)}");
Console.WriteLine($"Rs. 250 at 5% = {calculate(250m, 5m)}");
Compile() is slow: it makes new code every time you call it. Compile a tree one time, keep the delegate in a field or dictionary, and call the delegate as many times as you want. Calling the delegate is as fast as calling a normal method.
This is the expression-tree speed boost promised in the reflection lesson. You can build a fast property reader for any type and property name:
A fast getter built from a property name
using System;
using System.Linq.Expressions;
Func<object, object?> getter = MakeGetter(typeof(Book), "Pages");
var books = new[] { new Book { Title = "Godan", Pages = 312 }, new Book { Title = "Malgudi Days", Pages = 256 } };
foreach (var book in books)
Console.WriteLine($"{book.Title}: {getter(book)} pages");
static Func<object, object?> MakeGetter(Type type, string propertyName)
{
// (object obj) => (object)((Book)obj).Pages
ParameterExpression obj = Expression.Parameter(typeof(object), "obj");
UnaryExpression cast = Expression.Convert(obj, type);
MemberExpression property = Expression.Property(cast, propertyName);
UnaryExpression boxed = Expression.Convert(property, typeof(object));
return Expression.Lambda<Func<object, object?>>(boxed, obj).Compile();
}
class Book
{
public string Title { get; set; } = "";
public int Pages { get; set; }
}
Serializers and object mappers use this exact trick. They use reflection once to find properties, build a reader tree for each one, compile it, and save the delegates.
Executing Expressions
After you compile, you run the delegate like any other method: calculate(1000m, 18m).
There are two other ways to run a tree:
DynamicInvokeon aLambdaExpressionwhen you do not know its delegate type when you build. It takes anobject[]and is slower.- Expression blocks let one tree hold several steps, variables and loops. You can only build them by hand.
Run a tree you only know as LambdaExpression
using System;
using System.Linq.Expressions;
LambdaExpression[] formulas =
{
(Expression<Func<int, int>>)(n => n * n),
(Expression<Func<int, int, int>>)((a, b) => a * b + 1),
};
foreach (LambdaExpression formula in formulas)
{
Delegate run = formula.Compile();
object?[] inputs = formula.Parameters.Count == 1 ? new object?[] { 7 } : new object?[] { 3, 4 };
Console.WriteLine($"{formula} with ({string.Join(", ", inputs)}) = {run.DynamicInvoke(inputs)}");
}
Expression Blocks
A BlockExpression runs several expressions one after another, like statements inside braces. It can declare temporary variables. The value of the block is what its last expression gives.
A hand-built block with a temporary variable
using System;
using System.Linq.Expressions;
// int Discount(int price) { int cut = price / 10; Console.WriteLine(cut); return price - cut; }
ParameterExpression price = Expression.Parameter(typeof(int), "price");
ParameterExpression cut = Expression.Variable(typeof(int), "cut");
var writeLine = typeof(Console).GetMethod("WriteLine", new[] { typeof(int) })!;
BlockExpression body = Expression.Block(
new[] { cut },
Expression.Assign(cut, Expression.Divide(price, Expression.Constant(10))),
Expression.Call(writeLine, cut),
Expression.Subtract(price, cut));
var discount = Expression.Lambda<Func<int, int>>(body, price).Compile();
Console.WriteLine($"Final price: {discount(850)}");
Expression Visitors
To read or change a whole tree, you visit every node. Writing that walk by hand for 85 node types is a lot of work.
ExpressionVisitor does the walk for you. You inherit from it and override only the Visit... methods for the nodes you care about:
VisitBinaryfor+,>,&&and others,VisitMemberfor property reads,VisitConstantfor numbers,VisitParameterfor inputs,VisitMethodCallfor method calls.
Reading a Tree
This visitor collects every property name a lambda reads.
Find the properties a lambda reads
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
Expression<Func<Employee, bool>> rule = e => e.Salary > 50000 && e.City == "Pune" || e.Years > 10;
var finder = new PropertyFinder();
finder.Visit(rule);
Console.WriteLine($"Uses: {string.Join(", ", finder.Names)}");
class PropertyFinder : ExpressionVisitor
{
public List<string> Names { get; } = new();
protected override Expression VisitMember(MemberExpression node)
{
Names.Add(node.Member.Name);
return base.VisitMember(node); // keep walking into child nodes
}
}
class Employee
{
public decimal Salary { get; set; }
public string City { get; set; } = "";
public int Years { get; set; }
}
Rewriting a Tree
Expression trees cannot be changed: you cannot edit a node. A visitor gives back a new node instead. ExpressionVisitor rebuilds the tree around it.
This visitor swaps every + for -.
Rewrite + into -
using System;
using System.Linq.Expressions;
Expression<Func<int, int, int>> original = (a, b) => a + b * 2 + 10;
var flipped = (Expression<Func<int, int, int>>)new PlusToMinus().Visit(original);
Console.WriteLine($"Original: {original} -> {original.Compile()(5, 3)}");
Console.WriteLine($"Rewritten: {flipped} -> {flipped.Compile()(5, 3)}");
class PlusToMinus : ExpressionVisitor
{
protected override Expression VisitBinary(BinaryExpression node)
{
Expression left = Visit(node.Left);
Expression right = Visit(node.Right);
return node.NodeType == ExpressionType.Add
? Expression.Subtract(left, right)
: node.Update(left, node.Conversion, right);
}
}
The original tree stays the same. The visitor made a second, different tree.
Dynamic Query Generation
Here is the most useful real-world use of expression trees: building a filter while the program runs based on what a user picks.
Imagine a search page. The user picks a field, an operator and a value. You do not know what they will pick, so you cannot write the lambda ahead of time. You build it instead.
Build a filter from user choices
using System;
using System.Linq;
using System.Linq.Expressions;
var products = new[]
{
new Product("Kettle", "Kitchen", 1299m),
new Product("Mixer", "Kitchen", 3499m),
new Product("Lamp", "Home", 899m),
new Product("Fan", "Home", 2499m),
};
Run("Category", "==", "Kitchen");
Run("Price", ">", 1000m);
Run("Price", "<=", 899m);
void Run(string field, string op, object value)
{
Expression<Func<Product, bool>> filter = Build<Product>(field, op, value);
var names = products.AsQueryable().Where(filter).Select(p => p.Name);
Console.WriteLine($"{filter,-38} => {string.Join(", ", names)}");
}
static Expression<Func<T, bool>> Build<T>(string field, string op, object value)
{
ParameterExpression p = Expression.Parameter(typeof(T), "p");
MemberExpression left = Expression.Property(p, field);
ConstantExpression right = Expression.Constant(value, left.Type);
Expression body = op switch
{
"==" => Expression.Equal(left, right),
">" => Expression.GreaterThan(left, right),
"<" => Expression.LessThan(left, right),
">=" => Expression.GreaterThanOrEqual(left, right),
"<=" => Expression.LessThanOrEqual(left, right),
_ => throw new ArgumentException($"Unknown operator {op}"),
};
return Expression.Lambda<Func<T, bool>>(body, p);
}
record Product(string Name, string Category, decimal Price);
Combining Filters With And
Users usually pick multiple filters. You combine them with Expression.AndAlso, which is the tree version of &&.
Each filter has its own input p, so the combined tree must use one shared input. A small visitor swaps the inputs.
Combine two filters into one
using System;
using System.Linq;
using System.Linq.Expressions;
Expression<Func<Product, bool>> inKitchen = p => p.Category == "Kitchen";
Expression<Func<Product, bool>> cheap = x => x.Price < 2000m;
var both = And(inKitchen, cheap);
Console.WriteLine(both);
var products = new[]
{
new Product("Kettle", "Kitchen", 1299m),
new Product("Mixer", "Kitchen", 3499m),
new Product("Lamp", "Home", 899m),
};
Console.WriteLine(string.Join(", ", products.AsQueryable().Where(both).Select(p => p.Name)));
static Expression<Func<T, bool>> And<T>(Expression<Func<T, bool>> first, Expression<Func<T, bool>> second)
{
ParameterExpression shared = first.Parameters[0];
Expression secondBody = new SwapParameter(second.Parameters[0], shared).Visit(second.Body);
return Expression.Lambda<Func<T, bool>>(Expression.AndAlso(first.Body, secondBody), shared);
}
class SwapParameter(ParameterExpression from, ParameterExpression to) : ExpressionVisitor
{
protected override Expression VisitParameter(ParameterExpression node) => node == from ? to : node;
}
record Product(string Name, string Category, decimal Price);
Expression Trees in LINQ
Now you see why LINQ has two sets of methods with the same names.
Enumerable.Whereworks onIEnumerable<T>. It takes aFunc<T, bool>delegate and runs it on each item in memory.Queryable.Whereworks onIQueryable<T>. It takes anExpression<Func<T, bool>>tree and does not run it right away. It adds the tree to the query.
You write the same lambda for both. The compiler picks a delegate or a tree based on which method you call.
IQueryable collects a tree instead of running code
using System;
using System.Linq;
int[] marks = { 35, 72, 91, 58, 44 };
var inMemory = marks.Where(m => m >= 50); // IEnumerable<int>
var asQuery = marks.AsQueryable().Where(m => m >= 50) // IQueryable<int>
.OrderBy(m => m);
Console.WriteLine($"Enumerable: {string.Join(", ", inMemory)}");
Console.WriteLine($"Queryable: {string.Join(", ", asQuery)}");
Console.WriteLine("The query's tree:");
Console.WriteLine($" {asQuery.Expression}");
Console.WriteLine($" Root node: {asQuery.Expression.NodeType}");
asQuery.Expression holds the whole query as a tree: the source, then the Where, then the OrderBy. A query provider decides what to do with that tree. Here, the provider is the in-memory one, so it compiles and runs it. A database provider does something different.
If you save a filter in a Func<T, bool> variable and pass it to Where on an IQueryable, C# picks Enumerable.Where. The whole table is loaded into memory first, then filtered. Keep database filters as Expression<Func<T, bool>> so they stay translatable.
Expression Trees in ORM Frameworks
An ORM (Object-Relational Mapper) like Entity Framework Core lets you ask a database using LINQ. It cannot run your C# lambda inside SQL Server. So it reads the tree instead and translates each node into SQL.
With EF Core, this C# query:
var bigOrders = db.Orders
.Where(o => o.Total > 1000 && o.City == "Pune")
.OrderBy(o => o.Total)
.Select(o => o.Id)
.ToList();
becomes SQL that looks like this, which runs on the database server:
SELECT [o].[Id]
FROM [Orders] AS [o]
WHERE [o].[Total] > 1000.0 AND [o].[City] = N'Pune'
ORDER BY [o].[Total]
This needs a real database, so it is shown as plain code. But the core idea is just an ExpressionVisitor. Here is a tiny translator that turns a filter tree into a SQL WHERE clause:
A mini ORM: translate a filter into SQL
using System;
using System.Linq.Expressions;
using System.Text;
Console.WriteLine(ToSql<Order>(o => o.Total > 1000 && o.City == "Pune"));
Console.WriteLine(ToSql<Order>(o => o.City != "Delhi" || o.Total <= 500));
static string ToSql<T>(Expression<Func<T, bool>> filter)
{
var writer = new SqlWriter();
writer.Visit(filter.Body);
return $"SELECT * FROM {typeof(T).Name}s WHERE {writer.Sql}";
}
class SqlWriter : ExpressionVisitor
{
private readonly StringBuilder _sql = new();
public string Sql => _sql.ToString();
protected override Expression VisitBinary(BinaryExpression node)
{
_sql.Append('(');
Visit(node.Left);
_sql.Append(node.NodeType switch
{
ExpressionType.AndAlso => " AND ",
ExpressionType.OrElse => " OR ",
ExpressionType.Equal => " = ",
ExpressionType.NotEqual => " <> ",
ExpressionType.GreaterThan => " > ",
ExpressionType.LessThanOrEqual => " <= ",
_ => throw new NotSupportedException(node.NodeType.ToString()),
});
Visit(node.Right);
_sql.Append(')');
return node;
}
protected override Expression VisitMember(MemberExpression node)
{
_sql.Append($"[{node.Member.Name}]");
return node;
}
protected override Expression VisitConstant(ConstantExpression node)
{
_sql.Append(node.Value is string s ? $"'{s.Replace("'", "''")}'" : node.Value);
return node;
}
}
class Order
{
public decimal Total { get; set; }
public string City { get; set; } = "";
}
Real ORMs handle many more node types, use SQL parameters instead of pasting values, and support joins. But the way works the same: visit the tree and write SQL for each node.
This also explains a common EF Core error. If your lambda calls your own C# method, the ORM finds a MethodCallExpression it does not know how to turn into SQL:
db.Orders.Where(o => IsVip(o.Customer)).ToList();
// InvalidOperationException: The LINQ expression '...IsVip(o.Customer)'
// could not be translated.
The ORM can only translate things it knows. Your private C# method does not exist inside the database.
Where Expression Trees Are Used
- Entity Framework Core and other ORMs turn LINQ into SQL.
- Dynamic filters in search pages and grids build
Whereclauses while the program runs. - Mapping libraries like AutoMapper compile fast copy code.
- Validation libraries like FluentValidation read
x => x.Emailto get the property name. - Mocking libraries like Moq read
m => m.Send(It.IsAny<string>())to set up fakes. - Serializers and DI containers compile fast readers and constructors.
Common mistakes
| Mistake | Symptom | Fix |
|---|---|---|
| Calling a tree directly | CS0149 | Compile() first |
| Statement lambda as a tree | CS0834 | Use a single expression |
| Two different inputs | "variable referenced from scope" | Swap inputs with a visitor |
| Mixed types in a hand-built node | InvalidOperationException | Match types, or use Expression.Convert |
Func passed to an IQueryable | Whole table loaded into memory | Keep it as Expression<Func<...>> |
Compile() in a loop | Slow code | Compile one time and keep it |
| Own method inside an EF Core query | "could not be translated" | Move the logic outside the query |
What to Remember
- An expression tree stores code as data that other code can read.
Func<...>is compiled code.Expression<Func<...>>is a tree of the same lambda.- Every piece of a tree is a node with a
NodeTypeand aType. - Only single-expression lambdas can become trees.
- Build trees by hand with
Expression.Parameter,Constant,Add,Property,CallandLambda. Compile()turns a tree into a delegate. It is slow, so compile one time and keep the delegate.ExpressionVisitorreads or changes trees. Trees never change. Visitors give back new ones.- Combine filters with
Expression.AndAlsoand one shared input. IQueryablecollects a tree. A provider decides how to run it.- ORMs like EF Core translate trees into SQL. They can only handle what they understand.
Next: Dependency Injection