Expression Tree Basics
Overview
Section titled “Overview”The Specifications created in Part 1 filter in-memory collections using the IsSatisfiedBy method. However, in real applications, data resides in databases. ORMs like EF Core convert C# lambdas to SQL, and for that, they need Expression Trees rather than methods that return bool. In this chapter, we explore what Expression Trees are and why they are needed for Specifications.
Func is an executable black box, while Expression is an inspectable tree structure.
Learning Objectives
Section titled “Learning Objectives”Key Learning Objectives
Section titled “Key Learning Objectives”-
Can explain the fundamental difference between Func and Expression
Func<T, bool>is a compiled delegate whose internal structure cannot be examinedExpression<Func<T, bool>>preserves the code structure in tree form- Expression is inspectable through Body, Parameters, NodeType, etc.
-
Can explain why ORMs need Expression Trees
- EF Core must translate LINQ queries to SQL
- Func is opaque and cannot be converted to SQL
- Expression can be traversed and converted to SQL clauses
-
Can compile and cache Expressions for in-memory execution
- Can be converted to Func via
Expression.Compile()for in-memory execution - Caching the compiled result for reuse is efficient
- Can be converted to Func via
What You Will Verify Through Practice
Section titled “What You Will Verify Through Practice”- Accessing Expression’s Body, Parameters properties
- Compiling an Expression to Func and executing it
- Using Expression-based Where with AsQueryable()
Key Concepts
Section titled “Key Concepts”Func: An Opaque Black Box
Section titled “Func: An Opaque Black Box”Func<Product, bool> is a compiled delegate. It can be invoked at runtime to get results, but “what condition it represents” cannot be determined programmatically.
Func<Product, bool> func = p => p.Price > 1000;// No way to inspect the internals of func// EF Core cannot translate this to SQLExpression: An Inspectable Tree Structure
Section titled “Expression: An Inspectable Tree Structure”Expression<Func<Product, bool>> preserves the same lambda expression as a tree structure. The compiler converts the lambda into a data structure rather than code.
Expression<Func<Product, bool>> expr = p => p.Price > 1000;
// Tree structure is inspectableConsole.WriteLine(expr.Body); // (p.Price > 1000)Console.WriteLine(expr.Parameters); // pConsole.WriteLine(expr.Body.NodeType); // GreaterThanExpression -> Func Compilation
Section titled “Expression -> Func Compilation”An Expression can be converted to an executable Func through the Compile() method. This process has a cost, so caching the result is recommended.
var compiled = expr.Compile();var result = compiled(product); // true/falseWhy ORMs Need Expressions
Section titled “Why ORMs Need Expressions”| Aspect | Func | Expression |
|---|---|---|
| Internal Structure | Opaque (black box) | Inspectable (tree) |
| SQL Conversion | Impossible | Possible |
| IQueryable | Not supported | Can be used in Where clause |
| Execution Location | Always in memory | DB server or memory |
Project Description
Section titled “Project Description”Project Structure
Section titled “Project Structure”ExpressionIntro/ # Main project├── Program.cs # Expression Tree demo├── Product.cs # Product record├── ExpressionIntro.csproj # Project fileExpressionIntro.Tests.Unit/ # Test project├── ExpressionBasicsTests.cs # Expression basics tests├── Using.cs # Global using├── xunit.runner.json # xUnit configuration├── ExpressionIntro.Tests.Unit.csproj # Test project fileindex.md # This documentCore Code
Section titled “Core Code”Product.cs
Section titled “Product.cs”public record Product(string Name, decimal Price, int Stock, string Category);Expression Creation and Inspection
Section titled “Expression Creation and Inspection”// Func - opaque black boxFunc<Product, bool> func = p => p.Price > 1000;
// Expression - inspectable treeExpression<Func<Product, bool>> expr = p => p.Price > 1000;Console.WriteLine($"Body: {expr.Body}");Console.WriteLine($"Parameters: {string.Join(", ", expr.Parameters)}");
// Expression -> Func compilationvar compiled = expr.Compile();var product = new Product("Laptop", 1_500_000, 10, "Electronics");Console.WriteLine($"Result: {compiled(product)}");Summary at a Glance
Section titled “Summary at a Glance”Func vs Expression Comparison
Section titled “Func vs Expression Comparison”| Aspect | Func<T, bool> | Expression<Func<T, bool>> |
|---|---|---|
| Essence | Compiled code | Data representation of code |
| Inspection | Impossible | Body, Parameters, etc. accessible |
| SQL Conversion | Impossible | ORM traverses tree to convert |
| Execution | Direct invocation | Invoke after Compile() |
| IEnumerable | Can be used with Where | Needs Compile |
| IQueryable | Cannot be used | Can be used directly with Where |
Key Points
Section titled “Key Points”- Expression represents code as data, allowing programmatic analysis and transformation.
- ORMs need Expressions to generate SQL. With Func alone, all data must be loaded into memory.
- Compile() has a cost, so caching is recommended.
Q1: Where are Expression Trees used?
Section titled “Q1: Where are Expression Trees used?”A: Primarily in ORMs (Entity Framework Core), LINQ to SQL, dynamic query builders, etc. Expression Trees are essential when condition expressions written in C# code need to be converted to SQL or other query languages.
Q2: Should Expression always be used instead of Func?
Section titled “Q2: Should Expression always be used instead of Func?”A: No. When filtering in-memory collections, Func is more efficient. Expression should only be used when SQL conversion is needed. The Specification pattern uses Expression-based approaches to support both scenarios.
Q3: How much is the performance cost of Expression.Compile()?
Section titled “Q3: How much is the performance cost of Expression.Compile()?”A: Compile() converts the Expression Tree to IL code, which is relatively expensive. Therefore, it is recommended to cache and reuse the compiled result. Functorium’s ExpressionSpecification automatically performs this caching internally.
Q4: What is AsQueryable()?
Section titled “Q4: What is AsQueryable()?”A: AsQueryable() converts IEnumerable<T> to IQueryable<T>. Since IQueryable supports Expression-based Where, you can test Expression-based filtering even on in-memory collections. In real projects, EF Core’s DbSet<T> implements IQueryable<T>.
Now that you understand the concept of Expression Trees, in the next chapter we will implement the ExpressionSpecification<T> class that integrates them into Specifications.