Let's talk about reflection. If you're new to it, reflection is just code that looks at other code at runtime — it can inspect types, read attributes, and even create new types on the fly. It's super handy for plugins, testing, and framework work. That said, it can be slow or risky if misused, so we'll keep things practical and show safer patterns as we go.
Understanding Reflection and Metadata
Metadata is simply the information the compiler stores in an assembly about types, members, and attributes. Using reflection you can read that info at runtime, which opens up useful patterns such as discovering methods, properties and fields of a type, inspecting custom attributes to guide behavior, and invoking members or accessing fields dynamically when you need to adapt at runtime.
// Inspecting a class and its members
using System;
using System.Reflection;
public class Person
{
private string name;
public int Age { get; set; }
public Person(string name, int age)
{
this.name = name;
Age = age;
}
public void Greet() => Console.WriteLine($"Hello, my name is {name}.");
}
// Obtain the type
Type personType = typeof(Person);
// List all public instance methods
foreach (var method in personType.GetMethods(BindingFlags.Public | BindingFlags.Instance))
{
Console.WriteLine($"Method: {method.Name}");
}
// List all private instance fields
foreach (var field in personType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance))
{
Console.WriteLine($"Private Field: {field.Name}");
}
Quick note: GetMethods
returns public methods (including inherited ones). Using
BindingFlags.NonPublic
with GetFields
lets you see private fields. These calls are the
building blocks — once you can get MethodInfo
or FieldInfo
, you can invoke or read them.
Accessing Private Members Safely
Sometimes you'll need to peek at private fields (testing helpers or serializers are common cases). Reflection lets
you do that with GetValue
and SetValue
. Use this sparingly — it's like picking a lock:
handy when necessary, but it breaks encapsulation if overused.
var person = new Person("Alice", 30);
FieldInfo nameField = typeof(Person).GetField("name", BindingFlags.NonPublic | BindingFlags.Instance);
// Read private field
string nameValue = (string)nameField.GetValue(person);
Console.WriteLine($"Private name: {nameValue}"); // Output: Alice
// Modify private field
nameField.SetValue(person, "Bob");
person.Greet(); // Output: Hello, my name is Bob.
Reflection bypasses access modifiers, so use it sparingly. Always validate input when modifying private fields to avoid breaking encapsulation or introducing inconsistent state.
Dynamic Type Creation
Sometimes you actually want new types at runtime — for plugin systems, proxies, or generated DTOs. That’s where
System.Reflection.Emit
comes in: it lets you build assemblies, types, fields, constructors and
methods programmatically. It’s powerful, but also low-level — prefer higher-level approaches when possible.
using System;
using System.Reflection;
using System.Reflection.Emit;
// Define dynamic assembly and module
AssemblyName assemblyName = new AssemblyName("DynamicTypes");
AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule");
// Create a dynamic class with a private field
TypeBuilder typeBuilder = moduleBuilder.DefineType("DynamicPerson", TypeAttributes.Public);
FieldBuilder nameField = typeBuilder.DefineField("_name", typeof(string), FieldAttributes.Private);
// Define a constructor that initializes the field
ConstructorBuilder ctor = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, new Type[] { typeof(string) });
ILGenerator ctorIL = ctor.GetILGenerator();
ctorIL.Emit(OpCodes.Ldarg_0); // this
ctorIL.Emit(OpCodes.Call, typeof(object).GetConstructor(Type.EmptyTypes)); // base()
ctorIL.Emit(OpCodes.Ldarg_0);
ctorIL.Emit(OpCodes.Ldarg_1);
ctorIL.Emit(OpCodes.Stfld, nameField); // _name = arg
ctorIL.Emit(OpCodes.Ret);
// Define a public method
MethodBuilder greetMethod = typeBuilder.DefineMethod("Greet", MethodAttributes.Public, null, Type.EmptyTypes);
ILGenerator greetIL = greetMethod.GetILGenerator();
greetIL.Emit(OpCodes.Ldstr, "Hello from dynamic type!");
greetIL.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) }));
greetIL.Emit(OpCodes.Ret);
// Create the type
Type dynamicPersonType = typeBuilder.CreateType();
object dynamicPerson = Activator.CreateInstance(dynamicPersonType, new object[] { "Charlie" });
dynamicPersonType.GetMethod("Greet").Invoke(dynamicPerson, null);
In plain terms: the example above builds a new type, adds a private field and a constructor that sets it, and emits a method — all at runtime. It's a great tool for frameworks, but it's advanced. If you can solve a problem with expression trees or code generation ahead of time, prefer those first.
Adding Properties and Methods Dynamically
You can also add properties and full methods to dynamic types — so they can behave like normal classes. This is how some ORMs, serializers, and proxy libraries create efficient types tailored to your runtime scenario.
// Add a property dynamically
PropertyBuilder ageProperty = typeBuilder.DefineProperty("Age", PropertyAttributes.HasDefault, typeof(int), null);
// Create getter method
MethodBuilder getAge = typeBuilder.DefineMethod("get_Age", MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, typeof(int), Type.EmptyTypes);
ILGenerator getIL = getAge.GetILGenerator();
getIL.Emit(OpCodes.Ldarg_0);
getIL.Emit(OpCodes.Ldc_I4, 25);
getIL.Emit(OpCodes.Ret);
ageProperty.SetGetMethod(getAge);
With this property, the dynamic type now has a getter for Age
. Advanced scenarios can define setters,
events, and even implement interfaces dynamically.
Runtime Code Generation with Expression Trees
Reflection is flexible but can be slow if you call it a lot. Expression trees let you build code as data and then compile it into a delegate — so you get openness and speed. Use expression trees for hot paths where repeated invocation matters.
using System;
using System.Linq.Expressions;
// Define parameters
ParameterExpression a = Expression.Parameter(typeof(int), "a");
ParameterExpression b = Expression.Parameter(typeof(int), "b");
// Build addition expression
BinaryExpression body = Expression.Add(a, b);
// Compile expression into a delegate
var addFunc = Expression.Lambda>(body, a, b).Compile();
int result = addFunc(5, 7);
Console.WriteLine($"5 + 7 = {result}"); // Output: 12
In practice, expression trees are great for mapping frameworks, dynamic queries, or any place you need a fast function created at runtime. They are much friendlier than writing IL by hand.
Caching Reflection Metadata
Tiny tip: reflection calls are not free. If your app inspects the same types repeatedly, cache the
PropertyInfo
/MethodInfo
objects so you're not doing the lookup over and over.
// Cache property info
var propertiesCache = new Dictionary();
Type type = typeof(Person);
if (!propertiesCache.ContainsKey(type))
{
propertiesCache[type] = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
}
// Access cached properties
foreach (var prop in propertiesCache[type])
{
Console.WriteLine(prop.Name);
}
Advanced Use Cases
Some common advanced scenarios include dynamic proxies that intercept method calls for logging or validation, plugin systems that load assemblies and instantiate types at runtime, serialization frameworks that generate optimized serializers dynamically, and ORMs that build queries based on metadata. These are the kinds of places where reflection and runtime code generation can save a lot of boilerplate.
Debugging dynamically generated code requires special techniques. For example, you can add a
DebuggableAttribute
to enable breakpoints in generated assemblies, write unit tests that exercise
the dynamic behavior so you can catch regressions, and log generated IL when troubleshooting complicated cases.
For very complex delegates consider using Expression.CompileDebug
or adding intermediate logging to
make the behavior observable while you iterate.
Reflection and runtime code generation can introduce security vulnerabilities. Make sure you restrict reflection to trusted contexts, validate inputs that are used to generate IL or expression trees, limit execution permissions using sandboxing techniques where appropriate, and never expose sensitive private members to untrusted code.
Best Practices
In practice, cache reflection results to avoid repeated lookups, prefer expression trees or compiled delegates for high-frequency calls, validate and test generated code in multiple stages, and document what gets generated so the runtime behavior remains maintainable.
Summary
To wrap up: reflection lets you inspect metadata, access private members when needed, create dynamic types, and generate fast runtime code — but use it carefully. Key takeaways:
To recap: reflection gives you runtime access to types, methods, properties and fields. You can safely access
private members for test or serialization scenarios, create dynamic types with
System.Reflection.Emit
,
and use expression trees to generate fast dynamic methods. Caching and good debugging practices help with
performance and maintenance, and following security best practices prevents misuse of dynamic capabilities.
Mastering these techniques allows building runtime-extensible frameworks, dynamic applications, and high-performance systems while maintaining safety, readability, and maintainability.