Reflection & Dynamic Type Inspection

Reviewed & published by Brayan K

By the end of this lesson you'll be able to inspect any type's structure at runtime — its properties, methods, and fields — read and set values by name, invoke methods and build objects dynamically, and read custom attributes. This is the machinery behind frameworks like ASP.NET, Entity Framework, and JSON serializers.

Part of the free C# course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

💡 Real-World Analogy

Reflection is an X-ray for your code. Normally you interact with an object through its public buttons and levers — you call person.Greet() because you wrote that line knowing it exists. Reflection lets a program take an X-ray of a type it has never seen before: it can see every property, every method, every field — even the private ones tucked inside — and then act on what it finds. That's how a JSON serializer turns any object into text without you writing code for each class, and how a test runner discovers every method marked [Test]. The program inspects a type's own structure at runtime and reacts to it.

The Reflection Vocabulary

Reflection lives in the System.Reflection namespace and revolves around a family of "info" objects. Each one describes one piece of a type. Once you have a Type, everything else hangs off it.

ObjectDescribesHow you get it
TypeA whole class/struct/interfacetypeof(T) / obj.GetType()
PropertyInfoOne propertytype.GetProperty("X") / GetProperties()
MethodInfoOne methodtype.GetMethod("X") / GetMethods()
FieldInfoOne fieldtype.GetField("X") / GetFields()
ConstructorInfoOne constructortype.GetConstructor(...)
AttributeA metadata tagmember.GetCustomAttribute<T>()

By default, GetProperties() and friends return only the public instance members. To reach private or static members you must pass BindingFlags (covered in section 1).

1. Inspecting a Type

Everything starts with a Type object — the runtime's description of a class. Use typeof(Person) when you know the type at compile time, or obj.GetType() when you only have an instance. From a Type you can call GetProperties(), GetMethods(), and GetFields() to list its members. By default those see only public members; to include private ones you pass BindingFlags — a set of switches like Public | NonPublic | Instance. Read this worked example, run it, then you'll write your own loop.

using System;
using System.Reflection;

class Person
{
    public string Name { get; set; } = "";
    public int Age { get; set; }
    private string secret = "hidden";

    public void Greet() => Console.WriteLine($"Hi, I'm {Name}!");
    private void Think() => Console.WriteLine("Thinking...");

    public string GetInfo(string prefix) => $"{prefix}: {Name}, age {Age}";
}

class Program
{
    static void Main()
    {
        // Every type has a Type object describing its shape.
        // typeof(T) gets it at COMPILE time when you know the type.
        Type type = typeof(Person);

        Console.WriteLine($"=== Type: {type.Name} ===");   // === Type: Person ===
        Console.WriteLine($"Full name: {type.FullName}");   // Full name: Person
        Console.WriteLine($"Is class: {type.IsClass}");     // Is class: True

        // GetProperties() returns a PropertyInfo for each PUBLIC property.
        Console.WriteLine("\n--- Properties ---");
        foreach (PropertyInfo prop in type.GetProperties())
            Console.WriteLine($"  {prop.PropertyType.Name} {prop.Name}");
        //   String Name
        //   Int32 Age

        // GetMethods/GetFields need BindingFlags to see PRIVATE members.
        var flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;

        Console.WriteLine("\n--- Methods (declared here) ---");
        foreach (MethodInfo m in type.GetMethods(flags))
            if (m.DeclaringType == typeof(Person))     // skip inherited ToString() etc.
                Console.WriteLine($"  {(m.IsPublic ? "public" : "private")} {m.Name}");
        //   public Greet
        //   private Think
        //   public GetInfo

        Console.WriteLine("\n--- Fields ---");
        foreach (FieldInfo f in type.GetFields(flags))
            Console.WriteLine($"  {(f.IsPublic ? "public" : "private")} {f.FieldType.Name} {f.Name}");
        //   private String secret
    }
}

// ✅ Expected output:
//    === Type: Person ===
//    Full name: Person
//    Is class: True
//
//    --- Properties ---
//      String Name
//      Int32 Age
//
//    --- Methods (declared here) ---
//      public get_Name
//      public set_Name
//      public get_Age
//      public set_Age
//      public Greet
//      private Think
//      public GetInfo
//
//    --- Fields ---
//      private String <Name>k__BackingField
//      private Int32 <Age>k__BackingField
//      private String secret

Your turn. The program below gets a Type and loops over its properties — you just need to fill in how to get the type and how to print each property's name. Fill in the three ___ blanks, then run it.

using System;
using System.Reflection;

class Book
{
    public string Title { get; set; } = "";
    public string Author { get; set; } = "";
    public int Pages { get; set; }
}

class Program
{
    static void Main()
    {
        // 🎯 YOUR TURN — fill in the blanks marked with ___

        // 1) Get the Type object for the Book class.
        Type type = ___;            // 👉 use typeof(Book)

        Console.WriteLine($"Properties of {type.Name}:");

        // 2) Loop over every public property.
        foreach (PropertyInfo prop in type.GetProperties())
        {
            // 3) Print the property's type name, then its name.
            Console.WriteLine($"  {prop.PropertyType.Name} {___}");  // 👉 use prop.Name
        }

        // ✅ Expected output:
        //    Properties of Book:
        //      String Title
        //      String Author
        //      Int32 Pages
    }
}

2. Reading Values, Invoking Methods & Creating Objects

Inspecting structure is half the story — reflection also lets you act on an instance by name. PropertyInfo.GetValue(obj) reads a property's value and SetValue(obj, value) writes it. MethodInfo.Invoke(obj, args) calls a method, with the arguments packed into an object[] (use null for none). And Activator.CreateInstance(type) builds an object without writing new — the trick that powers plugin systems that discover types at runtime. Read this worked example, then you'll wire up your own.

using System;
using System.Reflection;

class Calculator
{
    public string Owner { get; set; } = "Lab";
    public int Add(int a, int b) => a + b;
    public string Describe() => "I'm a Calculator!";
}

class Program
{
    static void Main()
    {
        Type calcType = typeof(Calculator);

        // 1) Create an instance WITHOUT writing 'new Calculator()'.
        //    Activator.CreateInstance calls the parameterless constructor.
        object? calc = Activator.CreateInstance(calcType);   // a real Calculator

        // 2) Read a property VALUE off the instance.
        //    GetValue(obj) returns the value boxed as object.
        PropertyInfo? ownerProp = calcType.GetProperty("Owner");
        Console.WriteLine($"Owner = {ownerProp?.GetValue(calc)}");   // Owner = Lab

        // 3) SetValue writes a property by name.
        ownerProp?.SetValue(calc, "Alice");
        Console.WriteLine($"Owner = {ownerProp?.GetValue(calc)}");   // Owner = Alice

        // 4) Invoke a method BY NAME. Arguments go in an object[].
        MethodInfo? add = calcType.GetMethod("Add");
        object? sum = add?.Invoke(calc, new object[] { 5, 3 });
        Console.WriteLine($"Add(5, 3) = {sum}");                     // Add(5, 3) = 8

        // 5) A parameterless method takes null for its arguments.
        MethodInfo? describe = calcType.GetMethod("Describe");
        Console.WriteLine(describe?.Invoke(calc, null));            // I'm a Calculator!
    }
}

// ✅ Expected output:
//    Owner = Lab
//    Owner = Alice
//    Add(5, 3) = 8
//    I'm a Calculator!

Now you try. Read a property value off an instance, find a method by name, and invoke it with one argument. Fill in the three ___ blanks:

using System;
using System.Reflection;

class Greeter
{
    public string Language { get; set; } = "English";
    public string SayHello(string name) => $"Hello, {name}!";
}

class Program
{
    static void Main()
    {
        // 🎯 YOUR TURN — fill in the blanks marked with ___

        var greeter = new Greeter();
        Type type = greeter.GetType();    // GetType() works on an existing object

        // 1) Read the Language property's VALUE off the greeter instance.
        PropertyInfo? langProp = type.GetProperty("Language");
        Console.WriteLine($"Language = {langProp?.___(greeter)}");  // 👉 use GetValue

        // 2) Grab the SayHello method by its name.
        MethodInfo? hello = type.GetMethod("___");                  // 👉 "SayHello"

        // 3) Invoke it on 'greeter', passing one argument: "Sam".
        object? result = hello?.Invoke(greeter, new object[] { ___ });  // 👉 "Sam"
        Console.WriteLine(result);

        // ✅ Expected output:
        //    Language = English
        //    Hello, Sam!
    }
}

🔎 Deep Dive: BindingFlags — the visibility switches

If GetMethod("Think") returns null for a method you know exists, the culprit is almost always visibility. The no-argument overloads return only public instance members. To reach anything else you must combine BindingFlags with a bitwise OR (|):

var flags = BindingFlags.Public      // public members
          | BindingFlags.NonPublic   // private & protected members
          | BindingFlags.Instance;   // members that belong to an OBJECT

// Now private methods show up:
MethodInfo? think = type.GetMethod("Think", flags);

// For static members, add Static (and drop Instance if you only want static):
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static

Rule of thumb: you almost always need at least one of Instance or Static, plus at least one of Public or NonPublic. Forget the visibility pair and you'll silently get nothing back.

3. Custom Attributes

Attributes are metadata tags you attach to code in [Square brackets]. You define your own by extending Attribute, apply it to a class, method, or property, then read it back at runtime with GetCustomAttribute<T>(). This is the engine behind so much of .NET: ASP.NET discovers routes from [HttpGet], EF Core maps columns from [Required], and serializers skip fields marked [JsonIgnore]. The framework uses reflection to find your tags and act on them.

using System;
using System.Reflection;

// Define a custom attribute by extending Attribute.
[AttributeUsage(AttributeTargets.Method)]
class RouteAttribute : Attribute
{
    public string Path { get; }
    public string Verb { get; }
    public RouteAttribute(string path, string verb = "GET")
    {
        Path = path;
        Verb = verb;
    }
}

// Apply attributes — they're metadata tags attached to code.
class UserController
{
    [Route("/api/users", "GET")]
    public void List() => Console.WriteLine("Listing users");

    [Route("/api/users", "POST")]
    public void Create() => Console.WriteLine("Creating a user");

    public void Helper() { }   // no attribute — should be skipped
}

class Program
{
    static void Main()
    {
        Type type = typeof(UserController);

        Console.WriteLine("=== Discovered routes ===");
        foreach (MethodInfo m in type.GetMethods(BindingFlags.Public | BindingFlags.Instance))
        {
            // GetCustomAttribute returns null if the tag isn't present.
            var route = m.GetCustomAttribute<RouteAttribute>();
            if (route != null)
                Console.WriteLine($"  {route.Verb,-4} {route.Path} -> {m.Name}()");
        }
        // === Discovered routes ===
        //   GET  /api/users -> List()
        //   POST /api/users -> Create()
        // (Helper has no [Route], so reflection skips it.)
    }
}

// ✅ Expected output:
//    === Discovered routes ===
//      GET  /api/users -> List()
//      POST /api/users -> Create()

Pro Tips

Common Errors (and the fix)

📋 Quick Reference

TaskCodeNotes
Get type (compile time)typeof(Person)When you know the type
Get type (from instance)obj.GetType()At runtime
List propertiestype.GetProperties()Public instance by default
One method by nametype.GetMethod("Add")null if not found
See private members..., BindingFlags.NonPublic | InstancePass the flags
Read a valueprop.GetValue(obj)Returns object (boxed)
Write a valueprop.SetValue(obj, val)Sets the property
Call a methodmethod.Invoke(obj, new object[]{ 5, 3 })null args for none
Create an objectActivator.CreateInstance(type)No new needed
Read an attributemember.GetCustomAttribute<T>()null if absent

Frequently Asked Questions

Q: What's the difference between typeof and GetType()?

typeof(Person) works at compile time when you already know the type. obj.GetType() works at runtime on an instance and returns the object's actual type — which may be a subclass. If Animal a = new Dog();, then typeof(Animal) is Animal but a.GetType() is Dog.

Q: Why is my private method or field invisible to reflection?

The default GetMethod/GetField only returns public members. Add BindingFlags.NonPublic | BindingFlags.Instance (plus Static for static members) to the call to reach private ones.

Q: Is reflection slow enough to worry about?

For occasional use — startup, configuration, a one-off scan — it's fine. In a tight loop it's a problem: it can be 10–100× slower than a direct call. Cache the PropertyInfo/MethodInfo outside the loop, or switch to source generators or compiled delegates for hot paths.

Q: Why did Invoke throw a TargetInvocationException instead of the real error?

When the method you invoked throws, reflection wraps that exception in a TargetInvocationException. The original error is in its InnerException — inspect that to see what actually went wrong.

Mini-Challenge: A Generic Object Dumper

No blanks this time — just a brief and an outline. Write a Dump(object obj) method that uses reflection to print every public property of any object as Name = Value — the same trick a debugger or serializer uses. Run it on the Product and check your output against the expected lines in the comments.

using System;
using System.Reflection;

// A small class to dump. Feel free to add more properties.
class Product
{
    public string Name { get; set; } = "Widget";
    public decimal Price { get; set; } = 9.99m;
    public bool InStock { get; set; } = true;
}

class Program
{
    // 🎯 MINI-CHALLENGE: write a generic "object dumper".
    // Dump(object obj) should print every PUBLIC property as  Name = Value.
    // 1. Get the object's Type with obj.GetType().
    // 2. Loop over type.GetProperties().
    // 3. For each prop, print  $"{prop.Name} = {prop.GetValue(obj)}".
    //
    // ✅ Expected output for a default Product:
    //    Name = Widget
    //    Price = 9.99
    //    InStock = True
    static void Dump(object obj)
    {
        // your code here
    }

    static void Main()
    {
        Dump(new Product());
    }
}

🎉 Lesson Complete

Practice quiz

How do you get a Type object at compile time when you already know the type?

  • obj.GetType()
  • Type.From(Person)
  • typeof(Person)
  • new Type(Person)

Answer: typeof(Person). typeof(T) gets the Type at compile time; obj.GetType() is for when you only have an instance.

By default, what members does Type.GetProperties() return?

  • Only public instance members
  • All members including private
  • Only static members
  • Nothing until you pass BindingFlags

Answer: Only public instance members. Without BindingFlags, the no-argument overloads return only public instance members.

Which BindingFlags combination is needed to see a private instance method?

  • Public | Instance
  • Public | Static
  • NonPublic | Static
  • NonPublic | Instance

Answer: NonPublic | Instance. To reach a private instance member you need NonPublic plus Instance.

What does Activator.CreateInstance(type) do?

  • Returns the Type's name
  • Creates an object without writing 'new'
  • Lists the type's properties
  • Reads a property value

Answer: Creates an object without writing 'new'. Activator.CreateInstance builds an object dynamically, calling the parameterless constructor.

Which method reads a property's value off an instance via reflection?

  • prop.GetValue(obj)
  • prop.SetValue(obj)
  • prop.Invoke(obj)
  • prop.ReadValue(obj)

Answer: prop.GetValue(obj). PropertyInfo.GetValue(obj) reads a property's value; SetValue writes it.

When invoking a method with MethodInfo.Invoke, how are the arguments passed?

  • As named parameters
  • As a comma-separated string
  • Packed into an object[] (or null for none)
  • One at a time via chained calls

Answer: Packed into an object[] (or null for none). Invoke(obj, args) takes the arguments packed into an object[]; pass null when there are none.

How do you define your own custom attribute?

  • Implement IAttribute
  • Extend the Attribute class
  • Mark the class [Custom]
  • Inherit from Type

Answer: Extend the Attribute class. You define a custom attribute by extending the Attribute base class.

What does GetCustomAttribute<T>() return if the tag isn't present on a member?

  • A new empty attribute
  • It throws an exception
  • The base Attribute
  • null

Answer: null. GetCustomAttribute<T>() returns null when the attribute isn't applied to the member.

Why is reflection slow in a tight loop?

  • It recompiles the code each time
  • Boxing, security checks, and member lookups make it 10-100x slower than direct calls
  • It always allocates a new Type
  • It blocks on network I/O

Answer: Boxing, security checks, and member lookups make it 10-100x slower than direct calls. Reflected calls are roughly 10-100x slower due to boxing, security checks, and member lookups; cache lookups outside loops.

Why can trimming or Native AOT break reflection-based code?

  • Reflection requires the internet
  • AOT disables typeof
  • The trimmer drops members that only look used via reflection
  • Attributes are removed at runtime

Answer: The trimmer drops members that only look used via reflection. Code only reached via reflection looks unused, so the trimmer/AOT can cut it; annotate or prefer source generators.

Continue this course

Related lessons