Compiling C# Expression Trees into Executable Delegates
Learn how C# Expression.Compile() converts expression trees into executable delegates, when to use it, and how to avoid repeated compilation costs.
Compiling an expression in C# means turning an expression tree into a callable delegate. An expression tree is a data structure that describes logic as nodes. When you write Expression<Func<int, int>> and assign a lambda, the compiler builds a tree instead of emitting executable code. The tree itself cannot be invoked directly. Calling Compile() walks that tree and produces a delegate that actually runs the described logic.
What Compile() Produces from an Expression Tree
The delegate type produced by Compile() matches the expression's signature. An Expression<Func<int, int>> compiles to a Func<int, int>. An Expression<Action<string>> compiles to an Action<string>. This type preservation is why expression trees are useful for scenarios where code must be inspected, transformed, or generated at runtime and then executed.
The expression tree remains available after compilation. You can inspect it, modify it, or compile a modified version if needed. The compiled delegate is a separate artifact that holds the executable form.
Compiling a Lambda Expression
The simplest case is compiling a lambda assigned to an expression variable:
Expression<Func<int, int, int>> addExpression = (a, b) => a + b; Func<int, int, int> add = addExpression.Compile(); int result = add(2, 3); Console.WriteLine(result); // 5
The Compile() call returns a delegate that can be invoked like any other Func. The expression tree is no longer needed after compilation; the delegate holds the executable form.
This pattern appears in libraries that accept expressions as parameters. A validation library might accept Expression<Func<T, bool>> so it can inspect the property being validated, then compile the expression to evaluate it against instances.
How the Compilation Works at Runtime
When Compile() runs, the runtime walks the expression tree and generates executable code. On most .NET runtimes, this means emitting IL instructions that mirror the tree's operations. The resulting delegate points to the generated method.
Some runtimes also include an interpreter that can execute certain expression trees without generating IL. The interpreter avoids the allocation and startup cost of IL generation for simple trees. From the caller's perspective, the compiled delegate behaves the same either way.
The important detail is that Compile() is not free. It allocates memory for the generated code or interpreter state, walks the tree, and produces a delegate. That work happens every time you call Compile(), so calling it repeatedly with the same tree structure wastes that effort.
Compilation Cost and Execution Speed
The tradeoff has two sides. Building and compiling an expression tree costs more upfront than an equivalent direct method call, but it pays off when the same delegate is reused. When Compile() emits IL, invocation is nearly as fast as a directly compiled method. On runtimes that interpret expression trees instead, invocation may be slower, so reuse matters even more.
This matters in code that compiles the same expression inside a hot loop. Consider a repository method that compiles a predicate expression on every call:
public IEnumerable<T> Find(Expression<Func<T, bool>> predicate) { var compiled = predicate.Compile(); return _items.Where(compiled); }
If Find is called frequently with the same predicate, the compilation cost repeats on every call. The fix is to cache the compiled delegate, either by the caller or inside the method.
Caching Compiled Delegates
A common pattern is to store compiled delegates in a static dictionary keyed by a stable identifier:
private static readonly ConcurrentDictionary<string, Delegate> _cache = new(); public static Func<T, TResult> GetCompiled<T, TResult>(string key, Expression<Func<T, TResult>> expression) { return (Func<T, TResult>)_cache.GetOrAdd(key, _ => expression.Compile()); }
The key must be stable and meaningful to the caller. Using the expression's ToString() output is fragile because it can vary across runtime versions and does not capture closure values. A caller-supplied key is more reliable when the set of expressions is known in advance.
Caching matters most when expressions are constructed dynamically, such as when a query builder creates a predicate based on user input. Building the tree once and reusing the compiled delegate avoids repeated tree construction and compilation.
Building and Compiling Expressions Dynamically
Expression trees become more interesting when the logic is assembled at runtime. A common example is building a property accessor for a type discovered dynamically:
var parameter = Expression.Parameter(typeof(Order), "order"); var property = Expression.Property(parameter, "Total"); var lambda = Expression.Lambda<Func<Order, decimal>>(property, parameter); var getTotal = lambda.Compile(); decimal total = getTotal(order);
This compiles a delegate that reads Order.Total without reflection at call time. The tree is constructed with Expression factory methods, then compiled once. Subsequent invocations avoid both reflection and tree construction.
This approach is common in serializers, mappers, and data access layers where property access patterns repeat across many instances. The compiled delegate replaces repeated PropertyInfo.GetValue calls, which carry reflection overhead.
When Compile() Is the Wrong Tool
If the logic is known at compile time and you do not need to inspect or translate it, a regular method or lambda is always simpler. Expression trees add complexity for no benefit when the code never changes dynamically.
If the goal is only to invoke a method by name, reflection is often sufficient. MethodInfo.Invoke is slower than a compiled delegate, but for occasional calls the difference is irrelevant and the code is easier to read.
If the logic must be translated to another form, such as a SQL query, Compile() is not the right step. LINQ providers like Entity Framework Core interpret expression trees without compiling them to delegates. Compiling the tree would bypass the provider's translation and force client-side evaluation.
The decision comes down to whether the expression tree is a means to execute logic or a means to inspect and translate it. Compile() serves the first purpose. Providers serve the second.
Platform and AOT Compatibility
Compile() relies on runtime code generation. In environments that restrict dynamic code, such as Native AOT publishing or certain sandboxed runtimes, Compile() may be limited or unavailable. Some runtimes provide an interpreter that can handle simple trees, but you should not assume that compiled delegates are available in every target environment.
If your library targets those environments, avoid requiring Compile() in the hot path. Provide an alternative that uses direct delegates supplied by the caller, and treat compiled expressions as an optimization that may not be available everywhere.