Compiling C# Expression Trees into Executable Delegates
c# expression compile: Learn how C# Expression.Compile() converts expression trees into executable delegates, when to use it, and how to avoid repeated compilation costs.
c# expression compile requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
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 it again with different parameters. 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 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 is straightforward: building and compiling an expression tree costs more upfront than a direct method call, but the resulting delegate invokes at nearly the same speed as a directly compiled method. The cost is in the compilation step, not the invocation.
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, 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. The interpreter path covers some cases, but AOT scenarios generally expect expression trees to be interpreted or precompiled rather than compiled at runtime.
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.