Expression trees let you build C# code as data structures, which is surprisingly powerful for generating dynamic queries.
Let’s see it in action. Imagine you have a list of Product objects and you want to filter them based on a user-provided condition, like "price is greater than 50".
public class Product
{
public string Name { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
}
var products = new List<Product>
{
new Product { Name = "Laptop", Price = 1200.00m, Stock = 10 },
new Product { Name = "Mouse", Price = 25.00m, Stock = 50 },
new Product { Name = "Keyboard", Price = 75.00m, Stock = 20 },
new Product { Name = "Monitor", Price = 300.00m, Stock = 5 }
};
// We want to build a filter like: p => p.Price > 50
// Let's build this dynamically.
// 1. Define the parameter for our lambda expression (e.g., 'p' for product)
var parameter = Expression.Parameter(typeof(Product), "p");
// 2. Get the property we want to access (e.g., Price)
var propertyInfo = typeof(Product).GetProperty("Price");
var propertyAccess = Expression.MakeMemberAccess(parameter, propertyInfo);
// 3. Define the constant value we're comparing against (e.g., 50)
var constantValue = Expression.Constant(50m, typeof(decimal));
// 4. Create the comparison expression (e.g., greater than)
var comparison = Expression.GreaterThan(propertyAccess, constantValue);
// 5. Compile the expression tree into a delegate (a lambda expression)
var lambda = Expression.Lambda<Func<Product, bool>>(comparison, parameter);
// 6. Use the compiled lambda to filter the list
var filteredProducts = products.Where(lambda.Compile()).ToList();
foreach (var product in filteredProducts)
{
Console.WriteLine($"{product.Name} - ${product.Price}");
}
This code will output:
Laptop - $1200.00
Keyboard - $75.00
Monitor - $300.00
Expression trees are the abstract syntax tree representation of C# code. Instead of directly writing p => p.Price > 50, you’re constructing that logic piece by piece using Expression objects. This allows you to manipulate code as data – you can build it, modify it, and then compile it into executable delegates.
The core problem expression trees solve is enabling runtime code generation and manipulation. Think about ORMs like Entity Framework. When you write dbContext.Products.Where(p => p.Price > 50), EF doesn’t just run that C# code directly against your in-memory list. It takes that Expression<Func<Product, bool>>, converts it into an SQL query (e.g., SELECT * FROM Products WHERE Price > 50), and sends that to the database. Expression trees are the bridge between your C# code and other execution environments like SQL.
Here’s how you can build more complex logic. Suppose you want to filter by Name containing "o" AND Stock less than 30.
// For 'p => p.Name.Contains("o")'
var nameParam = Expression.Parameter(typeof(Product), "p");
var nameProperty = Expression.Property(nameParam, "Name");
var containsMethod = typeof(string).GetMethod("Contains", new[] { typeof(string) });
var nameConstant = Expression.Constant("o");
var nameContainsCall = Expression.Call(nameProperty, containsMethod, nameConstant);
// For 'p => p.Stock < 30'
var stockParam = Expression.Parameter(typeof(Product), "p"); // Note: Can reuse parameter if structure is same
var stockProperty = Expression.Property(stockParam, "Stock");
var stockConstant = Expression.Constant(30);
var stockLessThan = Expression.LessThan(stockProperty, stockConstant);
// Combine them with AND: 'p => p.Name.Contains("o") && p.Stock < 30'
var andExpression = Expression.AndAlso(nameContainsCall, stockLessThan);
// Create the lambda
var complexLambda = Expression.Lambda<Func<Product, bool>>(andExpression, nameParam);
var complexFilteredProducts = products.Where(complexLambda.Compile()).ToList();
foreach (var product in complexFilteredProducts)
{
Console.WriteLine($"{product.Name} - ${product.Price} ({product.Stock} in stock)");
}
This will output:
Mouse - $25.00 (50 in stock)
Monitor - $300.00 (5 in stock)
The key lever you control is the structure of the Expression objects you build. Each type of Expression (e.g., ParameterExpression, MemberExpression, ConstantExpression, MethodCallExpression, BinaryExpression) represents a specific part of C# code. By composing these, you can represent any valid C# expression. The Expression.Lambda method is what wraps your expression tree into a callable delegate. The Compile() method then turns that tree into executable code.
When you’re building complex queries, especially those involving multiple conditions or dynamic property access, you’ll frequently use Expression.Property or Expression.Field to access members, Expression.Call to invoke methods, and Expression.New to construct objects. For logical operations, Expression.AndAlso (short-circuiting AND), Expression.OrElse (short-circuiting OR), Expression.Equal, Expression.GreaterThan, etc., are your go-to methods for binary operations.
A common pitfall is forgetting to use Expression.AndAlso or Expression.OrElse when combining conditions. If you try to directly use Expression.And or Expression.Or, you’ll often get a runtime error because those are bitwise operators, not logical ones, and they expect numeric types. The AndAlso and OrElse methods correctly build the && and || logic, including the short-circuiting behavior.
The next step is often to integrate this into a query provider, like translating these expression trees into SQL or another query language.