Reflection becomes useful the moment you treat a Type as something you can walk, query, and call into. This lesson is about the practical mechanics: listing the members of a type, reading and writing property and field values dynamically, invoking methods on an instance or on the type itself, constructing objects when you only have a Type handle, and doing all of this for generics. We'll put the pieces together and build a small serializer that walks a Product and prints Name = Value lines without knowing anything about it at compile time.
Every Type exposes a family of Get* methods that return the members declared on it (and, depending on the flags, the ones it inherits). They all return MemberInfo subclasses: MethodInfo, PropertyInfo, FieldInfo, ConstructorInfo, EventInfo, plus nested types as Type themselves.
The GetMethods() call without DeclaredOnly would also list every public method inherited from object: ToString, Equals, GetHashCode, GetType. The DeclaredOnly flag trims the result to members declared on Product itself. The GetConstructors() call returned both .ctors, including the parameterless one the C# compiler keeps because we wrote it explicitly.
GetMembers() returns everything at once: properties, methods, fields, constructors, events, nested types. It's convenient for introspection tools that want a single iteration, but most code reaches for the typed variant (GetProperties, GetMethods, etc.) so the result is already strongly typed.
The same BindingFlags you saw in the _Reflection Basics_ lesson decide what comes back. BindingFlags.NonPublic | BindingFlags.Instance exposes private fields. BindingFlags.Static switches to static members. Without any flags, the methods default to Public | Instance, which is usually what application code wants.
Each GetMembers/GetProperties call walks the type's metadata tables and allocates a fresh array. The cost is small in absolute terms but real in a loop. Look the metadata up once, cache the MemberInfo[] (or the specific PropertyInfo needed), and reuse it for every subsequent operation.
PropertyInfo and FieldInfo both expose GetValue(object? instance) and SetValue(object? instance, object? value). The first argument is the object you're reading from or writing to; for static members, pass null. The value goes in and comes back as object?, so value types get boxed at the boundary.
The two SetValue calls landed in the same memory as the product.Name and product.Price accesses. Reflection doesn't go through a copy; it routes through the actual setter or the actual field slot, exactly as the compiler would have for a direct call.
FieldInfo works the same way. Reading a private field is one of the few legitimate uses of reflection in production code, usually for serialization or test instrumentation:
Read-only properties (those with no setter) reject SetValue with a ArgumentException. Init-only properties (the init accessor) also reject SetValue from outside the construction path, which is the runtime preserving the language guarantee. If you need to set an init-only property by reflection, you have to invoke it via the backing field instead.
A value-type write through reflection boxes the value before the call and the runtime unboxes it inside the setter. SetValue(obj, 42) allocates a small object on the heap for the int. Setting thousands of integers in a tight loop this way causes GC pressure. Direct obj.Number = 42 allocates nothing.
MethodInfo.Invoke(object? instance, object?[]? args) runs a method through reflection. The arguments come in as an object[] and the return value comes out as an object?. For instance methods, pass the target instance; for static methods, pass null.
Invoke returns object? for every method, including ones declared with a primitive return type. The caller has to cast back. For void methods, Invoke returns null.
Methods with out and ref parameters work too, with one twist: the object[] array is used for both input and output. Reflection writes the final value of each out/ref parameter back into the same slot of the array, so the caller reads it from there after the call returns.
The args[1] slot started as 0m (a boxed placeholder) and came back holding the boxed result of the out write. Reflection's calling convention treats the array as a bidirectional buffer, so any out-parameter ends up in the same index the caller reserved.
MethodInfo.Invoke is roughly 100x slower than a direct method call in typical microbenchmarks: every argument is boxed, the runtime checks types and accessibility, and the call goes through an indirection that the JIT can't easily inline. For one call per request it doesn't matter; for a million calls per second it does.
Calling a method by reflection is more than just instance.Method(args). The runtime threads each call through several stages, and understanding the stages explains both the cost and the failure modes.
The lookup stage is the one most often repeated needlessly. Every GetMethod("Foo") call walks the metadata tables, applies the binding flags, and allocates a MethodInfo. Caching that MethodInfo once and reusing it cuts the cost of subsequent calls roughly in half. The remaining cost lives in stages D through F: boxing each argument, validating that the supplied types match the parameters, and going through the runtime's invocation stub instead of a direct call site.
The return-type branch is where boxing shows up again. Any value-type return is wrapped in an object before it leaves Invoke. For a decimal return, that's a fresh heap allocation on every call. For a string or any reference type, it's a plain reference and no allocation.
When you have a Type but not an instance, Activator.CreateInstance is the simplest way to get one. The parameterless overload calls the public parameterless constructor:
Activator.CreateInstance(type, args) picks a constructor whose parameter list matches the arguments, then calls it. The runtime does the matching by type, with limited conversion: passing 42 to a constructor expecting long works because of widening, passing "42" does not because the runtime won't parse strings.
For full control, look up the ConstructorInfo directly and call Invoke on it. This bypasses the matching logic, which is faster and more predictable:
GetConstructor(Type[]) takes an array of parameter types and returns exactly the matching constructor (or null if none matches). Cache the ConstructorInfo once when the lookup is on a known type, and the per-instance cost drops to one Invoke and one allocation.
Activator.CreateInstance(type) for a public parameterless constructor is well-optimised by the runtime and is close to the cost of new T(). The args overload (CreateInstance(type, new object[]{ ... })) is roughly an order of magnitude slower because it has to box the arguments, search for a constructor, and validate the call. For repeated construction, cache the ConstructorInfo and call Invoke, or compile a delegate (mentioned at the end of this lesson).
Generic types in reflection have two flavours: the open generic (List<>, with the parameter unbound) and the closed generic (List<string>, with the parameter resolved). typeof(List<>) gives you the open form. typeof(List<string>) gives you the closed form. The two carry different metadata and answer IsGenericType and IsGenericTypeDefinition differently.
MakeGenericType does the opposite of GetGenericTypeDefinition: it takes an open generic plus a set of arguments and returns the closed form. This is how you build a List<Product> when you only know Product at runtime.
The Add method on a closed generic type already has the correct parameter type baked into its MethodInfo. We didn't have to call MakeGenericMethod on it; the method became closed automatically the moment its declaring type closed.
Generic methods are a different case. A method like static T First<T>(T[] items) => items[0]; has its own type parameter independent of the declaring type. To call it via reflection, you have to close the method itself with MakeGenericMethod:
openMethod is the open generic First<T>; calling Invoke on it would throw InvalidOperationException because the runtime needs a closed method to dispatch. closedMethod is First<decimal>, fully bound and ready to call.
Each MakeGenericType or MakeGenericMethod call allocates a fresh Type (or MethodInfo) object the first time a particular combination of arguments is seen, and the runtime caches the result for subsequent lookups. The first call is the expensive one; later calls with the same arguments are fast. Cache the closed type or method explicitly if the same combination shows up repeatedly.
Bringing the pieces together. The goal is a tiny function that takes any object, walks its public properties, and prints Name = Value lines. No knowledge of the concrete type is baked in; the same function handles a Product, an Order, or a Customer.
The function uses three pieces from this lesson: Type discovery via obj.GetType(), member enumeration via GetProperties(BindingFlags...), and dynamic reads via PropertyInfo.GetValue. Every property is treated uniformly; the function never mentions Product or Order by name.
A real serializer would do more: handle nested objects recursively, escape strings, format dates, skip properties with a [JsonIgnore]-style attribute. Each of those is one more piece of metadata to consult, and the structure stays the same. System.Text.Json uses this exact pattern internally before it hands the work off to a generated converter for hot paths.
The version above does one wasteful thing on every call: it re-runs GetProperties for every object. For n products, that's n metadata lookups and n array allocations. Caching the PropertyInfo[] per type cuts both:
The first Dump(new Customer ...) pays the metadata cost. Every subsequent call for a Customer reads from the cache and skips the GetProperties work entirely. The per-property GetValue cost is still there, but for many workloads (logging, diagnostics, tracing) that's already fast enough.
Caching PropertyInfo objects can drop the cost of reflection-heavy code by 2-3x compared to looking them up on every call. The remaining cost is the Invoke/GetValue machinery itself. To eliminate that, compile a delegate that calls the property's getter directly, covered briefly below.
The single biggest speedup over MethodInfo.Invoke and PropertyInfo.GetValue is to compile a delegate once and call it many times. Three approaches are common: Delegate.CreateDelegate (works for methods whose signature you know in advance), System.Linq.Expressions building an expression tree and calling .Compile() on it, and the lower-level System.Reflection.Emit.DynamicMethod for full control.
The pattern looks like this with Expression.Compile:
The Compile() call produces a real delegate that calls the property's getter directly. After the one-time compile cost, calling getName(product) is approximately as fast as product.Name. The trade-off is the build cost (which is real, milliseconds for a single delegate) and the fact that the compiled code lives in dynamically-emitted IL, which doesn't work on AOT-compiled environments like Native AOT.
For AOT-friendly speed without runtime IL emission, the modern answer is source generators. A source generator inspects your types at build time and emits the same kind of property-walking code you'd write by hand, with no reflection at runtime. System.Text.Json ships a source-generator mode for exactly this reason, and so do many newer libraries that used to rely on Expression.Compile.
For most application code, plain reflection with cached PropertyInfo[] is fine. Use Expression.Compile when reflection shows up at the top of a profiler. Use source generators when AOT is required or when build-time generation is preferable for other reasons.
Reflection is useful when you genuinely don't know the shape of the data until runtime: a serializer that accepts any class, a dependency injection container that resolves constructors, a test framework that finds [Test]-attributed methods, an ORM that builds queries from entity properties. Each of those is a library that has to work with types it didn't write.
It's the wrong tool when you do know the types at compile time. Reaching for GetProperty("Name") instead of writing product.Name adds string-based fragility (rename Name and the reflection call breaks at runtime instead of at compile time), it adds runtime cost, and it makes the code harder to follow.
Two modern alternatives reduce the need for reflection in many scenarios:
obj.Name syntactically while deferring the lookup to runtime. The DLR caches the call site, so repeated identical calls are fast. The trade-off is that the compiler can't help you catch typos.For the foreseeable future, reflection is still the most flexible and the easiest to get started with. The other two are optimisations you reach for when reflection shows up as a real cost.
10 quizzes