Table of Contents
- What is Reflection in C#?
- How Reflection Works: Under the Hood
- Core Reflection Classes in C#
- Common Use Cases for Reflection
- Step-by-Step Examples: Using Reflection in Practice
- Best Practices and Limitations
- Conclusion
- 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, orPropertyInfoinstances to reduce repeated metadata lookup overhead. - Use
nameof()for Magic Strings: ReplaceGetMethod("Greet")withGetMethod(nameof(Person.Greet))to catch typos at compile time. - Prefer Alternatives When Possible: Use generics,
dynamickeyword, 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.,
TargetInvocationExceptionfor 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
- Microsoft Docs: System.Reflection Namespace
- Microsoft Docs: Reflection in .NET
- C# in Depth: Reflection (Jon Skeet)
- Performance of Reflection vs. Other Techniques
Happy coding! 🚀