Expression Trees: Building Dynamic Logic at Runtime
Lesson 18 โข Advanced Track
What You'll Learn
- Understand the difference between compiled lambdas (Func) and expression trees (Expression<Func>)
- Build expression trees manually using the Expression API
- Compile expression trees into executable code at runtime
- Create dynamic query filters by composing expressions programmatically
- Use ExpressionVisitor to inspect and transform expression trees
- Apply expression trees to real-world scenarios like dynamic filtering
๐ก Real-World Analogy
A regular lambda is like a baked cake โ ready to eat but you can't change the recipe. An expression tree is like the recipe card itself โ you can read it, modify the ingredients, translate it into a different cuisine, or bake it into a cake whenever you're ready. Entity Framework uses expression trees to read your C# LINQ queries and translate them into SQL โ it needs the recipe, not the cake.
1. Expression Trees vs Compiled Lambdas
Func<int, int, int> is compiled IL code โ you can execute it but can't inspect its structure. Expression<Func<int, int, int>> represents the same logic as a data structure you can traverse, modify, and compile on demand. This is how EF Core translates LINQ to SQL.
Expression Trees Basics
Compare Func vs Expression and build trees manually.
using System;
using System.Linq.Expressions;
class Program
{
static void Main()
{
// A lambda compiled to IL (executable code)
Func<int, int, int> addFunc = (a, b) => a + b;
Console.WriteLine($"Func result: {addFunc(3, 5)}");
// The SAME lambda as an expression tree (data structure)
Expression<Func<int, int, int>> addExpr = (a, b) => a + b;
// Inspect the tree structure
Console.WriteLine($"\nExpression type: {addExpr.NodeType}");
...2. Dynamic Query Building
The real power of expression trees is dynamic composition. Build filter expressions conditionally based on user input โ only add category, price range, or search filters when the user specifies them. This is how search pages with optional filters work.
Dynamic Filter Builder
Build conditional query filters using expression composition.
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; }
public string Category { get; set; } = "";
}
class Program
{
// Build a dynamic filter expression
static Expression<Func<Product, bool>> BuildFilter(
string? category = null,
decimal? minPrice = null,
decimal? maxPrice = null)
{
var param = Expression.Pa
...3. ExpressionVisitor โ Transform Trees
ExpressionVisitor walks an expression tree and lets you modify nodes. Override methods like VisitBinary to replace, remove, or add nodes. This pattern is used by ORMs to optimise queries and by mocking frameworks to set up expectations.
Expression Visitor
Transform expression trees by replacing operations.
using System;
using System.Linq.Expressions;
// Custom visitor that replaces addition with multiplication
class AddToMultiplyVisitor : ExpressionVisitor
{
protected override Expression VisitBinary(BinaryExpression node)
{
if (node.NodeType == ExpressionType.Add)
{
Console.WriteLine($" Replacing {node} with Multiply");
return Expression.Multiply(
Visit(node.Left),
Visit(node.Right));
}
return base.Vi
...Pro Tips
- ๐ก EF Core needs Expression, not Func:
dbContext.Products.Where(expr)translates to SQL. If you pass aFunc, EF downloads all rows and filters in memory. - ๐ก Cache compiled expressions:
.Compile()is expensive. If you call the same expression repeatedly, compile once and reuse theFunc. - ๐ก Use LINQKit for easier composition: The PredicateBuilder library makes combining
Expressionpredicates with AND/OR much simpler. - ๐ก Expression trees are immutable: You can't modify a tree โ you create a new one.
ExpressionVisitorreturns a new tree.
Common Mistakes
- Using Func where Expression is needed: LINQ to Entities requires
Expression<Func<T, bool>>. UsingFunc<T, bool>causes client-side evaluation or crashes. - Forgetting to compile: Expression trees are data, not code. Call
.Compile()to get an executable delegate. - Complex trees in hot paths: Building and compiling expression trees is slow. Don't do it in tight loops โ build once, compile, cache.
- Not handling null body: When building dynamic filters, ensure the body expression is never null โ use
Expression.Constant(true)as a fallback.
๐ Lesson Complete
- โ Expression trees represent code as inspectable data structures
- โ
Expression<Func<T>>vsFunc<T>โ data vs compiled code - โ
Build trees manually with
Expression.Parameter,.Multiply,.Lambda - โ Dynamic query building: compose filters conditionally for search UIs
- โ
ExpressionVisitortransforms trees by overriding Visit methods - โ Next lesson: Reflection & Dynamic Type Inspection
Sign up for free to track which lessons you've completed and get learning reminders.