Reflection
26 min read- Reflection in C# — Comprehensive Study Guide
- System.Reflection Namespace Overview
- Type Inspection at Runtime
- Accessing and Invoking Members Dynamically
- MethodInfo, PropertyInfo, FieldInfo, ConstructorInfo Deep Dive
- Custom Attributes and [AttributeUsage]
- Generic Type Reflection
- Assembly Loading and Inspection
- Dynamic Object Creation (Activator.CreateInstance)
- Expression Trees vs Reflection
- Property Getter via Expression
- Property Setter via Expression
- Method Invocation via Expression
- Source Generators as Compile-Time Alternative
- Reflection.Emit for Dynamic Type Creation
- Performance Implications and Benchmarks
- Real-World Use Cases (ORMs, DI Containers, Serializers)
- Security Considerations
- Comprehensive Q&A Summary
Reflection in C# — Comprehensive Study Guide
"Reflection lets your program look in the mirror: it can discover its own types, read metadata, and invoke members it has never seen at compile time."
Use this guide to master System.Reflection for interviews. Every section follows the Q&A format interviewers use, with working C# code examples, performance trade-offs, and real-world patterns.
System.Reflection Namespace Overview
System.Reflection namespace and what problem does it solve?A: System.Reflection is the set of runtime APIs that let you inspect and interact with CLR metadata embedded in every .NET assembly. The metadata describes every type, method, property, field, event, and custom attribute the compiler recorded. It solves the problem of needing to work with types that are unknown at compile time — plugin systems, serializers, DI containers, and test runners all rely on it.
| Class | Purpose |
|---|---|
Type | The entry point. Represents a type and exposes its members. |
MethodInfo | Describes a method and lets you invoke it. |
PropertyInfo | Describes a property; read/write via GetValue/SetValue. |
FieldInfo | Describes a field; read/write via GetValue/SetValue. |
ConstructorInfo | Describes a constructor; invoke to create instances. |
EventInfo | Describes an event; add/remove handlers dynamically. |
ParameterInfo | Describes a method/constructor parameter. |
Assembly | Represents a loaded assembly; enumerate types. |
Module | Represents a module within an assembly. |
CustomAttributeData | Read attribute metadata without instantiating the attribute. |
A: The namespace provides a class hierarchy where every metadata element has a corresponding info type:
using System.Reflection;
// All of the above live in this namespace.
// Most reflection work starts from a Type object.MemberInfo and the specific info types?A: MemberInfo is the abstract base class for MethodInfo, PropertyInfo, FieldInfo, ConstructorInfo, and EventInfo. It provides common properties like Name, DeclaringType, MemberType, and GetCustomAttributes(). This inheritance lets you write generic code that operates on any kind of member.
MemberInfo[] members = typeof(string).GetMembers();
foreach (var m in members)
{
Console.WriteLine($"{m.MemberType}: {m.Name}");
// Output: Method: Contains, Property: Length, etc.
}Type Inspection at Runtime
Type object, and when would you use each?A: There are three approaches, each suited to a different scenario:
// 1. typeof — compile-time, no instance needed
// Use when you know the type at compile time.
Type t1 = typeof(string);
// 2. GetType() — from an existing instance
// Use when you have an object and need its runtime (concrete) type.
object obj = "hello";
Type t2 = obj.GetType();
// 3. Type.GetType() — from a string name
// Use when the type name comes from configuration or external input.
// Requires assembly-qualified name for types outside the calling assembly.
Type? t3 = Type.GetType("System.String");
Type? t4 = Type.GetType("MyApp.Services.OrderService, MyApp");
Key difference: typeof(T) always returns the compile-time type, while GetType() returns the actual runtime type. For a variable declared as IList<int> holding a List<int>, GetType() returns List<int>.
Type class expose for inspection?A: Type has dozens of properties for classifying and describing a type:
Type type = typeof(Dictionary<string, int>);
Console.WriteLine(type.FullName); // System.Collections.Generic.Dictionary`2[...]
Console.WriteLine(type.Name); // Dictionary`2
Console.WriteLine(type.Namespace); // System.Collections.Generic
Console.WriteLine(type.Assembly.FullName); // System.Private.CoreLib, ...
Console.WriteLine(type.IsGenericType); // True
Console.WriteLine(type.IsAbstract); // False
Console.WriteLine(type.IsInterface); // False
Console.WriteLine(type.IsValueType); // False
Console.WriteLine(type.IsSealed); // False
Console.WriteLine(type.IsClass); // True
Console.WriteLine(type.BaseType?.Name); // ObjectInterview Angle:
IsAssignableFromreads "backwards" relative to natural language. The receiver is the base; the argument is the derived type. Interviewers sometimes trip candidates on the direction.
A: Use IsAssignableFrom or IsSubclassOf for hierarchy checks. Be careful with the direction of IsAssignableFrom — it reads "backwards" relative to natural language.
// Is a specific type or derived from it?
// Reads: "Can an InvalidOperationException be assigned to an Exception variable?"
typeof(Exception).IsAssignableFrom(typeof(InvalidOperationException)); // true
// Interface implementation check
typeof(IDisposable).IsAssignableFrom(typeof(MemoryStream)); // true
// IsSubclassOf — excludes the type itself (unlike IsAssignableFrom)
typeof(InvalidOperationException).IsSubclassOf(typeof(Exception)); // true
typeof(Exception).IsSubclassOf(typeof(Exception)); // false
// Preferred in newer C# for runtime checks on instances:
bool isStream = someObject is Stream;Accessing and Invoking Members Dynamically
A: Use GetMethods(), GetProperties(), GetFields(), and GetConstructors() with BindingFlags to control what is included:
Type type = typeof(OrderService);
// Get all public instance methods
MethodInfo[] methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance);
// Get all properties, including non-public
PropertyInfo[] props = type.GetProperties(
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
// Get all fields, including private
FieldInfo[] fields = type.GetFields(
BindingFlags.NonPublic | BindingFlags.Instance);
// Get constructors
ConstructorInfo[] ctors = type.GetConstructors();| Flag | Meaning |
|---|---|
Public | Include public members |
NonPublic | Include internal, protected, private |
Instance | Include instance members |
Static | Include static members |
DeclaredOnly | Exclude inherited members |
FlattenHierarchy | Include static members from base types |
BindingFlags work, and what are the common pitfalls?A: BindingFlags is a flags enum that filters which members are returned. You must combine at least one visibility flag (Public or NonPublic) with at least one scope flag (Instance or Static), otherwise nothing is returned.
A common pitfall is omitting Instance when looking for instance members, getting an empty result, and assuming the member does not exist.
MethodInfo?A: Call GetMethod() to get the MethodInfo, then call Invoke() with the target instance and parameters:
public class Calculator
{
public int Add(int a, int b) => a + b;
}
var calc = new Calculator();
Type type = calc.GetType();
MethodInfo? addMethod = type.GetMethod("Add");
// Invoke: (instance, parameters) -> object?
object? result = addMethod?.Invoke(calc, new object[] { 3, 5 });
Console.WriteLine(result); // 8PropertyInfo?A: Use GetValue() and SetValue() on a PropertyInfo obtained via GetProperty():
public class Trade
{
public string Symbol { get; set; } = "AAPL";
public decimal Price { get; set; } = 150.25m;
}
var trade = new Trade();
Type type = trade.GetType();
PropertyInfo? symbolProp = type.GetProperty("Symbol");
string? symbol = (string?)symbolProp?.GetValue(trade); // "AAPL"
symbolProp?.SetValue(trade, "MSFT");
Console.WriteLine(trade.Symbol); // "MSFT"Interview Angle: Accessing private members is a red flag in production code. Interviewers may ask when it is acceptable: testing internal state, migrating legacy code, or framework internals where you control both sides.
FieldInfo?A: Pass BindingFlags.NonPublic | BindingFlags.Instance to GetField():
public class Connection
{
private readonly string _connectionString = "Server=localhost";
}
var conn = new Connection();
FieldInfo? field = typeof(Connection).GetField(
"_connectionString",
BindingFlags.NonPublic | BindingFlags.Instance);
string? value = (string?)field?.GetValue(conn); // "Server=localhost"
// WARNING: Writing to readonly fields is possible but dangerous
// and may behave unexpectedly with JIT optimizations.MethodInfo, PropertyInfo, FieldInfo, ConstructorInfo Deep Dive
MethodInfo?A: MethodInfo exposes the method's name, return type, visibility, parameters, and more:
MethodInfo? method = typeof(string).GetMethod(
"Contains", new[] { typeof(string) });
Console.WriteLine(method?.Name); // Contains
Console.WriteLine(method?.ReturnType.Name); // Boolean
Console.WriteLine(method?.IsPublic); // True
Console.WriteLine(method?.IsStatic); // False
Console.WriteLine(method?.IsVirtual); // True
// Parameter inspection
ParameterInfo[] parameters = method?.GetParameters() ?? Array.Empty<ParameterInfo>();
foreach (var p in parameters)
{
Console.WriteLine($" {p.Name}: {p.ParameterType.Name} (position {p.Position})");
}
// Output: value: String (position 0)PropertyInfo?A: You can determine the property type, whether it has getters/setters, and the underlying accessor method names:
PropertyInfo prop = typeof(Trade).GetProperty("Price")!;
Console.WriteLine(prop.PropertyType.Name); // Decimal
Console.WriteLine(prop.CanRead); // True
Console.WriteLine(prop.CanWrite); // True
Console.WriteLine(prop.GetMethod?.Name); // get_Price
Console.WriteLine(prop.SetMethod?.Name); // set_Price
// Init-only properties: SetMethod exists but calling SetValue
// after construction may throw in some scenarios.FieldInfo?A: You can determine the field type, whether it is static, const, or readonly:
FieldInfo field = typeof(int).GetField("MaxValue")!;
Console.WriteLine(field.FieldType.Name); // Int32
Console.WriteLine(field.IsStatic); // True
Console.WriteLine(field.IsLiteral); // True (it's a const)
Console.WriteLine(field.IsInitOnly); // False (const, not readonly)
Console.WriteLine(field.GetValue(null)); // 2147483647ConstructorInfo to wire dependencies?A: They inspect constructors to determine required dependencies, then resolve each parameter recursively:
public class OrderProcessor
{
private readonly ILogger _logger;
private readonly IOrderRepository _repo;
public OrderProcessor(ILogger logger, IOrderRepository repo)
{
_logger = logger;
_repo = repo;
}
}
ConstructorInfo[] ctors = typeof(OrderProcessor).GetConstructors();
foreach (var ctor in ctors)
{
var parms = ctor.GetParameters();
Console.WriteLine($"Ctor with {parms.Length} params:");
foreach (var p in parms)
Console.WriteLine($" {p.ParameterType.Name} {p.Name}");
}
// Create instance via ConstructorInfo
// (DI containers use patterns very similar to this)
ConstructorInfo ci = ctors[0];
object instance = ci.Invoke(new object[] { logger, repo });Custom Attributes and [AttributeUsage]
ValidOn(first argument): Where the attribute can be applied (Class,Method,Property,All, etc.).AllowMultiple: Whether multiple instances on the same target are allowed.Inherited: Whether derived classes/overriding methods inherit the attribute.
A: Custom attributes are metadata annotations that derive from System.Attribute. The [AttributeUsage] attribute controls where, how many times, and whether they inherit:
[AttributeUsage(
AttributeTargets.Class | AttributeTargets.Method,
AllowMultiple = false,
Inherited = true)]
public class AuditAttribute : Attribute
{
public string Reason { get; }
public int SeverityLevel { get; set; } = 1;
public AuditAttribute(string reason)
{
Reason = reason;
}
}
AttributeUsage controls:
A: Apply with [MyAttribute] syntax and read with GetCustomAttribute<T>() or GetCustomAttributes():
[Audit("Financial transaction", SeverityLevel = 3)]
public class TransferService
{
[Audit("Initiates wire transfer")]
public void Execute(TransferRequest request) { /* ... */ }
}
// Reading at runtime
Type type = typeof(TransferService);
// On the class
var classAttr = type.GetCustomAttribute<AuditAttribute>();
Console.WriteLine($"Class audit: {classAttr?.Reason}"); // Financial transaction
// On the method
MethodInfo method = type.GetMethod("Execute")!;
var methodAttr = method.GetCustomAttribute<AuditAttribute>();
Console.WriteLine($"Method audit: {methodAttr?.Reason}"); // Initiates wire transferGetCustomAttribute and CustomAttributeData?A: GetCustomAttribute instantiates the attribute (running its constructor), while CustomAttributeData reads raw metadata without instantiation. This matters when the attribute constructor has side effects or dependencies.
// Find all types in an assembly decorated with a specific attribute
var auditedTypes = Assembly.GetExecutingAssembly()
.GetTypes()
.Where(t => t.GetCustomAttribute<AuditAttribute>() != null);
// Lazy reading: CustomAttributeData avoids instantiating the attribute
var attrData = CustomAttributeData.GetCustomAttributes(type);
foreach (var data in attrData)
{
Console.WriteLine($"Attribute: {data.AttributeType.Name}");
foreach (var arg in data.ConstructorArguments)
Console.WriteLine($" Arg: {arg.Value}");
foreach (var named in data.NamedArguments)
Console.WriteLine($" {named.MemberName} = {named.TypedValue.Value}");
}A: Define attribute types for each rule, then scan properties at runtime:
[AttributeUsage(AttributeTargets.Property)]
public class RequiredAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Property)]
public class MaxLengthAttribute : Attribute
{
public int Length { get; }
public MaxLengthAttribute(int length) => Length = length;
}
public class Order
{
[Required]
[MaxLength(10)]
public string Symbol { get; set; } = "";
[Required]
public decimal? Price { get; set; }
}
public static List<string> Validate(object obj)
{
var errors = new List<string>();
foreach (var prop in obj.GetType().GetProperties())
{
var value = prop.GetValue(obj);
if (prop.GetCustomAttribute<RequiredAttribute>() != null
&& (value is null || (value is string s && string.IsNullOrEmpty(s))))
{
errors.Add($"{prop.Name} is required.");
}
if (prop.GetCustomAttribute<MaxLengthAttribute>() is { } maxLen
&& value is string str && str.Length > maxLen.Length)
{
errors.Add($"{prop.Name} exceeds max length of {maxLen.Length}.");
}
}
return errors;
}Generic Type Reflection
A: An open generic type has unresolved type parameters (e.g., List<>), while a closed generic type has all parameters resolved (e.g., List<int>). DI containers and mediator patterns use this distinction to match IHandler<T> to concrete handlers.
// Open generic — has unresolved type parameters
Type openList = typeof(List<>);
Console.WriteLine(openList.IsGenericTypeDefinition); // True
Console.WriteLine(openList.ContainsGenericParameters); // True
// Closed generic — all type parameters are resolved
Type closedList = typeof(List<string>);
Console.WriteLine(closedList.IsGenericTypeDefinition); // False
Console.WriteLine(closedList.ContainsGenericParameters); // FalseA: Use MakeGenericType() on the open generic type definition:
Type openDict = typeof(Dictionary<,>);
Type closedDict = openDict.MakeGenericType(typeof(string), typeof(int));
object instance = Activator.CreateInstance(closedDict)!;
Console.WriteLine(instance.GetType().Name); // Dictionary`2A: Get the open generic method definition, then call MakeGenericMethod() with concrete type arguments:
public class Serializer
{
public T Deserialize<T>(string json) => default!;
}
// Get the open generic method definition
MethodInfo openMethod = typeof(Serializer)
.GetMethod("Deserialize")!;
// Close it with a concrete type
MethodInfo closedMethod = openMethod.MakeGenericMethod(typeof(Order));
var serializer = new Serializer();
object? result = closedMethod.Invoke(serializer, new object[] { "{}" });Interview Angle: DI containers and MediatR use exactly this pattern to auto-register handlers. Be ready to walk through the
GetGenericTypeDefinition()check.
ICommandHandler<T>?A: Scan assembly types and inspect their interfaces, checking GetGenericTypeDefinition():
// Find all types that implement ICommandHandler<T> for any T
var handlerTypes = Assembly.GetExecutingAssembly()
.GetTypes()
.Where(t => t.GetInterfaces().Any(i =>
i.IsGenericType &&
i.GetGenericTypeDefinition() == typeof(ICommandHandler<>)))
.ToList();
// Extract the T from each implementation
foreach (var handler in handlerTypes)
{
var commandType = handler.GetInterfaces()
.First(i => i.IsGenericType &&
i.GetGenericTypeDefinition() == typeof(ICommandHandler<>))
.GetGenericArguments()[0];
Console.WriteLine($"{handler.Name} handles {commandType.Name}");
}Assembly Loading and Inspection
A: .NET provides several loading mechanisms, each with different use cases:
// Already loaded — get the assembly containing the current code
Assembly executing = Assembly.GetExecutingAssembly();
Assembly entry = Assembly.GetEntryAssembly()!;
Assembly calling = Assembly.GetCallingAssembly();
// Load by name (from probing paths / GAC)
Assembly byName = Assembly.Load("Newtonsoft.Json");
// Load from a specific file path
Assembly fromFile = Assembly.LoadFrom(@"C:\plugins\MyPlugin.dll");
// Reflection-only context (older .NET Framework; not available in .NET Core)
// Use MetadataLoadContext in .NET Core for inspection without executionA: GetTypes() throws ReflectionTypeLoadException if any type fails to load. Use a safe pattern for plugin hosts:
Assembly assembly = Assembly.GetExecutingAssembly();
// GetTypes() throws ReflectionTypeLoadException if any type fails to load
// GetExportedTypes() returns only public types
Type[] allTypes = assembly.GetTypes();
Type[] publicTypes = assembly.GetExportedTypes();
// Safe pattern for partial-load scenarios (plugin hosts)
Type[] SafeGetTypes(Assembly asm)
{
try
{
return asm.GetTypes();
}
catch (ReflectionTypeLoadException ex)
{
return ex.Types.Where(t => t != null).ToArray()!;
}
}MetadataLoadContext and why does it matter?A: MetadataLoadContext loads assemblies for inspection only, without executing any code. It cannot instantiate types or invoke methods. This is critical for security (inspecting untrusted plugins), build-time tools, and scenarios where you need metadata without side effects like static constructors running.
// Inspect assemblies without loading them into the execution context
// Useful for build tools, analyzers, and plugin validation
var resolver = new PathAssemblyResolver(new[]
{
typeof(object).Assembly.Location,
@"C:\plugins\MyPlugin.dll"
});
using var mlc = new MetadataLoadContext(resolver);
Assembly inspected = mlc.LoadFromAssemblyPath(@"C:\plugins\MyPlugin.dll");
foreach (var type in inspected.GetTypes())
Console.WriteLine(type.FullName);
// Types loaded here cannot be instantiated or invokedAssemblyLoadContext enable plugin isolation in .NET Core?A: AssemblyLoadContext provides dependency isolation so plugins can load different versions of shared libraries without conflicting with the host:
public class PluginLoadContext : AssemblyLoadContext
{
private readonly AssemblyDependencyResolver _resolver;
public PluginLoadContext(string pluginPath) : base(isCollectible: true)
{
_resolver = new AssemblyDependencyResolver(pluginPath);
}
protected override Assembly? Load(AssemblyName assemblyName)
{
string? assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
return assemblyPath != null ? LoadFromAssemblyPath(assemblyPath) : null;
}
}
// Usage — load plugin in isolated context
var context = new PluginLoadContext(pluginDllPath);
Assembly pluginAssembly = context.LoadFromAssemblyPath(pluginDllPath);
// When done, unload the context to free memory
context.Unload();Dynamic Object Creation (Activator.CreateInstance)
Activator.CreateInstance?A: Activator provides several overloads for different scenarios:
// Parameterless constructor
object list = Activator.CreateInstance(typeof(List<int>))!;
// With constructor arguments
object trade = Activator.CreateInstance(
typeof(Trade),
new object[] { "AAPL", 150.25m })!;
// Generic helper
T Create<T>() where T : new() => Activator.CreateInstance<T>();
// From a type name string (cross-assembly)
object? service = Activator.CreateInstance(
"MyApp.Services", // assembly name
"MyApp.Services.OrderService")?.Unwrap();Activator.CreateInstance vs new, and how do you fix it?A: Activator.CreateInstance is ~50-150x slower than new because it uses reflection internally. For hot paths, compile a constructor delegate instead:
public static class FastActivator<T>
{
// Compiled once, reused forever
public static readonly Func<T> Create = BuildActivator();
private static Func<T> BuildActivator()
{
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();
}
}
// Usage — near-zero overhead after first call
var trade = FastActivator<Trade>.Create();ConstructorInfo.Invoke, Activator.CreateInstance, and compiled delegates compare?A: They form a speed ladder from slowest to fastest:
// Slowest — full reflection dispatch every call
var obj1 = ctor.Invoke(Array.Empty<object>());
// Medium — Activator caches some metadata internally
var obj2 = Activator.CreateInstance(type);
// Fastest (reflection-based) — one-time compile, then native speed
var obj3 = compiledFactory();Expression Trees vs Reflection
A: Expression trees let you build code at runtime and compile it to a delegate. You pay the reflection cost once (at compile time) and then invoke the delegate at native speed.
Property Getter via Expression
public static Func<TObj, TProp> BuildGetter<TObj, TProp>(string propertyName)
{
var param = Expression.Parameter(typeof(TObj), "obj");
var propAccess = Expression.Property(param, propertyName);
var lambda = Expression.Lambda<Func<TObj, TProp>>(propAccess, param);
return lambda.Compile();
}
// Build once
var getSymbol = BuildGetter<Trade, string>("Symbol");
// Call millions of times with zero reflection cost
string symbol = getSymbol(trade);
Property Setter via Expression
public static Action<TObj, TProp> BuildSetter<TObj, TProp>(string propertyName)
{
var objParam = Expression.Parameter(typeof(TObj), "obj");
var valParam = Expression.Parameter(typeof(TProp), "value");
var propAccess = Expression.Property(objParam, propertyName);
var assign = Expression.Assign(propAccess, valParam);
var lambda = Expression.Lambda<Action<TObj, TProp>>(assign, objParam, valParam);
return lambda.Compile();
}
var setSymbol = BuildSetter<Trade, string>("Symbol");
setSymbol(trade, "GOOG"); // Fast, no reflection per call
Method Invocation via Expression
public static Func<object, object?[], object?> BuildMethodInvoker(MethodInfo method)
{
var instanceParam = Expression.Parameter(typeof(object), "instance");
var argsParam = Expression.Parameter(typeof(object?[]), "args");
var castInstance = Expression.Convert(instanceParam, method.DeclaringType!);
var parameters = method.GetParameters();
var argExpressions = parameters.Select((p, i) =>
Expression.Convert(
Expression.ArrayIndex(argsParam, Expression.Constant(i)),
p.ParameterType))
.ToArray<Expression>();
var call = Expression.Call(castInstance, method, argExpressions);
var castResult = Expression.Convert(call, typeof(object));
var lambda = Expression.Lambda<Func<object, object?[], object?>>(
castResult, instanceParam, argsParam);
return lambda.Compile();
}
CreateDelegate?A: Use CreateDelegate when you have a known method signature (it is the simplest and fastest). Use expression trees when you need to compose multiple operations (property chains, type conversions) or when signatures are not known at compile time. Use raw MethodInfo.Invoke only for cold paths or one-off calls where the overhead of building and compiling an expression is not justified.
// CreateDelegate — simplest, fastest, requires known signature
MethodInfo addMethod = typeof(Calculator).GetMethod("Add")!;
var addDelegate = (Func<Calculator, int, int, int>)
Delegate.CreateDelegate(typeof(Func<Calculator, int, int, int>), addMethod);
var calc = new Calculator();
int result = addDelegate(calc, 3, 5); // As fast as a direct callSource Generators as Compile-Time Alternative
| Reflection Approach | Source Generator Approach |
|---|---|
JsonSerializer inspects types at runtime | JsonSerializerContext generates serializers at compile time |
| DI scans assemblies for registrations | Generator emits AddServices() method with explicit registrations |
| AutoMapper inspects properties at runtime | Generator emits mapping methods at compile time |
A: Source generators (introduced in .NET 5) run during compilation and emit C# source code. Instead of discovering types/properties at runtime, the generated code contains explicit, strongly-typed logic. They replace the "discover at runtime" pattern with "generate at compile time."
System.Text.Json source generation work?A: You define a context class and the generator emits optimized serializers:
// Define a context — the generator emits optimized serializers
[JsonSerializable(typeof(Trade))]
[JsonSerializable(typeof(List<Trade>))]
public partial class AppJsonContext : JsonSerializerContext { }
// Usage — no runtime reflection
string json = JsonSerializer.Serialize(trade, AppJsonContext.Default.Trade);
Trade? deserialized = JsonSerializer.Deserialize(json, AppJsonContext.Default.Trade);- Performance: No runtime metadata inspection; generated code is as fast as hand-written.
- Trimming-safe: The linker can see exactly which members are used.
- AOT-compatible: No dynamic code generation needed.
- Compile-time errors: Misconfigurations fail the build instead of throwing at runtime.
Interview Angle: When asked "How would you avoid reflection overhead?", source generators are the modern answer for .NET 6+. Know the
System.Text.Jsonsource gen story and be able to contrast it withNewtonsoft.Json.
A: Four key benefits:
A: Source generators can only add code, not modify existing code. They cannot see other generated code (no chaining). They run at compile time, so they cannot handle truly dynamic scenarios (user-provided type names, runtime-loaded plugins). For those cases, reflection remains necessary.
Reflection.Emit for Dynamic Type Creation
Reflection.Emit and when would you use it?A: System.Reflection.Emit lets you generate IL at runtime to define new types and methods. It is the most powerful but most complex reflection API.
using System.Reflection.Emit;
// 1. Create a dynamic assembly and module
var assemblyName = new AssemblyName("DynamicProxy");
var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(
assemblyName, AssemblyBuilderAccess.Run);
var moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule");
// 2. Define a type
var typeBuilder = moduleBuilder.DefineType(
"DynamicGreeter",
TypeAttributes.Public | TypeAttributes.Class);
// 3. Add a method: public string Greet(string name) => "Hello, " + name;
var methodBuilder = typeBuilder.DefineMethod(
"Greet",
MethodAttributes.Public,
typeof(string),
new[] { typeof(string) });
ILGenerator il = methodBuilder.GetILGenerator();
il.Emit(OpCodes.Ldstr, "Hello, "); // push "Hello, "
il.Emit(OpCodes.Ldarg_1); // push 'name' argument
il.Emit(OpCodes.Call, typeof(string).GetMethod("Concat",
new[] { typeof(string), typeof(string) })!);
il.Emit(OpCodes.Ret); // return
// 4. Create the type and use it
Type dynamicType = typeBuilder.CreateType()!;
object instance = Activator.CreateInstance(dynamicType)!;
MethodInfo greet = dynamicType.GetMethod("Greet")!;
Console.WriteLine(greet.Invoke(instance, new object[] { "World" })); // Hello, World- Dynamic proxies: Mocking frameworks (Moq, Castle.DynamicProxy) generate proxy classes.
- ORM materializers: Entity Framework's older versions emit IL to map database rows to objects.
- Serialization: Custom binary serializers emit IL for maximum throughput.
Expression.Compile()is sufficient (simpler API, same performance for most cases).- Source generators can do the work at compile time.
- You target NativeAOT (
Reflection.Emitis not supported in AOT).
A: Use it for:
Avoid it when:
Performance Implications and Benchmarks
| Operation | Approximate Time | Relative |
|---|---|---|
| Direct method call | ~1 ns | 1x |
| Compiled expression delegate | ~2 ns | ~2x |
Cached delegate via CreateDelegate | ~2 ns | ~2x |
MethodInfo.Invoke (cached MethodInfo) | ~100-200 ns | ~100-200x |
Activator.CreateInstance | ~50-150 ns | ~50-150x |
ConstructorInfo.Invoke | ~100-200 ns | ~100-200x |
Type.GetMethod (uncached, per call) | ~500-1000 ns | ~500-1000x |
A: A direct method call is ~1 ns. MethodInfo.Invoke with a cached MethodInfo is ~100-200x slower. The lookup itself (GetMethod) adds ~500-1000 ns per call. Here are approximate BenchmarkDotNet results:
A: Cache Type, MethodInfo, and PropertyInfo objects. For hot paths, compile expression trees or use CreateDelegate. The decision hierarchy is: compile-time generation > compiled delegates > cached reflection > uncached reflection.
// BAD: Reflecting on every call
public object GetPropertyValue(object obj, string name)
{
// GetProperty + GetValue on every call
return obj.GetType().GetProperty(name)?.GetValue(obj)!;
}
// GOOD: Reflect once, cache the accessor
public class PropertyAccessor<TObj, TProp>
{
private static readonly ConcurrentDictionary<string, Func<TObj, TProp>> _cache = new();
public static TProp Get(TObj obj, string propertyName)
{
var getter = _cache.GetOrAdd(propertyName, name =>
{
var param = Expression.Parameter(typeof(TObj), "obj");
var prop = Expression.Property(param, name);
return Expression.Lambda<Func<TObj, TProp>>(prop, param).Compile();
});
return getter(obj);
}
}A:
Need to call a member dynamically?
+-- Can you do it at compile time?
| +-- YES: Use source generators or generics/interfaces
+-- Is it a hot path (called >1000x/sec)?
| +-- YES: Compile an expression or use CreateDelegate; cache the delegate
| +-- NO: Cached MethodInfo.Invoke is acceptable
+-- Is it one-time startup logic?
+-- YES: Raw reflection is fine (assembly scanning, DI registration)A: Use generic static classes for per-type caching (the CLR guarantees thread-safe static initialization) or ConcurrentDictionary for string-keyed lookups:
// Approach 1: Generic static class — lazily initialized once per T
public static class TypeMetadata<T>
{
public static readonly PropertyInfo[] Properties =
typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
public static readonly Func<T> Factory = BuildFactory();
private static Func<T> BuildFactory()
{
var ctor = typeof(T).GetConstructor(Type.EmptyTypes);
if (ctor == null) return () => throw new InvalidOperationException(
$"No parameterless ctor on {typeof(T).Name}");
var lambda = Expression.Lambda<Func<T>>(Expression.New(ctor));
return lambda.Compile();
}
}
// Usage — zero per-call overhead after first access
var props = TypeMetadata<Trade>.Properties;
var trade = TypeMetadata<Trade>.Factory();
// Approach 2: Startup warm-up cache for known types
public class ReflectionCache
{
private readonly IReadOnlyDictionary<Type, PropertyInfo[]> _propertyCache;
private readonly IReadOnlyDictionary<Type, Func<object>> _factories;
public ReflectionCache(IEnumerable<Type> typesToCache)
{
_propertyCache = typesToCache.ToDictionary(
t => t,
t => t.GetProperties(BindingFlags.Public | BindingFlags.Instance));
_factories = typesToCache
.Where(t => t.GetConstructor(Type.EmptyTypes) != null)
.ToDictionary(
t => t,
t =>
{
var ctor = t.GetConstructor(Type.EmptyTypes)!;
var expr = Expression.New(ctor);
return Expression.Lambda<Func<object>>(expr).Compile();
});
}
public PropertyInfo[] GetProperties(Type type) => _propertyCache[type];
public object CreateInstance(Type type) => _factories[type]();
}Delegate.CreateDelegate and how does it compare to MethodInfo.Invoke?A: CreateDelegate converts a MethodInfo into a strongly-typed delegate. Invoking the delegate is as fast as a direct method call because it bypasses reflection dispatch. MethodInfo.Invoke boxes value types, validates arguments at runtime, and uses late-binding dispatch, making it ~100x slower.
MethodInfo addMethod = typeof(Calculator).GetMethod("Add")!;
var addDelegate = (Func<Calculator, int, int, int>)
Delegate.CreateDelegate(typeof(Func<Calculator, int, int, int>), addMethod);
var calc = new Calculator();
int result = addDelegate(calc, 3, 5); // As fast as a direct callReal-World Use Cases (ORMs, DI Containers, Serializers)
A: ORMs inspect entity types to discover properties, match them to database columns, and set values. Production ORMs compile these accessors at startup for performance:
// Simplified EF-style materializer
public T Materialize<T>(IDataReader reader) where T : new()
{
var entity = new T();
var props = typeof(T).GetProperties()
.ToDictionary(p => p.Name, StringComparer.OrdinalIgnoreCase);
for (int i = 0; i < reader.FieldCount; i++)
{
string column = reader.GetName(i);
if (props.TryGetValue(column, out var prop) && !reader.IsDBNull(i))
{
prop.SetValue(entity, reader.GetValue(i));
}
}
return entity;
}
// Production ORMs compile these accessors at startup for performance.A: They scan assemblies for types implementing specific interfaces and register them:
// Simplified auto-registration: scan for IService implementations
public static IServiceCollection AutoRegister(
this IServiceCollection services, Assembly assembly)
{
var registrations = assembly.GetTypes()
.Where(t => t.IsClass && !t.IsAbstract)
.SelectMany(t => t.GetInterfaces(),
(impl, iface) => new { Interface = iface, Implementation = impl })
.Where(r => r.Interface.Name.EndsWith("Service"));
foreach (var reg in registrations)
services.AddScoped(reg.Interface, reg.Implementation);
return services;
}
// Usage in Startup
services.AutoRegister(Assembly.GetExecutingAssembly());A: They discover properties via reflection to build JSON or other formats dynamically:
// Simplified property discovery for serialization
public static string ToJson<T>(T obj)
{
var props = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
var pairs = props.Select(p =>
$"\"{p.Name}\": \"{p.GetValue(obj)}\"");
return "{ " + string.Join(", ", pairs) + " }";
}A: Define a shared interface in a common assembly, scan a directory for DLLs, load them, find implementing types, and instantiate:
public interface IPlugin
{
string Name { get; }
void Execute();
}
public class PluginHost
{
public List<IPlugin> LoadPlugins(string pluginDirectory)
{
var plugins = new List<IPlugin>();
foreach (var dll in Directory.GetFiles(pluginDirectory, "*.dll"))
{
var assembly = Assembly.LoadFrom(dll);
var pluginTypes = assembly.GetTypes()
.Where(t => typeof(IPlugin).IsAssignableFrom(t)
&& !t.IsAbstract
&& !t.IsInterface);
foreach (var type in pluginTypes)
{
if (Activator.CreateInstance(type) is IPlugin plugin)
plugins.Add(plugin);
}
}
return plugins;
}
}
For production, add version validation, dependency isolation via AssemblyLoadContext, and error handling for partial loads.
JsonSerializer in System.Text.Json leverage reflection?A: It inspects types at runtime to discover properties, check for [JsonPropertyName] attributes, determine serialization options, and build converters. In .NET 6+, source generators can precompute serializers to avoid this overhead entirely and enable trimming/AOT.
Security Considerations
A: Yes via BindingFlags.NonPublic, but it should be used sparingly. It creates fragile coupling to implementation details that can change without notice.
// This is always possible but should be treated as a code smell
FieldInfo? secretField = typeof(SecureService).GetField(
"_apiKey", BindingFlags.NonPublic | BindingFlags.Instance);
// Risk: internal implementation may change between versions.
// Risk: trimming may remove the field entirely.A: Validate assembly paths, restrict loaded types/namespaces, and never execute untrusted code without sandboxing:
// NEVER load assemblies from untrusted sources without validation
// Malicious assemblies can execute arbitrary code via static constructors
// Safer: Use MetadataLoadContext to inspect without executing
using var mlc = new MetadataLoadContext(resolver);
Assembly inspected = mlc.LoadFromAssemblyPath(untrustedPath);
// Types can be inspected but NOT instantiated or invokedA: The IL trimmer removes types and members it considers unreachable. Reflection-based access is invisible to the trimmer, so reflected members may be removed. Mitigate with annotations or source generators:
// Trimming removes metadata for unused types/members.
// Reflection over trimmed members fails silently (returns null) or throws.
// Annotate to preserve metadata:
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
public Type ConfigType { get; set; }
// Or in the project file:
// <TrimmerRootAssembly Include="MyApp.Plugins" />
// NativeAOT restrictions:
// - Reflection.Emit is NOT supported
// - Some reflection patterns require explicit rd.xml annotations
// - Source generators are the recommended alternative- Validate assembly paths and type names before loading.
- Restrict which namespaces/types can be loaded in plugin systems.
- Never expose reflection-based invocation to user input without strict allow-listing.
- Prefer
MetadataLoadContextwhen you only need to inspect, not execute. - Log all dynamic type loading for auditability.
A:
Comprehensive Q&A Summary
A: Reflection is the ability to inspect and interact with type metadata at runtime via System.Reflection. Use it for scenarios where compile-time type knowledge is insufficient: plugin discovery, attribute-driven frameworks, serialization, DI container auto-registration, and tooling like test runners or ORM materializers.
typeof, GetType(), and Type.GetType()?A: typeof(T) is a compile-time operator that returns the Type for a known type. obj.GetType() returns the runtime type of an instance (always the concrete type, not the declared type). Type.GetType("name") resolves a type from a string, requiring an assembly-qualified name for types outside the calling assembly or mscorlib.
A: They scan assemblies for types implementing specific interfaces, inspect constructors to determine dependencies (ConstructorInfo.GetParameters()), resolve each parameter recursively, and invoke the constructor. Production containers cache constructor delegates after first resolution to avoid repeated reflection.
A: Source generators run during compilation and emit C# source code. Instead of discovering types/properties at runtime, the generated code contains explicit, strongly-typed logic. System.Text.Json source gen, for example, generates serializers that are faster, trimming-safe, and AOT-compatible.
A: Define a shared interface (e.g., IPlugin) in a common assembly. At startup, scan a plugins directory for .dll files, load each via Assembly.LoadFrom, find types implementing IPlugin, and instantiate them with Activator.CreateInstance. For production, add version validation, dependency isolation via AssemblyLoadContext, and error handling for partial loads.
A: Write unit tests for discovery logic (ensuring correct types are found) and integration tests that verify attributes/config drive expected behavior. Test trimming scenarios by publishing a trimmed build and running a smoke test. Mock metadata where possible by abstracting the reflection layer behind an interface.