Solid Introduction to Expression Trees in C#

Expression trees are one of the complex topics in C#/.NET that needs to be understood. They represent code in a tree-like data structure, where each node is an expression (such as a method call, binary operation, or constant value). They allow you to construct, examine, and execute code dynamically at runtime.

Expression trees are particularly useful for creating dynamic code, analyzing code at runtime, and enabling frameworks like LINQ to SQL and Entity Framework to translate C# code into SQL queries or other operations.

SQL queries

Expression trees are composed of nodes, each representing a specific element of a program (e.g., a method call, a lambda expression, or a binary operation like + or -).

Before diving into the details of technical implementation, let's try to understand the use cases of Expression trees.

  1. LINQ Providers: In LINQ to SQL and Entity Framework, expression trees are used to parse LINQ queries and translate them into SQL statements. When you write a LINQ query like dbContext.Products.Where(p => p.Price > 100), the LINQ provider examines the expression tree representing p => p.Price > 100 and translates it into a SQL query (SELECT * FROM Products WHERE Price > 100).
  2. Dynamic Query Construction: Expression trees allow developers to dynamically construct queries at runtime. For example, you can build complex search conditions based on user input, dynamically combining predicates using expressions like Expression. Also Expression.OrElse.
  3. Meta-Programming: Expression trees enable meta-programming scenarios where you can inspect and manipulate code at runtime. You can analyze expression trees to understand the structure of code, allowing you to write tools that generate or transform code.
  4. Building Dynamic LINQ Queries: Expression trees allow you to construct dynamic LINQ queries by building predicates based on conditions at runtime. This is useful when constructing search filters or complex queries based on dynamic user input.
    var parameter = Expression.Parameter(typeof(Product), "p");
    
    var property = Expression.Property(parameter, "Price");
    
    var constant = Expression.Constant(100);
    
    var condition = Expression.GreaterThan(property, constant);
    
    var lambda = Expression.Lambda<Func<Product, bool>>(condition, parameter);
  5. Custom Rule Engines: Expression trees are used in rule engines where business rules are evaluated dynamically. Developers can build, compile, and execute rules represented by expression trees based on data at runtime.

What do we have as advanced Features?

  1. Expression Visitor: An ExpressionVisitor is a class in the System.Linq.Expressions namespace that allows you to traverse and modify expression trees. This is useful for scenarios where you need to analyze or modify parts of an expression tree.
    Example
    public class CustomExpressionVisitor : ExpressionVisitor
    {
        protected override Expression VisitBinary(BinaryExpression node)
        {
    
            // Example: Change all addition operations to multiplication
            if (node.NodeType == ExpressionType.Add)
            {
                return Expression.Multiply(node.Left, node.Right);
            }
            return base.VisitBinary(node);
        }
    }
  2. Expression Trees for LINQ Query Optimization: Expression trees can be used to optimize LINQ queries at runtime. By analyzing the structure of a LINQ query, frameworks can choose to cache certain expressions, rewrite inefficient queries, or perform other optimizations.
  3. Combining Expressions: You can combine multiple expressions dynamically to create more complex queries. For instance, you can dynamically build predicates using Expression.AndAlso Expression.OrElse.
    Example
    Expression<Func<Product, bool>> expr1 = p => p.Price > 100;
    
    Expression<Func<Product, bool>> expr2 = p => p.Category == "TV";
    
    var combined = Expression.Lambda<Func<Product, bool>>(
    
    Expression.AndAlso(expr1.Body, expr2.Body), expr1.Parameters);

Expression trees are created using the types defined in the System.Linq.Expressions namespace. The most common classes include.

  • Expression: The base class for all nodes in an expression tree.
  • LambdaExpression: Represents lambda expressions.
  • BinaryExpression: Represents binary operations (e.g., +, -, *, /).
  • MethodCallExpression: Represents method calls.

How to build them?

You can create expression trees manually using factory methods such as Expression.Add(), Expression.Constant(), and Expression.Lambda(). For example, to represent the expression x + 1 where x is a parameter.

ParameterExpression param = Expression.Parameter(typeof(int), "x");

ConstantExpression constant = Expression.Constant(1);

BinaryExpression body = Expression.Add(param, constant);

Expression<Func<int, int>> lambda = Expression.Lambda<Func<int, int>>(body, param);

Once an expression tree is built, it can be compiled into executable code using Compile().

var compiledLambda = lambda.Compile();

int result = compiledLambda(5);  // result = 6

Long story short, we have the following steps.

  • Construction: Expression trees are built using System.Linq.Expressions.Expression static methods such as Expression.Add(), Expression.Call(), and Expression.Lambda(). Each method constructs a node in the expression tree.
  • Compilation: Once the expression tree is built, you can compile it into executable code using the Compile() method, which turns the expression into a delegate that can be invoked.
  • Execution: After compiling, the expression behaves like any regular delegate, and you can invoke it with arguments.

Any limitations? Of course. Here they are,

  • No Full Language Support: While expression trees cover a wide range of C# constructs, they don’t support all C# features. For example, loops, try-catch blocks, and certain other flow-control constructs cannot be represented using expression trees.
  • Performance Overhead: Constructing and compiling expression trees can introduce a performance overhead compared to using compiled code. However, once compiled, the generated delegate executes with minimal overhead.
  • Complexity: Managing and manipulating expression trees for complex logic can become cumbersome due to their hierarchical and low-level nature. This is why they are often used in combination with higher-level abstractions.

In the end, let’s cover real-world examples of using Expression Trees.

  1. Entity Framework Core: EF Core uses expression trees to translate LINQ queries into SQL queries. The LINQ queries written in C# are parsed into expression trees, and then EF Core translates these trees into the corresponding SQL.
  2. Custom Query Builders: You can use expression trees to build custom query builders that generate complex search queries based on dynamic conditions. For example, if you’re building a search filter for a web application, you could use expression trees to construct the query based on the user’s input dynamically.
  3. Unit of Work and Repository Patterns: Expression trees are often used in repository patterns to implement dynamic filtering, sorting, and pagination in a reusable way.

Conclusion

Expression trees are an invaluable tool when you need to dynamically create, manipulate, or inspect code in a flexible and efficient way. They are particularly important for frameworks that rely on dynamic code generation, such as LINQ providers and rule engines.