Reflection and metaprogramming questions separate engineers who have used typeof(T).GetProperty(...) from engineers who understand what that call actually costs and what replaced it. Over the last decade the center of gravity in .NET has moved from runtime reflection toward compile-time code generation: source generators back JSON serialization, logging and configuration binding precisely because reflection is too slow and too AOT-hostile for framework-level code running millions of times a second. A senior interview in this area tests whether you can explain why that shift happened, when reflection is still the right tool, how to make it fast when it is, and what breaks when a reflection-heavy library meets Native AOT. Expect questions on caching strategy, Reflection.Emit, the newer UnsafeAccessor attribute, the dynamic keyword's actual runtime mechanics, and how to design an attribute-driven API that does not fall over under trimming.

Q1 Why is reflection slow, and how do you use it without paying that cost on every call?#

Short answer: Reflection is slow because each MethodInfo.Invoke or PropertyInfo.GetValue call does signature validation, boxes value-type arguments and return values into object, and goes through a general-purpose dispatch path instead of a direct call; the fix is to look the member up once and cache a fast invocation strategy, not to call the reflection APIs repeatedly.

The lookup itself (GetMethod, GetProperty) is also non-trivial: it walks metadata tables and, depending on BindingFlags, may scan the whole type hierarchy. The standard mitigation is a two-tier cache: a ConcurrentDictionary<Type, ...> (or ConditionalWeakTable<Type, T> when you need the entry to be collectible alongside a dynamically loaded type) that stores, per type, not the MemberInfo itself but a compiled delegate that invokes it directly.

C#
using System.Reflection;
using System.Collections.Concurrent;

public static class FastPropertyReader
{
    private static readonly ConcurrentDictionary<(Type, string), Func<object, object?>> Cache = new();

    public static object? Read(object instance, string propertyName)
    {
        var getter = Cache.GetOrAdd((instance.GetType(), propertyName), static key =>
        {
            var property = key.Item1.GetProperty(key.Item2)
                ?? throw new MissingMemberException(key.Item1.Name, key.Item2);
            var getMethod = property.GetGetMethod()!;

            // A typed, open-instance delegate: no per-call boxing of the receiver, no Invoke overhead.
            var untyped = (Func<object, object?>)Delegate.CreateDelegate(
                typeof(Func<object, object?>), getMethod.DeclaringType is { IsValueType: true }
                    ? throw new NotSupportedException("Use a compiled expression for value types.")
                    : null!, getMethod);
            return untyped;
        });

        return getter(instance);
    }
}

For value-type receivers, Delegate.CreateDelegate cannot bind an open-instance delegate the same way, so the common answer is a compiled Expression<Func<object, object?>> instead, covered later in this page. Either approach turns an O(lookup + invoke) cost paid every call into an O(1) dictionary hit after the first.

What interviewers look for: naming the actual costs (boxing, signature checks, hierarchy walks) rather than a vague "reflection is slow," and a concrete caching strategy rather than "I'd just cache it" with no shape given.

Common mistakes: caching the MemberInfo but still calling .Invoke on every use, which removes the lookup cost but keeps the far larger invocation cost.

Q2 Compare MethodInfo.Invoke, compiled expression trees, and Delegate.CreateDelegate for calling a member dynamically. When would you pick each?#

Short answer: MethodInfo.Invoke is the simplest and slowest, boxing every argument into an object[]; Delegate.CreateDelegate produces a strongly typed delegate bound directly to the method, as fast as a normal call, but only when you know the signature at the call site; a compiled Expression<TDelegate> sits in between, letting you build a typed call dynamically (useful when the member is only known by name at runtime) at the cost of a relatively expensive one-time compilation step.

The right choice depends on how often you call the member and how much of the signature you know statically. A one-off admin tool calling a handful of methods is fine with Invoke. A serializer or object mapper that will read the same property thousands of times per second needs the compiled or delegate-bound path, because the cost amortizes across every subsequent call.

C#
using System.Linq.Expressions;
using System.Reflection;

// Build once per (type, member) and cache; run millions of times after that.
static Func<object, object?> BuildGetter(PropertyInfo property)
{
    var instanceParam = Expression.Parameter(typeof(object), "instance");
    var typedInstance = Expression.Convert(instanceParam, property.DeclaringType!);
    var propertyAccess = Expression.Property(typedInstance, property);
    var boxedResult = Expression.Convert(propertyAccess, typeof(object));

    return Expression.Lambda<Func<object, object?>>(boxedResult, instanceParam).Compile();
}
ApproachFirst-call costSteady-state costWorks for value types
MethodInfo.InvokeLowHigh: boxing and dispatch every callYes
Delegate.CreateDelegateLowNear-nativeOnly static/closed generic binding
Compiled Expression<TDelegate>High: JIT-compiles an IL methodNear-nativeYes
DynamicMethod/ILGeneratorHighest: hand-written ILNativeYes

What interviewers look for: a cost model, not just names — specifically that expression compilation has a real one-time price, so it only pays off when the delegate is reused many times, which is exactly why libraries build it once and cache it forever per type.

Follow-up questions:

  • How would you decide whether the one-time compilation cost of an Expression tree is worth it for a given call frequency?
  • What happens to a compiled expression delegate's performance for a struct instance versus a class instance?

Q3 What is UnsafeAccessor, and when would you reach for it instead of reflection?#

Short answer: UnsafeAccessorAttribute, in System.Runtime.CompilerServices since .NET 8, lets you declare an extern static method that the runtime implements as a direct call into an otherwise inaccessible member — a constructor, method, static method, field or static field — resolved and JIT-compiled like an ordinary call instead of looked up through reflection metadata at run time.

You reach for it when you need repeated, high-frequency access to a non-public member and reflection's per-call cost (or its incompatibility with trimming and Native AOT) is a real problem, typically inside a source generator's emitted code or a serializer's hot path. It only examines the exact declared type you name, not its base classes, and for a struct's instance members the first parameter must be passed by ref. The Name property on the attribute overrides the target member's name, which is essential when the accessor method's own name would otherwise collide or when you want a readable local name for a mangled one; Name must be left unset for UnsafeAccessorKind.Constructor.

C#
using System.Runtime.CompilerServices;

internal sealed class RateLimiter
{
    private int _tokensRemaining = 100;
    private RateLimiter(int capacity) => _tokensRemaining = capacity;
}

public static class RateLimiterAccessors
{
    [UnsafeAccessor(UnsafeAccessorKind.Constructor)]
    public static extern RateLimiter Create(int capacity);

    [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_tokensRemaining")]
    public static extern ref int TokensRemainingField(RateLimiter limiter);
}

// var limiter = RateLimiterAccessors.Create(50);
// ref int tokens = ref RateLimiterAccessors.TokensRemainingField(limiter); // no boxing, no MemberInfo

Unlike ordinary reflection, this bypasses accessibility at the IL level the same way trusted framework code always could, so it is a deliberate escape hatch, not a general substitute for proper APIs; use it for interop shims and generator-emitted code, not for reaching into a library's private state from application code as a matter of convenience.

What interviewers look for: knowing it is resolved at compile/JIT time rather than through System.Reflection, which is exactly why it is fast and AOT-compatible where classic reflection over private members is not.

Common mistakes: confusing UnsafeAccessor with unsafe pointer code — they are unrelated; this feature is fully verifiable, type-safe IL, just aimed at a member the accessibility rules would normally hide.

Q4 How does the dynamic keyword work under the hood, and how is it different from reflection?#

Short answer: dynamic defers binding to run time through the Dynamic Language Runtime: at each call site, the C# runtime binder (Microsoft.CSharp.RuntimeBinder) performs the same overload resolution the compiler would do statically, then caches that decision on the call site so repeated calls with the same argument types skip re-binding; it is a form of reflection under the hood, but with caching and full C# binding-rule fidelity built in.

The practical difference from hand-written reflection is that dynamic gives you real C# semantics — implicit conversions, operator overloading, indexers, extension methods in scope — resolved as if the compiler had known the type, whereas raw reflection APIs require you to reimplement any of that resolution logic yourself. The cost is that every dynamic operation allocates a CallSite<T> the first time it runs, and even a cached call site is measurably slower than a statically bound call because it still checks whether the argument types match the cached binding.

C#
using System.Dynamic;

dynamic value = GetValueFromSomewhere();  // static type is 'dynamic'
var result = value.Total + value.Tax;     // resolved at run time, cached per call site

// Roughly equivalent to (simplified) what the compiler generates for repeated use:
// CallSite<Func<CallSite, object, object>> site = CallSite<...>.Create(binder);
// site.Target(site, value);

static object GetValueFromSomewhere() => new { Total = 100m, Tax = 8.5m };

Prefer dynamic for COM interop, working with dynamically-shaped data (ExpandoObject, JSON-as-object scenarios before typed deserialization was common) or calling into dynamic languages; prefer generics or interfaces when the shape is knowable at compile time, since both give you compile-time checking that dynamic deliberately throws away. dynamic also disables IntelliSense and defers every typo to a runtime RuntimeBinderException.

What interviewers look for: the DLR/call-site mechanics specifically, not just "dynamic skips type checking," and a clear opinion on when giving up compile-time safety is actually worth it.

Follow-up questions:

  • Why is calling a dynamic member inside a loop with mixed argument types slower than with a single consistent type?
  • How does dynamic interact with method overload resolution when some arguments are dynamic and others are not?

Q5 How do source generators replace reflection in real framework code, and what changes for the developer?#

Short answer: A source generator inspects your code at compile time and emits ordinary C# that does the same job reflection would have done at run time — reading a [JsonSerializable] type's properties, or binding a [LoggerMessage] template — so the resulting program contains a real method call instead of a MethodInfo.Invoke, with no reflection metadata inspection left at run time at all.

The developer-visible change is usually a partial declaration: you write the shape (a partial method, a partial class deriving from JsonSerializerContext) and the generator fills in the body during the build, which you can inspect under obj/ with EmitCompilerGeneratedFiles turned on. The runtime payoff is consistent across the libraries that made this switch: faster startup (no reflection-based metadata scanning), lower steady-state allocation, and compatibility with trimming and Native AOT, since the generated code references members directly instead of by string name, so the trimmer can see exactly what is used and keep it. The source generators guide covers building one of these from scratch.

C#
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;

// Source-generated JSON: metadata built at compile time, not discovered via reflection.
[JsonSerializable(typeof(OrderDto))]
public partial class AppJsonContext : JsonSerializerContext { }

public static partial class OrderLog
{
    // No boxing, no format-string parsing at the call site; compiled to a direct call.
    [LoggerMessage(EventId = 1001, Level = LogLevel.Warning,
        Message = "Order {OrderId} exceeded the credit limit")]
    public static partial void CreditLimitExceeded(ILogger logger, string orderId);
}

public sealed record OrderDto(string Number, decimal Total);

The trade-off is that generators only cover the shapes their authors anticipated: a fully dynamic scenario, such as serializing a type discovered by name from a plugin loaded at run time, still needs reflection, Type.GetType, or an explicit fallback path, because a source generator cannot see a type it never observed at compile time.

What interviewers look for: understanding that the win is not "generators are magic," it is "the same work moves from run time to build time," with concrete consequences for startup, allocations and AOT support.

Q6 How do you design a custom attribute well, and what should you know about its performance?#

Short answer: Restrict AttributeTargets precisely, decide AllowMultiple and Inherited deliberately rather than accepting the defaults unconsidered, prefer a small number of constructor (positional) parameters for required data and settable properties (named parameters) for optional data, and remember that reading an attribute instance via GetCustomAttribute allocates a new instance on every call unless you cache the result.

Attributes are metadata, not code: applying one has zero runtime cost, but reading one always goes through reflection unless a source generator reads it at compile time instead, which is why high-traffic attribute-driven frameworks (ASP.NET Core's model binding, validation libraries) cache attribute instances per member the first time they are seen. AttributeUsage.Inherited defaults to true, which surprises people: a derived class implicitly carries a base class's attributes unless you set Inherited = false, and that inheritance is itself a reflection walk up the type hierarchy, another reason to cache.

C#
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public sealed class SensitiveAttribute : Attribute
{
    // Positional parameter: required, part of the attribute's identity.
    public SensitiveAttribute(SensitivityLevel level) => Level = level;

    public SensitivityLevel Level { get; }

    // Named parameter: optional, has a sensible default.
    public string? MaskWith { get; init; } = "***";
}

public enum SensitivityLevel { Low, High }

public sealed class CustomerProfile
{
    [Sensitive(SensitivityLevel.High, MaskWith = "[redacted]")]
    public string TaxId { get; init; } = "";
}

Seal attribute classes unless you specifically want a hierarchy of them; it communicates intent and matches how the overwhelming majority of framework attributes are declared. For AOT- and trimming-sensitive code, prefer inspecting CustomAttributeData (which reads raw metadata without instantiating the attribute or loading its dependencies) over GetCustomAttribute when you only need to know an attribute's arguments, not a live instance.

What interviewers look for: the AttributeUsage knobs used deliberately rather than left at their defaults, and awareness that reading attributes has a real, cacheable cost separate from applying them.

Common mistakes: forgetting Inherited = false on an attribute that should apply to exactly the type it is placed on, then being surprised when a derived class "inherits" a marker it was never meant to carry.

Q7 What breaks in reflection-heavy code under Native AOT and trimming, and how do you fix it?#

Short answer: Trimming removes code the analyzer cannot prove is reachable, so reflection that looks up a member by a string built at run time (Type.GetType(someVariable), GetMethod(name)) can silently fail because the target was trimmed away; Native AOT additionally has no JIT at all, so anything that generates code at run time — Reflection.Emit, DynamicMethod, compiled Expression trees, and runtime MakeGenericType/MakeGenericMethod over value types in some cases — simply cannot run and throws PlatformNotSupportedException or a RequiresDynamicCode warning at build time.

The fix path has three levels. First, prefer APIs that already have an AOT-safe implementation: System.Text.Json's source-generated JsonSerializerContext instead of the reflection-based serializer, [LoggerMessage] instead of reflection-driven logging. Second, when you must keep reflection, annotate it: [DynamicallyAccessedMembers] on a Type parameter tells the trimmer which members to preserve, and [RequiresUnreferencedCode] / [RequiresDynamicCode] mark an API as unsafe under trimming or AOT so callers get a build-time warning instead of a run-time surprise. Third, for genuinely dynamic scenarios (plugins, scripting), accept that the feature is JIT-only and document it, or design the plugin contract around source-generated metadata the plugin ships alongside its assembly.

C#
using System.Diagnostics.CodeAnalysis;

public static class Mapper
{
    [RequiresUnreferencedCode("Uses reflection to enumerate properties; not trim-safe.")]
    public static Dictionary<string, object?> ToDictionary(
        [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type type,
        object instance)
    {
        var result = new Dictionary<string, object?>();
        foreach (var property in type.GetProperties())
        {
            result[property.Name] = property.GetValue(instance);
        }

        return result;
    }
}

The source generators guide and the Native AOT and trimming guide both cover this trade-off table from the framework's own perspective.

What interviewers look for: distinguishing trimming problems (code removed, fixable with annotations) from Native AOT problems (no JIT at all, not fixable for Reflection.Emit-style APIs by any annotation), since candidates who blur the two give imprecise fixes.

Follow-up questions:

  • Why can [DynamicallyAccessedMembers] save a trimmed app but not a Native AOT one that also calls Reflection.Emit?
  • How would you test that a library is actually trim-safe before publishing it?

Q8 How does Reflection.Emit / DynamicMethod actually work, and is it still the right tool today?#

Short answer: DynamicMethod and the AssemblyBuilder/TypeBuilder family let you emit raw IL instructions at run time with ILGenerator, which the JIT then compiles into native code the first time it runs, giving you a real method with near-native call performance; today it is largely superseded by source generators for anything the generator can see at compile time, and by compiled expression trees for anything simpler that doesn't need full IL control.

DynamicMethod is the lighter-weight option: it produces a single method you can invoke through a delegate without ever defining a containing type or assembly, and it supports skipVisibility, letting generated code call private members directly, faster and more flexibly than reflection's own accessibility checks. Full Reflection.Emit with TypeBuilder is heavier: you build an entire dynamic assembly and type, useful for proxy generation, mocking frameworks and object-relational mappers that need a genuinely new type at run time, not just a method.

C#
using System.Reflection;
using System.Reflection.Emit;

// Emit a method equivalent to: static int AddOne(int x) => x + 1;
var method = new DynamicMethod("AddOne", typeof(int), [typeof(int)]);
var il = method.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldc_I4_1);
il.Emit(OpCodes.Add);
il.Emit(OpCodes.Ret);

var addOne = (Func<int, int>)method.CreateDelegate(typeof(Func<int, int>));
Console.WriteLine(addOne(41)); // 42, running as compiled native code after the first call

The honest answer to "is it still the right tool" is: rarely, for new code. It has no Native AOT support at all, it is the hardest of these techniques to get right (hand-written IL fails at run time with an opaque InvalidProgramException if the stack balance is wrong), and most of its historical use cases — fast property access, dynamic proxies, serializers — now have a source-generator-based or expression-tree-based alternative that is safer to write and AOT-compatible. It remains the right tool inside JIT-only, performance-critical infrastructure where you need full control over the emitted IL that expression trees do not expose, such as some mocking and ORM libraries' proxy generation.

What interviewers look for: respect for how powerful and dangerous this API is, plus the judgment to reach for a source generator or expression tree first in 2026, not nostalgia for hand-rolled IL as the default answer.

Q9 How would you build a fast, reusable property accessor cache for a serializer or object mapper?#

Short answer: Build a ConcurrentDictionary<Type, TypeAccessor> where each TypeAccessor holds compiled getter and setter delegates for every relevant property, built once per type on first use with Expression.Lambda(...).Compile(), and reused for the lifetime of the process; never call PropertyInfo.GetValue/SetValue in a loop that runs per object instance.

The design has two moving parts: the one-time build step, which does the reflection and expression compilation, and the steady-state read/write step, which only calls delegates. Getting the caching key right matters as much as the delegate itself — keying purely on Type is usually enough for a mapper, but a serializer that also depends on attributes (naming policies, ignore flags) needs those baked into the cached model too, so a shape change in configuration invalidates the right cache entries.

C#
using System.Linq.Expressions;
using System.Reflection;
using System.Collections.Concurrent;

public sealed class TypeAccessor
{
    private static readonly ConcurrentDictionary<Type, TypeAccessor> Cache = new();
    private readonly Dictionary<string, Func<object, object?>> _getters;

    private TypeAccessor(Type type)
    {
        _getters = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
            .Where(p => p.CanRead && p.GetIndexParameters().Length == 0)
            .ToDictionary(p => p.Name, BuildGetter);
    }

    public static TypeAccessor For(Type type) => Cache.GetOrAdd(type, t => new TypeAccessor(t));

    public object? Get(object instance, string propertyName) => _getters[propertyName](instance);

    private static Func<object, object?> BuildGetter(PropertyInfo property)
    {
        var instanceParam = Expression.Parameter(typeof(object), "instance");
        var typed = Expression.Convert(instanceParam, property.DeclaringType!);
        var access = Expression.Convert(Expression.Property(typed, property), typeof(object));
        return Expression.Lambda<Func<object, object?>>(access, instanceParam).Compile();
    }
}

This is precisely the shape used by serialization and mapping libraries before they adopted source generators; it remains the right answer for any scenario where the set of types is only known at run time, since a source generator cannot emit code for a type it never saw during your build.

What interviewers look for: the two-phase design (build once, execute many times) stated explicitly, and awareness of thread-safety during the build phase (ConcurrentDictionary.GetOrAdd can run the factory more than once under contention, so the factory itself must be side-effect-free).

Q10 What are the security and versioning risks of reflecting over non-public members?#

Short answer: Modern .NET has no Code Access Security or partial-trust sandboxing, so BindingFlags.NonPublic reflection over private members is unrestricted by the runtime; the real risks are architectural, not permission-based: you couple your code to another type's implementation details, which can change in any release without a version bump in the public contract, and you widen the attack surface for anything that reflects over types loaded from untrusted input.

Because private and internal are compile-time accessibility rules enforced by the compiler, not the runtime, reflection with BindingFlags.NonPublic | BindingFlags.Instance can read and even set fields a type's author never intended external code to touch. InternalsVisibleTo is the sanctioned version of crossing that boundary — it grants a specific, named friend assembly compiler-checked access to internal members, which is visible in source control and reviewed like any other API surface change, unlike reflection, which is invisible until it breaks. A library that reflects into another library's private state (a common "fix" for a missing extensibility point) has no contract to rely on: the next patch release can rename or remove the field with no warning, because from the original author's point of view, it was never public.

The narrower security concern is reflection combined with untrusted input: constructing a Type from a string supplied by a caller and reflecting over it (or worse, invoking a member on it) is a well-known building block for insecure deserialization and plugin-loading vulnerabilities, because it lets an attacker influence which code runs. Validate and allow-list types before reflecting over anything that originated outside your trust boundary, and prefer a narrow, explicit plugin contract (an interface, or attribute-driven registration checked against an allow-list) over open-ended Type.GetType(untrustedName).

What interviewers look for: correcting the outdated assumption that reflection is "sandboxed" by the runtime, and reframing the risk correctly as coupling and input-trust, which is the risk model that actually applies to modern .NET.

Common mistakes: citing Code Access Security or partial trust as a current .NET protection — it was removed from .NET Core onward and does not apply to reflection today.

Quick-Fire Round#

QuestionAnswer
What's the main runtime cost of MethodInfo.Invoke?Argument/return boxing plus general-purpose dispatch, on every call.
What .NET version introduced UnsafeAccessorAttribute?.NET 8.
Can UnsafeAccessor reach members of a base class?No, only the exact declared type is examined.
What resolves a dynamic call at run time?The C# runtime binder, via a cached CallSite<T>.
Does trimming or Native AOT support Reflection.Emit?Neither; there is no JIT to run emitted IL under Native AOT.
What attribute tells the trimmer which members a Type parameter needs preserved?[DynamicallyAccessedMembers].
What's the AOT-safe way to inspect an attribute's arguments without instantiating it?Read CustomAttributeData instead of calling GetCustomAttribute.
Does AttributeUsage.Inherited default to true or false?True.
What exception can badly emitted IL throw at run time?InvalidProgramException.
Does modern .NET sandbox reflection over private members?No, Code Access Security and partial trust no longer exist.

How to Prepare#

  • Implement a property-getter cache twice: once with raw reflection, once with compiled expressions, and be ready to explain the performance gap in your own words.
  • Write a minimal UnsafeAccessorAttribute example from memory, including the extern static signature shape.
  • Practice explaining the DLR call-site caching model for dynamic without hand-waving "it's slow reflection."
  • Pick one framework feature (System.Text.Json, [LoggerMessage], configuration binding) and describe exactly what its source generator replaces.
  • Review the trimming annotations ([DynamicallyAccessedMembers], [RequiresUnreferencedCode], [RequiresDynamicCode]) and what each one actually guarantees.
  • Prepare a concrete story about a private-member reflection dependency that broke on an upgrade, and what you changed afterward.