Skip to main content
Expression Trees in C#: Beyond the Basics

Expression Trees in C#: Beyond the Basics

Dive deep into Expression Trees in C#. Learn how they represent code as data, enabling powerful scenarios like ORMs, dynamic queries, and code generation.

  1. Posts/

Expression Trees in C#: Beyond the Basics

·871 words·5 mins· loading
👤

Chris Malpass

Author

Have you ever wondered how Entity Framework translates your C# LINQ queries into SQL?

1
var users = db.Users.Where(u => u.IsActive && u.RegistrationDate > lastYear);

The magic behind this is a powerful and often misunderstood feature of .NET: Expression Trees.

An Expression Tree is a data structure that represents code. Instead of compiling your lambda expression into executable IL (Intermediate Language), the compiler can transform it into a tree-like object model. Every node in the tree represents a piece of the code: a binary operation, a method call, a property access, etc.

In short, an expression tree turns your code into data.

Func<T> vs. Expression<Func<T>>
#

The key to understanding expression trees is the difference between these two types:

  1. Func<User, bool> myFunc = u => u.IsActive;

    • This is a standard delegate. The lambda u => u.IsActive is compiled into executable code (IL) that your program can run directly.
    • You can call it: myFunc(someUser).
    • You cannot inspect it. You don’t know how it determines if a user is active.
  2. Expression<Func<User, bool>> myExpr = u => u.IsActive;

    • This is an expression tree. The lambda is not compiled into executable code.
    • Instead, it’s converted into a tree of objects representing the logic.
    • You can inspect it. You can walk the tree and see that it’s a property access on the IsActive member of the User object.
    • You can compile and run it if you want to: myExpr.Compile()(someUser).

By wrapping your lambda in Expression<>, you give the compiler permission to treat it as data.

Deconstructing an Expression Tree
#

Let’s look at a slightly more complex expression and see how it’s represented. First, let’s define a simple model to work with:

1
2
3
4
5
public class User
{
    public bool IsActive { get; set; }
    public int FollowerCount { get; set; }
}

Now, let’s inspect an expression that filters these users:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using System.Linq.Expressions;

Expression<Func<User, bool>> filter = u => u.IsActive && u.FollowerCount > 100;

// The root of the tree is the binary 'AndAlso' operation (&&)
var andExpression = (BinaryExpression)filter.Body;

Console.WriteLine($"Operation: {andExpression.NodeType}"); // AndAlso

// The left side of the '&&' is the property access 'u.IsActive'
var left = (MemberExpression)andExpression.Left;
Console.WriteLine($"Left: Property '{left.Member.Name}'"); // Left: Property 'IsActive'

// The right side is the '>' comparison
var right = (BinaryExpression)andExpression.Right;
Console.WriteLine($"Right Operation: {right.NodeType}"); // GreaterThan

// The right side's left part is the property access 'u.FollowerCount'
var rightLeft = (MemberExpression)right.Left;
Console.WriteLine($"Right's Left: Property '{rightLeft.Member.Name}'"); // Right's Left: Property 'FollowerCount'

// The right side's right part is the constant value 100
var rightRight = (ConstantExpression)right.Right;
Console.WriteLine($"Right's Right: Value '{rightRight.Value}'"); // Right's Right: Value '100'

As you can see, we can programmatically walk the entire lambda and understand its intent.

So What’s the Point?
#

This ability to inspect code as data is what enables some of the most powerful libraries in .NET:

  1. Object-Relational Mappers (ORMs) like Entity Framework:

    • EF takes your Expression<Func<User, bool>> from a .Where() call.
    • It walks the expression tree.
    • When it sees AndAlso, it writes "AND".
    • When it sees u.IsActive, it writes "[IsActive] = 1".
    • When it sees u.FollowerCount > 100, it writes "[FollowerCount] > 100".
    • By translating the tree, it builds a SQL query string that can be executed by the database.
  2. Dynamic Querying:

    • You can build expression trees manually at runtime. This allows you to construct complex, type-safe queries based on user input or configuration without resorting to string concatenation (which is vulnerable to SQL injection).
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    
    // 1. Define the parameter 'u' (as in 'u => ...')
    var userParam = Expression.Parameter(typeof(User), "u");
    
    // 2. Create expressions for property access
    var isActiveProp = Expression.Property(userParam, nameof(User.IsActive));
    var followerProp = Expression.Property(userParam, nameof(User.FollowerCount));
    
    // 3. Create constant values to compare against
    var trueConst = Expression.Constant(true);
    var numConst = Expression.Constant(100);
    
    // 4. Build the comparison logic
    // u.IsActive == true
    var isActiveCheck = Expression.Equal(isActiveProp, trueConst);
    // u.FollowerCount > 100
    var followerCheck = Expression.GreaterThan(followerProp, numConst);
    
    // 5. Combine them with AND (&&)
    var combined = Expression.AndAlso(isActiveCheck, followerCheck);
    
    // 6. Compile the final lambda
    var lambda = Expression.Lambda<Func<User, bool>>(combined, userParam);
    
    // Result: u => (u.IsActive == True) AndAlso (u.FollowerCount > 100)
    
  3. Mapping Libraries like AutoMapper:

    • AutoMapper can use expression trees to generate highly optimized mapping functions between two types at runtime, avoiding the performance overhead of reflection on every call.

Conclusion
#

Expression Trees are a C# “power user” feature. While you may not write them every day, understanding them is crucial to grasping how many modern .NET libraries work under the hood. They are the bridge that allows your C# code to be understood and translated into other languages, like SQL, or to be dynamically constructed and executed at runtime. The next time you write a LINQ to SQL query, take a moment to appreciate the elegant data structure that is working behind the scenes.

Further Reading
#