C# Story

Chapter #8 – Reflection

runtime type inspection, dynamic invocation, attributes, emit

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