codelessgenie guide

How to Use Reflection in C#: Dynamic Programming Explained

In C#, most programming tasks rely on **compile-time type checking**, where the compiler verifies types, methods, and members before execution. But what if you need to interact with code whose structure isn’t known until runtime? Enter **reflection**—a powerful feature that enables programs to inspect, analyze, and manipulate their own metadata (type information, methods, properties, etc.) dynamically. Reflection is the backbone of many modern C# frameworks and tools: think dependency injection containers (e.g., ASP.NET Core DI), object-relational mappers (ORMs like Entity Framework), serialization libraries (Newtonsoft.Json), and unit testing frameworks (xUnit, NUnit). It allows you to build flexible, extensible applications (e.g., plugins, dynamic UIs, or code generators) by decoupling code from compile-time dependencies. In this blog, we’ll demystify reflection in C#. We’ll start with the basics, explore its core components, walk through practical examples, and discuss best practices to use it effectively. By the end, you’ll understand when and how to leverage reflection to solve complex dynamic programming challenges.

Table of Contents

  1. What is Reflection in C#?
  2. How Reflection Works: Under the Hood
  3. Core Reflection Classes in C#
  4. Common Use Cases for Reflection
  5. Step-by-Step Examples: Using Reflection in Practice
  6. Best Practices and Limitations
  7. Conclusion
  8. References

1. What is Reflection in C#?

Reflection is a feature of the .NET runtime that allows a program to:

  • Inspect the metadata of types (classes, structs, interfaces, enums, etc.).
  • Instantiate objects, invoke methods, or access properties/fields dynamically (without compile-time knowledge of their names).
  • Analyze and use custom attributes applied to types or members.
  • Load and execute code from external assemblies (e.g., plugins) at runtime.

In simpler terms, reflection lets your code “look in the mirror” and interact with itself programmatically.

2. How Reflection Works: Under the Hood

Reflection relies on two key components:

Metadata

When you compile C# code, the compiler generates a metadata table stored in the assembly (.exe or .dll). This table contains information about:

  • Types (name, base type, interfaces, visibility).
  • Members (methods, properties, fields, constructors).
  • Attributes (custom or built-in).
  • Assembly details (version, culture, strong name).

The .NET Reflection API

The .NET Framework provides a set of classes in the System.Reflection namespace to read and manipulate this metadata. Key classes include Type, Assembly, MethodInfo, PropertyInfo, and ConstructorInfo.

3. Core Reflection Classes

To use reflection effectively, you need to understand these foundational classes:

Type Class

The Type class is the heart of reflection. It represents type metadata and provides methods to inspect members (methods, properties, etc.). Every object in C# has a GetType() method that returns its Type instance (e.g., typeof(string) or "hello".GetType()).

Assembly Class

Represents a .NET assembly (.exe or .dll). Use it to load assemblies, enumerate types within them, or retrieve assembly-level metadata (e.g., version).

MemberInfo and Derived Classes

  • MethodInfo: Inspect and invoke methods.
  • PropertyInfo: Inspect and manipulate properties.
  • FieldInfo: Access fields (public or private).
  • ConstructorInfo: Instantiate objects via constructors.
  • ParameterInfo: Describe method/constructor parameters.

Attribute Class

Base class for all custom attributes. Use GetCustomAttributes() to retrieve attributes applied to types or members.

4. Common Use Cases for Reflection

Reflection shines in scenarios where static code is insufficient:

  • Serialization/Deserialization: Libraries like Newtonsoft.Json use reflection to map JSON properties to object fields dynamically.
  • Dependency Injection (DI): DI containers (e.g., Microsoft.Extensions.DependencyInjection) use reflection to resolve and instantiate services at runtime.
  • ORMs: Tools like Entity Framework use reflection to map database tables to C# classes.
  • Unit Testing: Frameworks like xUnit use reflection to discover and execute test methods.
  • Plugin Architectures: Load external plugins (assemblies) at runtime without recompiling the main application.
  • Dynamic Code Generation: Tools like T4 templates use reflection to generate code based on existing types.

5. Step-by-Step Examples

Let’s explore practical reflection scenarios with code examples. We’ll use a target class Person for consistency:

// Target class for reflection examples
public class Person
{
    // Public property
    public string Name { get; set; }
    
    // Private field
    private int _age;
    
    // Public constructor
    public Person(string name, int age)
    {
        Name = name;
        _age = age;
    }
    
    // Public method
    public void Greet(string message) => Console.WriteLine($"{Name} says: {message}");
    
    // Private method
    private string GetSecret() => $"Secret age: {_age}";
    
    // Static method
    public static string GetSpecies() => "Homo sapiens";
    
    // Custom attribute
    [Obsolete("Use Greet() instead")]
    public void OldGreet() => Console.WriteLine($"Hello, {Name}");
}

Example 1: Getting Type Information

Let’s start by inspecting the Person type to retrieve its metadata.

using System;
using System.Reflection;

class Program
{
    static void Main()
    {
        // Get Type instance for Person
        Type personType = typeof(Person);

        // Basic type info
        Console.WriteLine($"Type Name: {personType.Name}");
        Console.WriteLine($"Full Name: {personType.FullName}");
        Console.WriteLine($"Base Type: {personType.BaseType?.Name}"); // System.Object

        // List public methods
        Console.WriteLine("\nPublic Methods:");
        foreach (MethodInfo method in personType.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static))
        {
            Console.WriteLine($"- {method.Name}");
        }

        // List properties
        Console.WriteLine("\nProperties:");
        foreach (PropertyInfo prop in personType.GetProperties())
        {
            Console.WriteLine($"- {prop.Name} (Type: {prop.PropertyType.Name})");
        }
    }
}

Output:

Type Name: Person
Full Name: ReflectionDemo.Person
Base Type: Object

Public Methods:
- Greet
- GetSpecies
- OldGreet
- get_Name
- set_Name
- ToString
- Equals
- GetHashCode
- GetType

Properties:
- Name (Type: String)

Note: BindingFlags specifies which members to retrieve (e.g., Public, Instance, Static).

Example 2: Instantiating Objects Dynamically

Use Activator.CreateInstance() or ConstructorInfo.Invoke() to create objects without compile-time knowledge of the type.

// Instantiate Person using the constructor (string name, int age)
Type personType = typeof(Person);
object personInstance = Activator.CreateInstance(personType, "Alice", 30); // Parameters: name, age

// Cast to Person (optional, if type is known)
Person alice = (Person)personInstance;
Console.WriteLine($"Created: {alice.Name}, Age: {alice.GetType().GetField("_age", BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(alice)}");

Output:

Created: Alice, Age: 30

Example 3: Invoking Methods Dynamically

Use MethodInfo.Invoke() to call methods, even private ones.

// Get the Greet method (public instance method)
MethodInfo greetMethod = personType.GetMethod("Greet", new[] { typeof(string) }); // Parameters: string message
greetMethod.Invoke(alice, new object[] { "Hello, Reflection!" }); // Invoke with parameters

// Get the private GetSecret method
MethodInfo secretMethod = personType.GetMethod("GetSecret", BindingFlags.NonPublic | BindingFlags.Instance);
string secret = (string)secretMethod.Invoke(alice, null); // No parameters
Console.WriteLine(secret);

Output:

Alice says: Hello, Reflection!
Secret age: 30

Example 4: Accessing Properties and Fields

Read/write properties/fields using PropertyInfo and FieldInfo.

// Access the Name property (public)
PropertyInfo nameProp = personType.GetProperty("Name");
nameProp.SetValue(alice, "Alice Smith"); // Update property
Console.WriteLine($"Updated Name: {nameProp.GetValue(alice)}");

// Access the private _age field
FieldInfo ageField = personType.GetField("_age", BindingFlags.NonPublic | BindingFlags.Instance);
ageField.SetValue(alice, 31); // Update private field
Console.WriteLine($"Updated Age: {ageField.GetValue(alice)}");

Output:

Updated Name: Alice Smith
Updated Age: 31

Example 5: Working with Attributes

Retrieve custom attributes applied to types or members.

// Check if OldGreet is obsolete
MethodInfo oldGreetMethod = personType.GetMethod("OldGreet");
ObsoleteAttribute obsoleteAttr = (ObsoleteAttribute)oldGreetMethod.GetCustomAttribute(typeof(ObsoleteAttribute));
if (obsoleteAttr != null)
{
    Console.WriteLine($"Warning: {obsoleteAttr.Message}");
}

Output:

Warning: Use Greet() instead

Example 6: Loading External Assemblies

Load a compiled DLL at runtime and use its types (useful for plugins).

Step 1: Create a External Library

Create a class library project PluginLibrary with:

namespace PluginLibrary;
public class GreeterPlugin
{
    public string GetGreeting() => "Hello from Plugin!";
}

Compile it to generate PluginLibrary.dll.

Step 2: Load the DLL at Runtime

// Load the external assembly
Assembly pluginAssembly = Assembly.LoadFrom(@"C:\Path\To\PluginLibrary.dll");

// Get the GreeterPlugin type
Type greeterType = pluginAssembly.GetType("PluginLibrary.GreeterPlugin");

// Instantiate and invoke
object greeter = Activator.CreateInstance(greeterType);
MethodInfo getGreetingMethod = greeterType.GetMethod("GetGreeting");
string greeting = (string)getGreetingMethod.Invoke(greeter, null);

Console.WriteLine(greeting);

Output:

Hello from Plugin!

6. Best Practices and Limitations

Reflection is powerful but comes with tradeoffs:

Limitations

  • Performance Overhead: Reflection is slower than static code (due to runtime metadata lookup and security checks). Avoid in performance-critical paths.
  • Security Risks: Requires ReflectionPermission (restricted in partial-trust environments like ASP.NET). Accessing private members can break encapsulation.
  • Maintainability: Uses “magic strings” (e.g., GetMethod("Greet")), which are error-prone if method names change.
  • Compile-Time Safety: No compiler checks—errors (e.g., missing methods) occur at runtime.

Best Practices

  • Cache Reflection Objects: Cache Type, MethodInfo, or PropertyInfo instances to reduce repeated metadata lookup overhead.
  • Use nameof() for Magic Strings: Replace GetMethod("Greet") with GetMethod(nameof(Person.Greet)) to catch typos at compile time.
  • Prefer Alternatives When Possible: Use generics, dynamic keyword, or expression trees for better performance and safety.
  • Restrict Access: Avoid accessing private members unless necessary—they may change in future versions.
  • Handle Exceptions: Wrap reflection code in try-catch blocks (e.g., TargetInvocationException for failed method calls).

7. Conclusion

Reflection is a versatile tool for dynamic programming in C#, enabling scenarios like plugin architectures, serialization, and DI. However, it should be used judiciously due to performance and maintainability costs.

By mastering reflection, you gain the ability to write flexible, extensible code that adapts to runtime conditions. Remember: use reflection when static code can’t solve the problem, and always prefer safer alternatives (generics, expression trees) when possible.

8. References

Happy coding! 🚀