Reflection is runtime metadata inspection and dynamic member access through System.Reflection. It is the mechanism behind many framework features (DI containers, serializers, test discovery, plugin loading), but it trades compile-time guarantees for runtime flexibility. In practice, use reflection when the target type/member is unknown until runtime, and avoid it on hot paths unless you cache metadata or compile delegates.

How It Works

At runtime, the CLR exposes assembly/type/member metadata via objects like Type, MethodInfo, PropertyInfo, and ConstructorInfo.

Typical flow:

  1. Get a Type (typeof(T), obj.GetType(), or loading an assembly).
  2. Select members using APIs like GetMethods, GetProperty, GetConstructors with BindingFlags.
  3. Read metadata (Name, Attributes, parameters, custom attributes).
  4. Optionally invoke dynamically (MethodInfo.Invoke) or construct instances (Activator.CreateInstance).
using System;
using System.Linq;
using System.Reflection;
 
Type t = typeof(string);
MethodInfo[] publicInstanceMethods = t.GetMethods(BindingFlags.Public | BindingFlags.Instance);
 
foreach (var m in publicInstanceMethods)
{
    Console.WriteLine($"{m.Name}({string.Join(", ", m.GetParameters().Select(p => p.ParameterType.Name))})");
}

Common Patterns

  • Attribute-driven behavior: scan types/members and read attributes to decide routing, validation, serialization, or registration.
  • Dynamic activation: create objects from discovered types (for example, plugin types implementing an interface).
  • Late-bound invocation: call members by name when contracts are not known at compile time.
  • Metadata analysis tools: generate docs, diagnostics, or code based on assembly/type information.

Example (attribute lookup + invoke):

using System;
using System.Linq;
using System.Reflection;
 
[AttributeUsage(AttributeTargets.Method)]
public sealed class JobAttribute : Attribute
{
    public string Name { get; }
    public JobAttribute(string name) => Name = name;
}
 
public sealed class Jobs
{
    [Job("rebuild-index")]
    public void RebuildIndex() => Console.WriteLine("Index rebuilt");
}
 
var target = new Jobs();
var method = typeof(Jobs)
    .GetMethods(BindingFlags.Public | BindingFlags.Instance)
    .FirstOrDefault(m => m.GetCustomAttribute<JobAttribute>()?.Name == "rebuild-index");
 
method?.Invoke(target, null);

From Slow Reflection to Fast Access

MethodInfo.Invoke is slow because every call re-marshals arguments and does runtime checks. When you call the same member repeatedly, do the reflection once and cache a delegate:

// Bind a strongly-typed delegate to a discovered method — invoked at near-direct-call speed
var action = (Action<Jobs>)method!.CreateDelegate(typeof(Action<Jobs>));
action(target); // no per-call reflection overhead
 
// Or compile an expression tree (works for constructors, property getters, etc.)
var ctor = Expression.Lambda<Func<Jobs>>(Expression.New(typeof(Jobs))).Compile();

For runtime code generation there’s System.Reflection.Emit / DynamicMethod (emit IL directly) — powerful but incompatible with NativeAOT/trimming since there’s no JIT to compile the emitted IL. On AOT targets, prefer source generators, which produce the code at build time.

To reach private members, the old way is BindingFlags.NonPublic (slow, breaks encapsulation, fragile under trimming). On .NET 8+ use [UnsafeAccessor] — a zero-overhead, AOT-safe, statically-checked way to call a private method or access a private field without reflection at all.

Pitfalls

  • Reflection is slower than direct calls because it does metadata lookup, boxing, and runtime checks; repeated uncached lookups (GetMethod/GetProperty in loops) can become a major throughput bottleneck. Cache MemberInfo and prefer compiled delegates for hot paths.
  • BindingFlags mistakes often return empty results or surprising member sets (for example, missing Instance/Static or Public/NonPublic combinations). Always specify flags explicitly and test inherited/non-public scenarios.
  • Reflection-heavy code can fail under trimming/AOT when required members are removed because the linker cannot infer dynamic access. For types known at compile time, use linker annotations like DynamicallyAccessedMembers; for truly dynamic scenarios (for example plugin type names from config), expect RequiresUnreferencedCode warnings and consider explicit registration or source generation.

Tradeoffs

  • Reflection vs interfaces/generics: reflection is more flexible for unknown types, while interfaces/generics are faster, safer, and easier to refactor.
  • Reflection invocation vs compiled delegates: MethodInfo.Invoke is simpler but slower; delegate compilation has upfront complexity but pays off for repeated calls.
  • Runtime discovery vs source generation: runtime discovery minimizes build-time setup, while source generation improves startup/performance and is more trim/AOT friendly.

Questions

References