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 .NET Type of 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}"); // 30

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