C# Expression Trees: How to Inspect Code Dynamically
Expression trees are powerful data structures in C# that represent
code as an abstract syntax tree rather than executable instructions.
This article explains what expression trees are, how they differ from
standard delegates, and how you can traverse and inspect them at runtime
to analyze code dynamically. You will learn the core components of the
System.Linq.Expressions namespace, see practical code
examples for breaking down expressions, and understand how to implement
custom expression visitors for dynamic analysis.
What Are Expression Trees?
In C#, an expression tree represents strongly typed code as a tree-like hierarchy of nodes. Each node in the tree is an expression—for example, a method call, a binary operation like addition, or a constant value.
When you write a standard lambda expression assigned to a delegate, the C# compiler emits Intermediate Language (IL) that executes directly:
Func<int, bool> isPositive = x => x > 0;When you assign the exact same lambda to an
Expression<TDelegate> type, the compiler does not
generate executable IL. Instead, it generates metadata that builds an
expression tree representing the structure of that lambda:
using System.Linq.Expressions;
Expression<Func<int, bool>> isPositiveExpr = x => x > 0;Because this code exists as data rather than compiled bytecode, your application can inspect, modify, or translate it at runtime. This mechanism is the foundational technology behind Object-Relational Mappers (ORMs) like Entity Framework Core, which parses C# expression trees into SQL queries.
Anatomy of an Expression Tree
All nodes within an expression tree derive from the abstract base
class System.Linq.Expressions.Expression. The most common
derived node types include:
LambdaExpression: Represents the entire lambda function, containing parameters and a body.ParameterExpression: Represents a variable or parameter within the expression (e.g.,x).BinaryExpression: Represents an operation with two operands, such as+,-,*,==, or>.ConstantExpression: Represents a hardcoded value (e.g.,0,"hello").MethodCallExpression: Represents a call to a static or instance method.MemberExpression: Represents accessing a field or property.
Every Expression object exposes two fundamental
properties for inspection:
NodeType: An enum (ExpressionType) defining the kind of operation (e.g.,GreaterThan,Add,Call).Type: The .NETTypeof the value returned by evaluating the node.
Inspecting an Expression Tree Manually
You can inspect the components of an expression tree by decomposing its nodes into their specific derived types.
Consider analyzing the condition x => x > 0:
using System;
using System.Linq.Expressions;
Expression<Func<int, bool>> expr = x => x > 0;
// 1. Inspect the root Lambda
Console.WriteLine($"Lambda Return Type: {expr.ReturnType}");
Console.WriteLine($"Parameter Count: {expr.Parameters.Count}");
Console.WriteLine($"Parameter Name: {expr.Parameters[0].Name}");
// 2. Inspect the Body (BinaryExpression: x > 0)
if (expr.Body is BinaryExpression binaryBody)
{
Console.WriteLine($"Operation: {binaryBody.NodeType}"); // GreaterThan
// Inspect the left side (Parameter: x)
if (binaryBody.Left is ParameterExpression leftParam)
{
Console.WriteLine($"Left Operand: {leftParam.Name} (Type: {leftParam.Type.Name})");
}
// Inspect the right side (Constant: 0)
if (binaryBody.Right is ConstantExpression rightConst)
{
Console.WriteLine($"Right Operand: {rightConst.Value} (Type: {rightConst.Type.Name})");
}
}Running this code breaks down the lambda without executing it, extracting the variable name, operator, and constant operand dynamically.
Inspecting Trees with ExpressionVisitor
Manually casting nodes becomes impractical for complex or nested
expressions. The standard and most robust way to inspect or rewrite
expression trees in C# is by subclassing
ExpressionVisitor.
The ExpressionVisitor class uses the visitor pattern. It
recursively traverses every branch of the tree and provides specialized
virtual methods for each node type (e.g., VisitBinary,
VisitMember, VisitConstant).
Example: Building a Dynamic Property Inspector
Below is a custom visitor that inspects an expression and logs every property access and constant value used in a query:
using System;
using System.Linq.Expressions;
public class CodeInspectorVisitor : ExpressionVisitor
{
protected override Expression VisitMember(MemberExpression node)
{
Console.WriteLine($"Inspected Property/Field: {node.Member.Name} on type {node.Member.DeclaringType?.Name}");
return base.VisitMember(node);
}
protected override Expression VisitConstant(ConstantExpression node)
{
Console.WriteLine($"Inspected Constant: {node.Value}");
return base.VisitConstant(node);
}
protected override Expression VisitBinary(BinaryExpression node)
{
Console.WriteLine($"Inspected Binary Operator: {node.NodeType}");
return base.VisitBinary(node);
}
}You can pass an expression to this visitor to inspect its entire structure automatically:
public class Person
{
public string Name { get; set; } = string.Empty;
public int Age { get; set; }
}
class Program
{
static void Main()
{
Expression<Func<Person, bool>> filter = p => p.Age >= 18 && p.Name == "Alice";
var inspector = new CodeInspectorVisitor();
inspector.Visit(filter);
}
}The visitor recursively traverses down the tree from the root logical
AND node, inspecting the operands, member accesses (Age,
Name), comparison operators, and constant values
(18, "Alice").
Dynamic Compilation and Execution
After inspecting an expression tree, you may choose to execute it
dynamically. The Compile() method compiles the data
structure into an executable delegate at runtime via dynamic IL
generation:
Expression<Func<int, int, int>> addExpr = (a, b) => a + b;
// Inspect the operation
Console.WriteLine($"Operation: {addExpr.Body.NodeType}"); // Add
// Compile the tree into executable code
Func<int, int, int> compiledAdd = addExpr.Compile();
// Execute
int result = compiledAdd(10, 20);
Console.WriteLine($"Result: {result}"); // 30Expression trees provide a structured bridge between compile-time code definitions and runtime dynamic inspection, allowing C# developers to build rich querying engines, dynamic validators, and serialization frameworks.