Dynamic invocation and its bill
MethodInfo.Invoke vs cached delegates vs compiled expressions โ measured, not folklore.
Dynamic invocation and its bill
Three ways to call a method you discovered at runtime, in ascending setup cost and descending per-call cost:
methodInfo.Invoke(obj, args)โ slowest per call: argument boxing, array allocation, security/registrar checks.- Cached delegate:
(Func<Order, decimal>)methodInfo.CreateDelegate(...)โ build once, then near-direct call speed. - Compiled expression:
Expression.Lambda<Func<...>>(...).Compile()โ most setup, fastest body; can also inline field access.
// Resolve once:
static readonly ConcurrentDictionary<(Type, string), Func<object, object?>> Getters = new();
static object? Get(object obj, string name) => Getters.GetOrAdd(
(obj.GetType(), name),
static key =>
{
var p = key.Item1.GetProperty(key.Item2)!;
return target => p.GetValue(target); // captures resolved PropertyInfo
});
Numbers to internalize (order-of-magnitude, machine-dependent โ always
measure your own): direct call 1x; cached delegate ~2โ5x; compiled
expression ~2โ3x; raw Invoke ~30โ100x. The jump from 1 to 30 is the
reason every serious serializer (System.Text.Json, EF materializers)
caches or code-gens instead of reflecting per call.
CreateDelegate has one trap: it validates the delegate signature against
the method at creation โ a mismatch throws immediately (good), while a
cached-wrong delegate throws on every call (bad). Prefer failing during
startup: build all delegates in a warm-up phase, not lazily on first
request.