For LINQ to be able to translate your queries into a different system at runtime it must be able to preserve the syntax of your operations rather than emitting code to perform it in-memory. Rather than trying to reverse-engineer compiled code at runtime the compiler instead preserves the intent of your code in an "expression tree".
When a function or variable expects a LambdaExpression - normally via the sub-type Expression<Func<T>> - the compiler parses the code as normal but rather than generating Intermediate Language (IL) instructions as normally it instead generates code that re-builds the code as an "expression tree".
This expression tree captures all the intent of the code you wrote in a way that LINQ providers can examine and then produce their own interpretation of (e.g. SQL) for remote execution (e.g. PostgreSQL).
Code
// The compiler preserves the intent as a tree of expressions
Expression<Func<Customer, bool>> expr = c => c.City == "London";
// A LINQ provider can translate this to SQL, REST, etc.
var londonCustomers = db.Customers.Where(expr);// A compiled delegate — executable but opaque
Func<Customer, bool> func = c => c.City == "London";
// LINQ to Objects can execute this but no provider can translate it
var londonCustomers = customers.Where(func);Notes
Expression trees:
- can still be compiled and executed by using the
Compilemethod on them - are immutable (they cannot be changed once built)
- Expression Visitor lets you effectively substitute expressions with new expressions
- these visitors are combined by nesting them at the top level
- are not limited to LINQ - you can just take a function, examine it, modify it and compile it at runtime!