Module 1 · Expression Trees, Delegates and Generics · Lesson 1 of 8
Expression trees
1.1 What is an expression tree?
When you write a lambda expression in a LINQ query, the compiler usually turns it into a delegate — compiled IL code that executes directly. An expression tree takes a different approach: it represents the code as a data structure (a tree) instead of compiled instructions. Each node of the tree is an Expression that represents an operation (method call, binary operation, property access, etc.). According to the official documentation, expression trees let you examine, modify or execute code at run time. Entity Framework and other ORMs use them to analyze your lambda expressions and translate them to SQL.
Think of cooking: you could cook a dish yourself (delegate), or you could write down the recipe for someone else to follow (expression tree). The recipe can be inspected, modified, translated into another language, or combined with other recipes. Similarly, an expression tree is like a recipe for code that another system (like a database provider) can interpret and translate.
1.2 Creating expression trees
There are two primary ways to create an expression tree:
- Let the compiler generate it for you. When you assign a lambda to a variable of type
Expression<TDelegate>, the compiler builds an expression tree instead of a compiled delegate.
Example:
Expression<Func<int, bool>> isSmall = num => num < 5;
- Build it manually. The
System.Linq.Expressionsnamespace exposes classes such asExpression.Parameter,Expression.Constant,Expression.Add, etc., which you can use to compose a tree one node at a time. This is useful when constructing queries dynamically at runtime.
Expression trees are immutable. To modify one, you must build a new tree (often by using an ExpressionVisitor to walk the existing tree and replace nodes). They only support expression lambdas, not statement lambdas, and many language features have no direct representation. For example, await expressions and async lambdas are not supported in expression trees, so you cannot build asynchronous expression trees.
1.3 Delegates versus expression trees
A delegate is compiled IL code. When the runtime loads your program, the method body is compiled to IL (intermediate language), and the JIT compiler later turns that IL into native instructions. A regular delegate (e.g., Func<Person, bool>) is like a fully prepared meal — you can eat it but cannot easily inspect or rearrange its ingredients.
An expression tree preserves the structure of the code. A Expression<Func<Person, bool>> stores a tree of nodes such as ParameterExpression (the person parameter), MemberExpression (reading person.Age) and BinaryExpression (the > operation). This structure can be inspected, modified and even translated to other forms, such as SQL. A Medium article summarises this difference: delegates are compiled to IL and executed directly, while expression trees maintain the code as a tree that can be inspected or modified. The same article shows how a lambda like person => person.Age > 18 becomes a tree of nodes with a LambdaExpression at the root, a BinaryExpression for the comparison and MemberExpression / ConstantExpression for the operands.
1.4 Simple example: building and executing an expression tree
// reference: System.Linq.Expressions Expression<Func<int, int, int>> addExpr = (a, b) => a + b; // examine the tree Console.WriteLine(addExpr.Body); // prints "(a + b)" Console.WriteLine(addExpr.Parameters[0].Name); // prints "a" // compile to a delegate and execute Func<int, int, int> addFunc = addExpr.Compile(); Console.WriteLine(addFunc(2, 3)); // prints 5
Expression<TDelegate>.Compile() produces an actual delegate that can be executed. The compiled delegate will run almost as fast as a manually written lambda because the heavy work happens when the tree is compiled.
1.5 More complex example: dynamic filtering
Imagine an API that allows users to filter a list of products by price range, category and rating. At design time you cannot know which filters will be applied, so you need to build the predicate dynamically. You could concatenate strings to produce a SQL WHERE clause, but that is error-prone. Instead, build an expression tree and let Entity Framework translate it to SQL:
public static Expression<Func<Product, bool>> BuildProductFilter(decimal?
minPrice, decimal? maxPrice, int? categoryId)
{
// parameter: product =>
ParameterExpression param = Expression.Parameter(typeof(Product),
"product");
Expression body = Expression.Constant(true); // start with 'true'
if (minPrice != null)
{
Expression prop = Expression.Property(param, nameof(Product.Price));
Expression min = Expression.Constant(minPrice.Value);
body = Expression.AndAlso(body, Expression.GreaterThanOrEqual(prop,
min));
}
if (maxPrice != null)
{
Expression prop = Expression.Property(param, nameof(Product.Price));
Expression max = Expression.Constant(maxPrice.Value);
body = Expression.AndAlso(body, Expression.LessThanOrEqual(prop, max));
}
if (categoryId != null)
{
Expression prop = Expression.Property(param,
nameof(Product.CategoryId));
Expression cat = Expression.Constant(categoryId.Value);
body = Expression.AndAlso(body, Expression.Equal(prop, cat));
}
return Expression.Lambda<Func<Product, bool>>(body, param);
}You can then use the resulting expression in a LINQ query:
dbContext.Products.Where(filterExpression);
EF Core will translate the expression tree into SQL. This pattern is common in ORMs, query builders and dynamic rule engines.
1.6 When to use expression trees
- Dynamic query generation and translation. ORMs like Entity Framework, Dapper or MongoDB drivers rely on expression trees to translate C# expressions into SQL, NoSQL queries or other languages. Without expression trees the provider cannot inspect your lambda to generate the correct query.
- Caching compiled expressions for performance. When you repeatedly invoke reflection (e.g., reading property values from objects), you can build expression trees once, compile them to delegates and cache them. Compiled delegates are faster than repeated reflection but slower than direct property access, so caching avoids overhead.
- Metaprogramming and DSLs. Expression trees enable frameworks like System.Linq.Dynamic and libraries such as [AutoMapper] to build expression-based domain-specific languages. They allow you to interpret code as data and transform it into other forms (e.g., building a filter builder for UI grids).
- Interoperability with dynamic languages. The Dynamic Language Runtime (DLR) uses expression trees to represent code from dynamic languages in a form that the CLR can execute.
1.7 When not to use expression trees
Expression trees come with limitations and overhead. They cannot represent many newer C# constructs (null-coalescing assignment, pattern matching, async/await, etc.). They are also cumbersome to build manually and slower than direct delegate invocation. Use regular delegates or compiled lambdas when you simply need to call a function; only build expression trees when you must inspect or translate the code.
1.8 Performance considerations
An expression tree is built and compiled at run time. The initial construction and compilation cost is higher than writing the lambda directly. Once compiled, the delegate is almost as fast as a hand-written method, but building it repeatedly can degrade performance. Therefore, cache compiled expressions when they will be reused. Also note that providers like Entity Framework analyze the tree and generate SQL; extremely complex expression trees can lead to complex queries that hurt database performance.
1.9 Additional examples of expression trees
To deepen your understanding, let's explore how expression trees look internally and how you can inspect them in a console app.
Decomposing a simple comparison
The following code creates an expression tree for the lambda num => num < 5 and then inspects its components. It shows that the tree has a LambdaExpression node whose body is a BinaryExpression (LessThan) with a ParameterExpression on the left and a ConstantExpression on the right. The Microsoft documentation shows how to decompose this tree.
using System;
using System.Linq.Expressions;
Expression<Func<int, bool>> exprTree = num => num < 5;
// Decompose the expression tree
ParameterExpression param = (ParameterExpression)exprTree.Parameters[0];
BinaryExpression body = (BinaryExpression)exprTree.Body;
ParameterExpression left = (ParameterExpression)body.Left;
ConstantExpression right = (ConstantExpression)body.Right;
Console.WriteLine($"{param.Name} => {left.Name} {body.NodeType} {right.Value}");
// Output: num => num LessThan 5This example demonstrates that an expression tree stores the structure of your lambda rather than compiling it to IL. By inspecting the nodes you can see the parameter name, the operation and the constant. Tools such as ExpressionVisitor (in System.Linq.Expressions) allow you to recursively traverse or modify nodes. The Interpret Expressions article shows how to write visitors to print or transform expression trees.
Examining constants and simple arithmetic
You can create other expression tree nodes manually. For example, the following code builds a constant node and prints its type and value:
var constant = Expression.Constant(24, typeof(int));
Console.WriteLine($"Node type: {constant.NodeType}, Type: {constant.Type}, Value: {constant.Value}");
// Output: Node type: Constant, Type: System.Int32, Value: 24For an arithmetic expression like (a, b) => a + b, you can inspect the parameters and body:
Expression<Func<int, int, int>> addition = (a, b) => a + b;
Console.WriteLine($"Lambda has {addition.Parameters.Count} parameters: " +
string.Join(", ", addition.Parameters.Select(p => p.Name)));
BinaryExpression sumBody = (BinaryExpression)addition.Body;
Console.WriteLine($"Operation: {sumBody.NodeType}, Left: {((ParameterExpression)sumBody.Left).Name}, Right: {((ParameterExpression)sumBody.Right).Name}");These examples show how to explore expression trees in a console application and reinforce the analogy of a recipe: each node is an ingredient that you can inspect or modify before compiling the whole recipe into executable code.
Illustration
The diagram below visualizes the tree structure for the lambda num => num < 5. The root Lambda node contains a LessThan node, which has two children: a Parameter node for num and a Constant node for 5.