← Open in the full interactive course (progress tracking, search & more)

Every dependency injection container, every JSON serializer, every ORM, every test runner you've used has been reading your code's own metadata back to itself, at runtime, this whole time.

Think about what actually has to happen for System.Text.Json to turn your Customer object into {"name": "Alice", "balance": 100}. Nobody wrote a line of code that says "read the Name property, then the Balance property." The serializer has never seen your Customer class before — you wrote it, it didn't. And yet it correctly discovers every public property, reads its name, reads its value, and writes it out. Or think about ASP.NET Core's dependency injection container: you register services.AddScoped<IOrderService, OrderService>(), and somehow, without you writing a single line of "new up an OrderService with these three constructor arguments," it just works — for a constructor the container has never seen before either.

Both of these are the same underlying capability: code that inspects other code's structure — its types, methods, properties — at runtime, and acts on what it finds. That capability is called reflection, and once you understand it, an enormous amount of "how does that even work" in the .NET ecosystem stops being magic.

In this lesson: what reflection actually is, the core types (Type, Assembly, MethodInfo, PropertyInfo), invoking a method dynamically, the real-world tools you've already benefited from without knowing it, and the real, measurable performance cost that motivates the next lesson.

What Is It?

The Simple Explanation

Normally, when you write customer.Withdraw(50), you know at compile time exactly which method you're calling — the compiler checked it, and the call is baked directly into the compiled code. Reflection is the opposite: it lets code ask, at runtime, "what methods does this object's type actually have?" — discovering the answer as data (a name, a list of parameters) rather than knowing it up front — and then call whichever method it found, by that discovered information, instead of by writing the method's name directly in source code.

The Technical Definition

Reflection is the ability of a .NET program to inspect its own (or another assembly's) metadata — type definitions, members, attributes — at runtime, and to dynamically create instances, get/set field and property values, and invoke methods based on that inspected information, rather than through code the compiler resolved at compile time. It's built on the fact that every compiled .NET assembly carries rich metadata describing every type and member it contains, alongside the actual executable code — the CLR (and, transitively, your program) can always read that metadata back.

Ordinary code

Reflection

Why Does It Exist?

The Problem

Some genuinely useful tools have to work with types they've never seen before, written by someone else, compiled after the tool itself. A JSON library ships as a compiled package before your Customer class even exists — it can't have a hand-written if (obj is Customer) { ... } branch for a type it doesn't know about. A dependency injection container needs to construct arbitrary services registered by application code it has no advance knowledge of. Without some way to discover a type's shape at runtime, none of that generality is possible — every such library would need the application to hand-write serialization/construction code for every single type, by hand, forever.

The Need

Developers needed a way for code to ask a type about itself — "what properties do you have, what's their name, what's their type, what constructors are available" — and then act on those answers generically, without the calling code needing to know the specific type in advance.

The Solution

The System.Reflection namespace, built directly on top of the metadata every .NET assembly already carries. Any type, any assembly, any method — known at compile time or not — can be inspected and used through a uniform set of reflection types, which is exactly what makes generic, type-agnostic infrastructure code (serializers, DI containers, ORMs, test runners) possible at all.

Big Picture

THE REFLECTION TYPE FAMILY
Assembly — a loaded .dll/.exe and everything defined in it
↓ contains many
Type — the runtime representation of a class/struct/interface/etc.
↓ has many
MethodInfo / PropertyInfo / FieldInfo / ConstructorInfo — individual members, discoverable and invokable

How It Works

DISCOVER, THEN INVOKE — STEP BY STEP
1. GET A Type OBJECT — THE ENTRY POINT INTO REFLECTION
Type type = typeof(Customer);           // compile-time known — the common case
// or, at runtime, from an instance you already have:
Customer c = new Customer();
Type sameType = c.GetType();
// or by name — the truly dynamic case, e.g. loaded from config:
Type? byName = Type.GetType("MyApp.Customer");
2. DISCOVER MEMBERS
PropertyInfo[] properties = type.GetProperties();
MethodInfo? withdraw = type.GetMethod("Withdraw");
ConstructorInfo[] ctors = type.GetConstructors();
3. CREATE AN INSTANCE, IF YOU DON'T ALREADY HAVE ONE
object? instance = Activator.CreateInstance(type); // calls a matching constructor dynamically
4. READ, WRITE, OR INVOKE — USING THE DISCOVERED MEMBER
PropertyInfo? balanceProp = type.GetProperty("Balance");
object? currentBalance = balanceProp?.GetValue(instance);   // reads the property
balanceProp?.SetValue(instance, 500m);                      // writes the property

withdraw?.Invoke(instance, [50m]);                           // calls the method dynamically

Simple Example

public class Customer
{
    public string Name { get; set; } = "";
    public decimal Balance { get; set; }
    public void Withdraw(decimal amount) => Balance -= amount;
}

var customer = new Customer { Name = "Alice", Balance = 100m };
Type type = customer.GetType();

// Discover and print every public property, generically — works for ANY type, not just Customer
foreach (PropertyInfo prop in type.GetProperties())
{
    object? value = prop.GetValue(customer);
    Console.WriteLine($"{prop.Name} = {value}");
}
// Name = Alice
// Balance = 100

// Discover and call a method by name, with an argument
MethodInfo? withdraw = type.GetMethod("Withdraw");
withdraw?.Invoke(customer, [30m]);
Console.WriteLine(customer.Balance); // 70

Meaning: None of this code mentions Name, Balance, or Withdraw as compile-time-known identifiers — the property-printing loop, in particular, would print the exact same way for a completely different class with completely different properties, unchanged. That's the entire point: this code is generic over any type, discovered at runtime.

Real-World Example

You've used at least four tools built directly on reflection, likely without ever opening their source:

DI containers

JSON serializers

ORMs

Test frameworks

A small illustration of the DI-container pattern, built from the same pieces you just used above:

public class SimpleContainer
{
    private readonly Dictionary<Type, object> _instances = [];
    public void Register<TService>(TService instance) => _instances[typeof(TService)] = instance!;

    public object CreateWithDependencies(Type implementationType)
    {
        ConstructorInfo ctor = implementationType.GetConstructors()[0]; // (simplified — real containers pick more carefully)
        object?[] args = ctor.GetParameters()
            .Select(p => _instances[p.ParameterType])
            .ToArray();
        return ctor.Invoke(args); // constructs the object, supplying discovered dependencies automatically
    }
}

// Register the pieces OrderService needs, without OrderService's constructor being hard-coded anywhere here
var container = new SimpleContainer();
container.Register<ILogger>(new ConsoleLogger());
var orderService = (OrderService)container.CreateWithDependencies(typeof(OrderService));

This is a deliberately simplified sketch, but it's the genuine shape of what a real DI container does: reflect over a constructor's parameters, resolve each one, and invoke the constructor — for any type you register, without the container ever being written against that specific type.

Analogy

Reading the box instead of knowing the product

Ordinary code is like using a tool you already know by heart — you reach for your screwdriver by name, because you know exactly what it is and how to hold it. Reflection is like being handed a sealed box labeled only "some kind of tool," with no advance knowledge of what's inside, and a set of instructions for examining any box: "read the label to find out what it is, check what it can do, then use it accordingly." A JSON serializer never gets to "know" your Customer class the way you do when you write customer.Name directly — it has to open the box, read the label ("this has a property called Name, of type string"), and act on that description every single time.

Under the Hood

WHY REFLECTION IS SLOWER — THE REAL, CONCRETE COSTS
1. MEMBER LOOKUP IS A STRING-KEYED SEARCH, NOT A DIRECT ADDRESS
2. VALUE-TYPE ARGUMENTS AND RETURN VALUES GET BOXED
3. NO JIT INLINING ACROSS THE Invoke BOUNDARY
4. WHY THIS MATTERS FOR HOT PATHS

Common Confusion

1. Reflection is not the same thing as dynamic

dynamic (covered in earlier modules) defers type checking and member resolution to runtime automatically, with syntax that reads exactly like normal member access — the compiler quietly generates reflection-like lookup code behind the scenes for you. Reflection is the explicit, lower-level API you call yourself (GetMethod, Invoke) to do that lookup and invocation directly. dynamic is, in a real sense, built on the same underlying dynamic-dispatch machinery reflection exposes — just with friendlier syntax hiding the mechanics.

2. Reflection can access private members too — and that has real implications

Passing BindingFlags.NonPublic | BindingFlags.Instance to member-lookup calls lets reflection read or invoke private fields, properties, and methods, bypassing normal access-modifier enforcement entirely. This is genuinely useful for certain testing/tooling scenarios, but it also means "private" is a compile-time-only barrier from reflection's point of view — worth knowing before assuming a private member is truly unreachable from outside its class.

3. "Slow" doesn't mean "never use it" — it means "know where you're using it"

Reflection's overhead is real, but it's routinely and correctly used in exactly the places this lesson describes — application startup, DI container configuration, one-time attribute scanning — where it runs a small, bounded number of times rather than in a hot loop. The problem is specifically reflection running on every iteration of a hot path, not reflection existing in a codebase at all.

Common Mistakes

Mistake 1 — Calling GetMethod/GetProperty repeatedly inside a hot loop

//  re-discovers the same MethodInfo on every single item — wasted lookup work every iteration
foreach (var item in millionsOfItems)
{
    item.GetType().GetMethod("Process")?.Invoke(item, null);
}

Look the member up once outside the loop (if all items share a type), or cache MethodInfo/PropertyInfo results keyed by type — the lookup itself is repeatable work you don't need to redo for every element.

Mistake 2 — Assuming a member always exists and skipping the null check

type.GetMethod("Withdraw").Invoke(customer, [50m]);GetMethod returns null, not an exception, when no matching member is found; this compiles but throws a NullReferenceException at runtime for the smallest typo in the method name.

Always check for null (or use the null-conditional ?.) after a reflection lookup — there's no compile-time safety net here, unlike an ordinary method call the compiler would have rejected outright for a typo.

Mistake 3 — Reaching for reflection when a simpler, statically-typed tool already solves the problem

Using reflection to call a method you actually know about at compile time, just because it feels more "flexible." If you know the type and member at compile time, call it directly — an interface, a delegate, or generics (covered earlier in this Advanced tier) almost always solves the same problem with better performance and full compile-time checking. Reflection earns its place specifically when the type genuinely isn't known until runtime.

When Should I Use It?

Mental Model

Ordinary code = the compiler already knows exactly what you're calling — fast, checked, direct.
Reflection = code discovers what exists, as data, then acts on that discovery — flexible, unchecked until runtime, genuinely slower per call.
Type = the entry point; MethodInfo/PropertyInfo/FieldInfo/ConstructorInfo = descriptions of individual members you can then invoke or read/write.

Remember: almost every "how does that even work without me writing code for it" tool in .NET — DI, serialization, ORMs, test discovery — is reflection doing exactly what you just did by hand in this lesson, generalized across arbitrary types.

Key Takeaway


Check Your Understanding

You've seen what reflection is, how to use it, and the real tools that depend on it every day. Let's check your understanding.

1. How does a general-purpose JSON serializer, compiled and shipped before your Customer class ever existed, know how to serialize it?

Show answer

Correct: B

Why B is correct: This is exactly the capability this lesson introduces — reflection lets code discover a type's members at runtime and act on that discovery generically, which is precisely how a serializer written before your type existed can still handle it correctly.

Why A is incorrect: That's exactly what reflection-based serializers exist to avoid — no per-type hand-written code is needed for the common case.

Why C is incorrect: The C# compiler doesn't generate serialization code automatically for arbitrary types by default — reflection-based serializers do this discovery at runtime instead (though source generators, covered next, are a compile-time alternative some libraries also support).

Why D is incorrect: General-purpose reflection-based serializers work on any type without needing to be told about it in advance — that's the whole point.

Reinforcement: Reflection is what makes "works on types the library author never saw" possible at all.

2. Why does MethodInfo.Invoke(target, [50m]) incur boxing overhead that a direct call like customer.Withdraw(50m) does not?

Show answer

Correct: B

Why B is correct: As covered in "Under the Hood," Invoke's object?[] parameter forces every value-type argument to be boxed to fit that generic signature — overhead that simply doesn't exist for a direct call, where the argument is passed in its native form.

Why A is incorrect: A direct call like customer.Withdraw(50m) passes the decimal value directly, with no boxing at all — boxing is specific to the reflection path here, not a universal fact about decimal.

Why C is incorrect: The method's own compiled code is identical either way — the overhead is entirely in how the call reaches it, not in the method itself.

Why D is incorrect: The overhead described is specifically about boxing the arguments/return value, not about allocating a new object representing the method on every call.

Reinforcement: The object-typed signature of Invoke is the direct, mechanical source of reflection's boxing overhead for value-type arguments.

3. A method needs to run once, at application startup, to construct a handful of registered services via their constructors. Another method runs on every incoming HTTP request in a high-throughput API. Where is reflection's performance cost most likely to actually matter?

Show answer

Correct: C

Why C is correct: Reflection's overhead is per-call. A handful of calls at startup is negligible against total startup time; the same overhead repeated on every request of a high-throughput path accumulates into a real, measurable cost — exactly the distinction drawn in "Under the Hood" and "When Should I Use It?"

Why A is incorrect: Call frequency is precisely what determines whether the overhead matters — it's not a fixed, context-independent cost.

Why B is incorrect: Startup code needing to be fast doesn't change where the overhead actually accumulates — a handful of one-time reflective calls is genuinely cheap in absolute terms, regardless of general startup-speed goals.

Why D is incorrect: The costs (member lookup, boxing, no inlining) are real and documented — this lesson exists specifically because they matter in the right (or wrong) context.

Reinforcement: The question to ask is never "is reflection slow," but "how many times will this particular reflective call run."

You now understand how a huge share of the .NET ecosystem's "it just works with any type" tooling is actually built — and exactly why that flexibility has a real performance price. Next up: attributes, the declarative metadata reflection so often reads back.


dotnetmadeeasy.com — Learn C# and .NET, the right way.