Expression trees, a unique feature in C#, provide a way to represent code as a data structure. At a high level, they allow developers to treat code as data, enabling dynamic generation and manipulation of code during runtime. This capability is especially useful in scenarios like building dynamic LINQ queries or creating runtime-compiled delegates.
Basics of Expression Trees
Lambda expressions in C# are a concise way to represent anonymous methods. For instance:
Func<int, int, int> add = (a, b) => a + b;
However, when we want to inspect or manipulate the lambda expression itself, we use expression trees:
Expression<Func<int, int, int>> addExpression = (a, b) => a + b;
The key difference is the Expression<> wrapper, which tells the compiler to treat the lambda as data rather than executable code.
Creating Expression Trees
Expression trees can be created manually using the Expression class’s factory methods:
var paramA = Expression.Parameter(typeof(int), "a");
var paramB = Expression.Parameter(typeof(int), "b");
var body = Expression.Add(paramA, paramB);
var addExpression = Expression.Lambda<Func<int, int, int>>(body, paramA, paramB);
var compiled = addExpression.Compile();
Console.WriteLine(compiled(1, 2)); // Outputs: 3
In the code above, not only we created an expression tree, but we also compiled it to a delegate and executed it.
Real-world Use Cases
- Dynamic LINQ Queries: Expression trees enable the creation of dynamic LINQ queries, useful when building flexible search or filter functionalities.
- Dynamic Predicates and Selectors: Generate predicates or selectors at runtime based on user input or other dynamic conditions.
Let see an example:
List<Product> products = new()
{
new Product { Name = "Laptop", Price = 1000M },
new Product { Name = "Mouse", Price = 20M },
new Product { Name = "Keyboard", Price = 50M }
};
// Dynamic filter criteria
string userInputName = "Laptop"; // Example user input
decimal? userInputPrice = null; // No price filter
// Building the expression tree
var param = Expression.Parameter(typeof(Product), "p");
Expression predicate = Expression.Constant(true); // Default to true for AND operations
if (!string.IsNullOrEmpty(userInputName))
{
var nameProperty = Expression.Property(param, "Name");
var nameValue = Expression.Constant(userInputName);
var nameEquals = Expression.Equal(nameProperty, nameValue);
predicate = Expression.AndAlso(predicate, nameEquals);
}
if (userInputPrice.HasValue)
{
var priceProperty = Expression.Property(param, "Price");
var priceValue = Expression.Constant(userInputPrice.Value);
var priceEquals = Expression.Equal(priceProperty, priceValue);
predicate = Expression.AndAlso(predicate, priceEquals);
}
var lambda = Expression.Lambda<Func<Product, bool>>(predicate, param);
var filteredProducts = products.AsQueryable().Where(lambda).ToList();
foreach (var product in filteredProducts)
{
Console.WriteLine(product);
}
public record Product
{
public string Name { get; set; }
public decimal Price { get; set; }
}
Expression Tree Visitors
The ExpressionVisitor class provides a way to traverse and potentially modify an expression tree. It uses the visitor pattern, where a separate method is called for each type of node in the tree.
Here’s a simple example of an ExpressionVisitor that replaces all instances of a specific parameter with a constant expression:
public class ParameterReplacer : ExpressionVisitor
{
private readonly ParameterExpression _oldParameter;
private readonly Expression _replacement;
public ParameterReplacer(ParameterExpression oldParameter, Expression replacement)
{
_oldParameter = oldParameter;
_replacement = replacement;
}
protected override Expression VisitParameter(ParameterExpression node)
{
if (node == _oldParameter)
return _replacement;
return base.VisitParameter(node);
}
}
// Usage:
Expression<Func<int, int>> expr = x => x + 1;
var replacer = new ParameterReplacer(expr.Parameters[0], Expression.Constant(68));
var newExpr = replacer.Visit(expr.Body) as BinaryExpression;
Console.WriteLine(newExpr); // Outputs: (68 + 1)
Conclusion
Expression trees are a powerful tool in the C# developer’s arsenal, enabling dynamic code generation and manipulation. By understanding and leveraging their capabilities, developers can build more flexible and dynamic applications.
In a future post I will explain how you can extend EF Core capabilities using expression tree visitors and even implement not supported methods.
Leave a comment