Expression Trees as a Reflection Alternative in C#: When to Switch
C# expression trees reflection is a phrase you'll encounter the moment you start optimizing any system that relies on dynamic type inspection. Expression trees as a reflection alternative in C# let you treat code as data -- building executable logic at runtime from System.Linq.Expressions objects rather than raw IL -- and they can be dramatically faster than repeated reflection and often approach direct-call performance after the one-time compilation cost. Reflection, by contrast, pays a per-call cost every time. Understanding when to stop calling PropertyInfo.GetValue() and start building Func<T, TResult> delegates is one of the higher-leverage performance skills in .NET development.
This guide walks you through the full pipeline: from reflection-derived metadata, to expression tree construction, to compiled and cached delegates. By the end you'll have working code for property getters, property setters, constructor factories, and method invokers -- plus a real-world object mapper that ties them all together.
What Are Expression Trees?
An expression tree represents code as a tree of objects. Each node in the tree is an Expression subtype: BinaryExpression, MethodCallExpression, MemberExpression, LambdaExpression, and so on. The tree describes what to compute, not how -- and .Compile() converts that description into an actual delegate backed by JIT-compiled native code.
using System.Linq.Expressions;
// Build the expression tree for: (int x) => x * 2
ParameterExpression param = Expression.Parameter(typeof(int), "x");
BinaryExpression body = Expression.Multiply(param, Expression.Constant(2));
Expression<Func<int, int>> lambda = Expression.Lambda<Func<int, int>>(body, param);
// Compile it -- this is the one-time cost
Func<int, int> doubler = lambda.Compile();
// Call it like any other delegate -- no reflection overhead here
int result = doubler(7); // 14
The key insight: .Compile() triggers runtime code generation -- it emits IL and produces a delegate at runtime -- but you only pay this cost once. Cache the resulting delegate and every subsequent call is native-speed.
This is fundamentally different from reflection. When you call PropertyInfo.GetValue(obj), the runtime has to:
- Validate that the property exists on the object's actual type.
- Dispatch through a virtual call chain.
- Box and unbox value types.
- Return an
object.
Every single call repeats that work. Expression trees front-load all of it into the one-time .Compile() step.
For a deeper look at how reflection works before we start replacing it, see Reflection in C#: 4 Simple But Powerful Code Examples.
The Reflection-to-Expression-Tree Pipeline
The general pattern for replacing a reflection call with a compiled expression has four steps:
- Get the
MemberInfo--PropertyInfo,MethodInfo,ConstructorInfo, etc. This is the only part that requires reflection, and you do it once. - Build the expression tree -- use
Expression.*factory methods to describe the operation. - Wrap in a
LambdaExpressionand call.Compile(). - Cache the delegate -- store it in a
staticfield, aConcurrentDictionary<Type, Delegate>, or aFrozenDictionary<Type, Delegate>for best read performance in .NET 8+.
// Conceptual skeleton
static TDelegate BuildAndCache<TDelegate>(Type type, string memberName)
where TDelegate : Delegate
{
// Step 1: reflect once
var member = type.GetProperty(memberName) ?? throw new ArgumentException(...);
// Step 2: build the tree
var param = Expression.Parameter(typeof(object), "obj");
var cast = Expression.Convert(param, type);
var access = Expression.Property(cast, (PropertyInfo)member);
var body = Expression.Convert(access, typeof(object));
var lambda = Expression.Lambda<TDelegate>(body, param);
// Step 3: compile
return lambda.Compile();
}
Let's apply that pattern to each common scenario.
Building a Compiled Property Getter
Reading a property value is the most common use of reflection in practice -- ORMs, mappers, serializers all do it. Here is a compiled getter that takes any object and returns the property value as object:
using System.Linq.Expressions;
using System.Collections.Frozen;
public static class CompiledPropertyAccessor
{
private static readonly ConcurrentDictionary<(Type, string), Func<object, object?>> _cache = new();
public static Func<object, object?> GetGetter(Type type, string propertyName)
=> _cache.GetOrAdd((type, propertyName), static key =>
{
var (t, name) = key;
var prop = t.GetProperty(name)
?? throw new ArgumentException($"Property '{name}' not found on {t.Name}");
// Build: (object obj) => (object)(((TOwner)obj).PropertyName)
var objParam = Expression.Parameter(typeof(object), "obj");
var typedAccess = Expression.Property(
Expression.Convert(objParam, t),
prop);
var boxed = Expression.Convert(typedAccess, typeof(object));
var lambda = Expression.Lambda<Func<object, object?>>(boxed, objParam);
return lambda.Compile();
});
}
Usage is simple:
public sealed record Customer(string Name, int Age);
var customer = new Customer("Alice", 30);
var nameGetter = CompiledPropertyAccessor.GetGetter(typeof(Customer), "Name");
var ageGetter = CompiledPropertyAccessor.GetGetter(typeof(Customer), "Age");
Console.WriteLine(nameGetter(customer)); // Alice
Console.WriteLine(ageGetter(customer)); // 30
The first call for each property pays the compilation cost. Every subsequent call is a delegate invocation -- no reflection involved.
If you need a strongly-typed getter with full generic inference, you can also build Func<TOwner, TProperty> using generic type parameters in the lambda. That version avoids the boxing cost for value-type properties.
Building a Compiled Property Setter
Setters are slightly more involved because we need Expression.Assign, and we need to handle the fact that record types with init-only setters require special handling. For mutable properties:
public static class CompiledPropertySetter
{
private static readonly ConcurrentDictionary<(Type, string), Action<object, object?>> _cache = new();
public static Action<object, object?> GetSetter(Type type, string propertyName)
=> _cache.GetOrAdd((type, propertyName), static key =>
{
var (t, name) = key;
var prop = t.GetProperty(name)
?? throw new ArgumentException($"Property '{name}' not found on {t.Name}");
if (prop.SetMethod is null)
{
throw new InvalidOperationException($"Property '{name}' has no setter.");
}
// Build: (object obj, object? value) => ((TOwner)obj).PropertyName = (TProp)value
var objParam = Expression.Parameter(typeof(object), "obj");
var valueParam = Expression.Parameter(typeof(object), "value");
var typedObj = Expression.Convert(objParam, t);
var typedValue = Expression.Convert(valueParam, prop.PropertyType);
var propAccess = Expression.Property(typedObj, prop);
var assign = Expression.Assign(propAccess, typedValue);
var lambda = Expression.Lambda<Action<object, object?>>(assign, objParam, valueParam);
return lambda.Compile();
});
}
For init-only properties (common with records in .NET 10), you'd need to use ConstructorInfo to build a new instance rather than mutating an existing one. More on that in the factory section below.
Building a Compiled Constructor Factory
Object instantiation via reflection is another classic hot-path bottleneck. Activator.CreateInstance is convenient but pays reflection costs every time. For a comparison of Activator.CreateInstance vs direct constructor invocation, see Activator.CreateInstance in C# - A Quick Rundown and ConstructorInfo - How To Make Reflection in DotNet Faster for Instantiation.
Expression trees give you a no-overhead path for parameterless and parameterized constructors alike:
public static class CompiledFactory
{
// Parameterless factory
public static Func<T> BuildFactory<T>()
{
var ctor = typeof(T).GetConstructor(Type.EmptyTypes)
?? throw new InvalidOperationException($"{typeof(T).Name} has no parameterless constructor.");
var newExpr = Expression.New(ctor);
var lambda = Expression.Lambda<Func<T>>(newExpr);
return lambda.Compile();
}
// Factory with typed parameters -- build for any constructor signature
public static Func<object[], object> BuildFactory(Type type, Type[] paramTypes)
{
var ctor = type.GetConstructor(paramTypes)
?? throw new InvalidOperationException(
$"No matching constructor found on {type.Name}.");
// Build: (object[] args) => new T((T0)args[0], (T1)args[1], ...)
var argsParam = Expression.Parameter(typeof(object[]), "args");
var ctorArgs = paramTypes.Select((t, i) =>
Expression.Convert(
Expression.ArrayIndex(argsParam, Expression.Constant(i)),
t) as Expression).ToArray();
var newExpr = Expression.New(ctor, ctorArgs);
var boxed = Expression.Convert(newExpr, typeof(object));
var lambda = Expression.Lambda<Func<object[], object>>(boxed, argsParam);
return lambda.Compile();
}
}
Usage:
// Cached at startup -- zero allocation per call after this
var customerFactory = CompiledFactory.BuildFactory<Customer>();
var c1 = customerFactory(); // no reflection, no boxing
Building a Compiled Method Invoker
MethodInfo.Invoke() is notoriously slow. Expression.Call lets you replace it with a compiled delegate:
public static class CompiledMethodInvoker
{
public static Func<object, object?[], object?> Build(MethodInfo method)
{
var instanceParam = Expression.Parameter(typeof(object), "instance");
var argsParam = Expression.Parameter(typeof(object?[]), "args");
var paramInfos = method.GetParameters();
// Cast each argument from object[] to the expected parameter type
var castArgs = paramInfos.Select((p, i) =>
Expression.Convert(
Expression.ArrayIndex(argsParam, Expression.Constant(i)),
p.ParameterType) as Expression).ToArray();
// Cast the instance to the declaring type
var instance = Expression.Convert(instanceParam, method.DeclaringType!);
var call = method.ReturnType == typeof(void)
? (Expression)Expression.Block(Expression.Call(instance, method, castArgs), Expression.Constant(null, typeof(object)))
: Expression.Convert(Expression.Call(instance, method, castArgs), typeof(object));
var lambda = Expression.Lambda<Func<object, object?[], object?>>(
call, instanceParam, argsParam);
return lambda.Compile();
}
}
For static methods, omit the instance parameter and call Expression.Call(null, method, castArgs) instead.
Real-World Use Case: A Generic Object Mapper
Let's tie these together into something useful -- a mapper that copies matching properties from a source object to a destination object. This is the core of tools like AutoMapper, and it's a perfect showcase for why compiled expressions matter:
public sealed class ExpressionMapper<TSource, TDest>
where TDest : new()
{
private readonly List<Action<TSource, TDest>> _mappings;
public ExpressionMapper()
{
_mappings = BuildMappings();
}
public TDest Map(TSource source)
{
var dest = new TDest();
foreach (var mapping in _mappings)
{
mapping(source, dest);
}
return dest;
}
private static List<Action<TSource, TDest>> BuildMappings()
{
var sourceProps = typeof(TSource).GetProperties()
.ToDictionary(p => p.Name);
var destProps = typeof(TDest).GetProperties()
.Where(p => p.CanWrite);
var mappings = new List<Action<TSource, TDest>>();
foreach (var destProp in destProps)
{
if (!sourceProps.TryGetValue(destProp.Name, out var sourceProp))
{
continue;
}
if (sourceProp.PropertyType != destProp.PropertyType)
{
continue;
}
// Build: (TSource src, TDest dst) => dst.Prop = src.Prop
var srcParam = Expression.Parameter(typeof(TSource), "src");
var dstParam = Expression.Parameter(typeof(TDest), "dst");
var getValue = Expression.Property(srcParam, sourceProp);
var setValue = Expression.Assign(Expression.Property(dstParam, destProp), getValue);
var lambda = Expression.Lambda<Action<TSource, TDest>>(setValue, srcParam, dstParam);
mappings.Add(lambda.Compile());
}
return mappings;
}
}
Notice that this approach reflects once (in the constructor), compiles the delegates, and from that point forward Map() calls zero reflection APIs. The reflection cost is a one-time startup payment.
// Startup (typically in DI registration)
var mapper = new ExpressionMapper<CustomerEntity, CustomerDto>();
// Hot path -- no reflection anywhere
var dto = mapper.Map(entity);
Trade-Offs: Complexity vs Performance
Expression trees are not free. Here's an honest accounting:
Costs:
- Compilation latency at startup -- calling
.Compile()triggers runtime code generation. For a large type with many properties, this can add milliseconds to your startup time. In serverless environments with cold starts, this matters. - Code complexity -- expression tree construction code is verbose and unfamiliar to most developers. It's harder to review and debug than equivalent reflection code.
- Debugging is harder -- you cannot step through an expression tree at runtime the way you can through regular code. You can inspect the tree structure but not trace execution.
- AOT incompatibility -- Expression tree objects (the data structure) are AOT-safe. However,
.Compile()performs runtime code generation and is NOT supported under Native AOT. For AOT-compatible dynamic dispatch, consider source generators orUnsafeAccessor(for compile-time-known members) instead.
Benefits:
- Near-zero per-call overhead -- once compiled, delegate invocation costs the same as calling a method directly.
- No boxing for value types (when using strongly-typed generics in the lambda signature).
- Works with any type -- unlike source generators, expression trees work with types that aren't known until runtime.
- Single implementation -- one compiled delegate cache works for all types, not a generated class per type.
The sweet spot: high-frequency calls (serialization, mapping, property binding) where the set of types is fixed after startup but not known at compile time.
When NOT to Use Expression Trees
Expression trees are a precision tool. Don't reach for them by default.
One-shot or infrequent calls. If you call PropertyInfo.GetValue() once during application startup to read a config value, there is no performance problem to solve. The overhead is negligible at that frequency, and PropertyInfo.GetValue() is dramatically simpler to read.
Types known at compile time. If you're writing a mapper between two specific types you own, just write the mapping code. Direct property assignment is faster than a compiled delegate for the trivial case, and it's immediately readable.
AOT / NativeAOT targets. Expression tree objects (the data structure) are AOT-safe. However, .Compile() performs runtime code generation and is NOT supported under Native AOT. For AOT-compatible dynamic dispatch, consider source generators or UnsafeAccessor (for compile-time-known members) instead. The Source Generation vs Reflection in Needlr article covers this trade-off in depth for DI scenarios.
Very short-lived processes. CLI tools or Lambda functions with tiny execution windows may spend more time in .Compile() than they save. Profile before optimizing.
For the many cases where you just need dynamic instantiation without going all the way to expression trees, see Activator.CreateInstance vs Type.InvokeMember - A Clear Winner? for a lighter-weight comparison.
Expression Trees in .NET 10
.NET 10 doesn't fundamentally change the expression tree API -- it has been stable since .NET Framework 3.5. What .NET 10 does bring is broader context:
FrozenDictionary<TKey, TValue>(available since .NET 8) makes read-only delegate caches faster. Use it when your cache is populated once at startup and then only read.- Better NativeAOT tooling makes it easier to detect at compile time if you're accidentally using
Expression.Compile()in an AOT target -- the trimmer will warn you. - Improved diagnostics in the JIT mean expression-tree-compiled delegates now show up more clearly in profiler traces than in older runtimes.
Choosing Between Expression Trees and Direct Reflection
Picking the right tool is about matching the solution to the frequency and context of use. Think about it in three categories.
High frequency, startup-bounded types. A mapper, serializer, or property binder that will handle the same types thousands of times per second is the clearest win for expression trees as a reflection alternative in C#. The up-front compilation cost is negligible against the long-term savings. Build once, call forever.
Medium frequency, varied types. An admin dashboard that dynamically renders forms for arbitrary types -- maybe a few hundred different types across the lifetime of the application -- is a reasonable candidate. Each type pays the compilation cost once. The total overhead is bounded by the number of distinct types, not the number of calls.
Low frequency or one-off calls. Configuration loading, diagnostic introspection, migration scripts -- anything that happens once or rarely has no performance problem to solve. Use PropertyInfo.GetValue() directly. The code will be clearer and the maintenance burden lower.
Teams and maintainability. Expression tree construction code is unfamiliar to developers who haven't worked with System.Linq.Expressions before. If your team will need to maintain this code without the context of why it was written, consider whether the performance gain is worth the cognitive cost. Document the intent. Keep the cache lookup and the expression construction separated into small, named methods with clear responsibilities.
Alternative: source generators. If expression trees feel like the right optimization but the runtime compilation cost at startup is a concern, source generators produce equivalent delegate-speed code with zero startup cost. The trade-off is a more complex build pipeline and compile-time-only type knowledge. See Source Generation vs Reflection in Needlr for a real-world comparison of these two approaches for a DI scanning scenario.
What is an expression tree in C#?
An expression tree is a data structure that represents C# code as a tree of Expression objects from System.Linq.Expressions. Each node describes an operation: a method call, a property access, a binary operation, etc. You can build the tree at runtime, inspect it, transform it, and compile it to a delegate using .Compile(). LINQ providers like Entity Framework use expression trees to translate C# lambda expressions into SQL queries.
How do expression trees differ from reflection?
Reflection inspects and invokes members at runtime, paying a per-call cost every time. Expression trees also use reflection to build the tree, but then compile the tree to a native delegate once. After that, every call goes through the delegate with no reflection overhead. Reflection is simpler to write; expression trees are faster for repeated calls.
Are expression trees faster than reflection in C#?
Yes, for repeated calls. The per-call cost of a compiled delegate is essentially the same as a direct method call. The one-time .Compile() cost is paid upfront. The crossover point -- where expression trees become faster than reflection -- typically happens after a few hundred invocations of the same operation, depending on the operation complexity.
Can expression trees be used with .NET 10 NativeAOT?
Expression tree objects (the data structure) are AOT-safe. However, .Compile() performs runtime code generation and is NOT supported under Native AOT -- it requires the JIT. If you're targeting NativeAOT in .NET 10, use source generators or UnsafeAccessor (for compile-time-known members) instead. The trimmer in .NET 10 will produce a warning if you use Expression.Compile() in an AOT-targeted project.
When should I use expression trees instead of source generators?
Use expression trees when the types being operated on are not known at compile time -- for example, a generic serializer that handles any type registered at runtime, or a plugin system that loads assemblies dynamically. Source generators are the right choice when the full set of types is known at compile time and you want zero runtime overhead and AOT compatibility.
How do I cache compiled expression tree delegates in .NET 10?
The most common patterns are a static field (for single-type scenarios), a ConcurrentDictionary<Type, Delegate> (thread-safe with lazy population), or a FrozenDictionary<Type, Delegate> (fastest reads, built once at startup). For DI-heavy applications, you can also register the compiled delegates as singletons so the DI container manages their lifetime.
Do expression trees work with generic types and constraints?
Yes, but building expressions for generic types requires working with Type objects at runtime rather than C# type parameters at compile time. You build the Expression.Lambda<> against concrete Type objects. For strongly-typed scenarios, you can use a generic wrapper method that accepts type parameters and calls into a non-generic expression builder internally.
Conclusion
Expression trees occupy a specific and valuable niche in the .NET developer's toolkit. They're not a replacement for reflection in all cases -- they're a performance upgrade for the specific case of repeated dynamic member access or object instantiation.
The workflow is always the same: reflect once to get the metadata, build the expression tree that describes the operation, compile to a delegate, and cache. After that, your "dynamic" code runs at full native speed.
Use them for serializers, mappers, DI container internals, property binders, and any framework code that needs to work generically across types while serving high request volumes. Skip them for one-time calls, AOT targets, or cases where the simpler reflection API is already fast enough.
In .NET 10, the story gets nuanced at the edges -- NativeAOT is increasingly mainstream, and source generators are increasingly capable. For scenarios where the full type set is known at compile time, source generators win. For runtime-dynamic scenarios, compiled expression trees remain the right answer.

