CSharpStory_Reflection.html
copyright © James Fawcett
Revised: 04/26/2026
8.0 Prologue
Reflection is the ability of a program to inspect and manipulate its
own structure at runtime — discovering types, reading metadata, invoking methods by name,
and even creating new types on the fly. The System.Reflection namespace
provides the full API. Reflection powers serializers, dependency-injection containers,
ORMs, test frameworks, and many other infrastructure libraries.
8.1 The Type Object
System.Type is the entry point for all reflection. Three ways to obtain one:
// 1. From a compile-time type name
Type t1 = typeof(List<int>);
// 2. From an instance at runtime
object obj = new Dictionary<string, int>();
Type t2 = obj.GetType();
// 3. By name (late-bound; may return null)
Type? t3 = Type.GetType("System.Collections.Generic.List`1");
Useful Type properties:
- Name, FullName, Namespace, AssemblyQualifiedName
- IsClass, IsInterface, IsValueType, IsEnum, IsGenericType
- BaseType — the direct base class
- GetInterfaces() — all implemented interfaces
- IsAssignableTo(other) / IsAssignableFrom(other)
8.2 Inspecting Members
Type exposes methods to enumerate every member. Pass
BindingFlags to control which members are returned:
Type t = typeof(string);
BindingFlags pub = BindingFlags.Public | BindingFlags.Instance;
// Methods
foreach (MethodInfo m in t.GetMethods(pub))
Console.WriteLine($" {m.ReturnType.Name} {m.Name}");
// Properties
foreach (PropertyInfo p in t.GetProperties(pub))
Console.WriteLine($" {p.PropertyType.Name} {p.Name}");
// Fields (usually private; add NonPublic flag)
BindingFlags priv = pub | BindingFlags.NonPublic;
foreach (FieldInfo f in t.GetFields(priv))
Console.WriteLine($" {f.FieldType.Name} {f.Name}");
// Constructors
foreach (ConstructorInfo c in t.GetConstructors(pub))
Console.WriteLine($" .ctor({string.Join(", ", c.GetParameters().Select(p => p.ParameterType.Name))})");
BindingFlags.DeclaredOnly restricts results to members declared on the type
itself (not inherited). BindingFlags.Static includes static members.
8.3 Assembly Inspection
An Assembly object represents a loaded .dll or .exe. You can enumerate
all types it defines, load it from a file path, or load it by name:
// Assembly containing a known type
Assembly asm = typeof(string).Assembly;
Console.WriteLine(asm.FullName);
// All public types in the assembly
foreach (Type t in asm.GetExportedTypes())
Console.WriteLine(t.FullName);
// Load from file path
Assembly plugin = Assembly.LoadFrom("MyPlugin.dll");
Type? entry = plugin.GetType("MyPlugin.EntryPoint");
Assemblies loaded with Assembly.LoadFrom cannot be unloaded individually
in the default context. Use AssemblyLoadContext (collectible) if you need
plugin hot-reload or unloading.
8.4 Dynamic Creation and Invocation
Activator.CreateInstance constructs an object when the type is only known
at runtime. MethodInfo.Invoke calls a method by its reflected descriptor:
// Create instance of a type known only at runtime
Type t = Type.GetType("System.Text.StringBuilder")!;
object? sb = Activator.CreateInstance(t, "hello"); // calls ctor(string)
// Invoke a method by name
MethodInfo? append = t.GetMethod("Append", new[] { typeof(string) });
append?.Invoke(sb, new object[] { " world" });
// Read a property value
PropertyInfo? length = t.GetProperty("Length");
int len = (int)length?.GetValue(sb)!;
Console.WriteLine(len); // 11
// Generic method: must supply type arguments
MethodInfo? parse = typeof(int).GetMethod("Parse", new[] { typeof(string) });
int n = (int)parse!.Invoke(null, new object[] { "42" })!;
For generic methods, call MakeGenericMethod(typeArgs) before invoking:
MethodInfo? max = typeof(Enumerable)
.GetMethods()
.First(m => m.Name == "Max" && m.GetParameters().Length == 1);
MethodInfo maxInt = max.MakeGenericMethod(typeof(int));
int result = (int)maxInt.Invoke(null, new object[] { new[] { 3, 1, 4, 1, 5 } })!;
8.5 Reading Attributes at Runtime
Attributes are metadata attached to types, methods, properties, or parameters. Reflection
reads them with GetCustomAttributes or the generic
GetCustomAttribute<T>:
[AttributeUsage(AttributeTargets.Class)]
public sealed class VersionAttribute : Attribute {
public int Major { get; }
public int Minor { get; }
public VersionAttribute(int major, int minor) { Major = major; Minor = minor; }
}
[Version(2, 1)]
public class MyService { }
// Reading at runtime
Type t = typeof(MyService);
var ver = t.GetCustomAttribute<VersionAttribute>();
if (ver is not null)
Console.WriteLine($"v{ver.Major}.{ver.Minor}"); // v2.1
// Enumerate all attributes on a method
MethodInfo? m = t.GetMethod("Run");
foreach (Attribute a in m?.GetCustomAttributes() ?? [])
Console.WriteLine(a.GetType().Name);
8.6 Reflection.Emit — Generating Code at Runtime
System.Reflection.Emit lets you generate IL code and create new types
at runtime. This is how expression-tree compilers, mock-object generators, and
some serializers achieve near-native performance from dynamically generated code:
AssemblyBuilder ab = AssemblyBuilder.DefineDynamicAssembly(
new AssemblyName("DynamicAsm"), AssemblyBuilderAccess.Run);
ModuleBuilder mb = ab.DefineDynamicModule("DynamicMod");
TypeBuilder tb = mb.DefineType("Greeter", TypeAttributes.Public);
MethodBuilder method = tb.DefineMethod(
"Hello", MethodAttributes.Public | MethodAttributes.Static,
typeof(string), Type.EmptyTypes);
ILGenerator il = method.GetILGenerator();
il.Emit(OpCodes.Ldstr, "Hello from emitted code!");
il.Emit(OpCodes.Ret);
Type finished = tb.CreateType()!;
string msg = (string)finished.GetMethod("Hello")!.Invoke(null, null)!;
Console.WriteLine(msg);
For most use cases, source generators (Roslyn, C# 9+) and
compiled expression trees (Expression.Lambda(...).Compile())
are simpler and more maintainable alternatives to raw Emit.
8.7 Performance and Alternatives
Reflection carries runtime costs:
- Type lookup — cheap if cached; expensive if repeated via Type.GetType(string)
- Member lookup (GetMethod, GetProperty) — moderate; cache the MethodInfo
- Invoke — significant boxing/unboxing overhead per call for value types
Mitigation strategies:
- Cache Type, MethodInfo, PropertyInfo objects in static fields
- Compile a delegate from a MethodInfo via CreateDelegate for hot paths
- Use Expression<Func<T,TResult>> trees compiled to delegates
- Use Roslyn source generators to move reflection work to compile time
- Enable System.Text.Json source generation for AOT-safe serialization
// Convert MethodInfo to a direct delegate — called at native speed
MethodInfo? mi = typeof(string).GetMethod("IsNullOrEmpty");
var isNullOrEmpty = (Func<string?, bool>)
Delegate.CreateDelegate(typeof(Func<string?, bool>), mi!);
bool result = isNullOrEmpty(null); // fast — no reflection overhead per call
8.8 Epilogue
This chapter covered the System.Reflection API: obtaining Type
objects, inspecting members and assemblies, creating instances dynamically, reading
attributes, generating IL with Emit, and managing performance through caching and
compiled delegates. The next chapter surveys the .NET Base Class Library.
8.9 References
Reflection and attributes — Microsoft docs
System.Reflection — API docs
System.Reflection.Emit — API docs
AssemblyLoadContext — Microsoft docs