Understanding Reflection
Reflection lets you examine types, read metadata, and invoke members dynamically at runtime. You'll use reflection for dependency injection containers, serialization libraries, plugin systems, and tools that work with unknown types. It's powerful but comes with performance costs and complexity.
The Type class is the entry point to reflection. You get Type objects through typeof() for compile-time known types or GetType() on instances. Once you have a Type, you can discover its properties, methods, fields, and attributes.
You'll learn how to inspect types, invoke methods dynamically, access properties, and understand when reflection is worth the trade-offs versus alternatives like source generators or strongly-typed approaches.
Inspecting Types with GetType and typeof
The typeof operator returns a Type object for a compile-time known type. It's resolved at compile time and has minimal runtime cost. GetType() is an instance method that returns the actual runtime type of an object, handling polymorphism and derived types.
Once you have a Type object, you can query its properties, methods, constructors, and attributes. This metadata lets you understand object structure without hardcoding knowledge about specific types. Frameworks use this extensively for automatic configuration.
Here's how to inspect types and discover their members:
using System;
using System.Reflection;
var person = new Person { Id = 1, Name = "John Doe", Age = 30, Email = "john@example.com" };
// Get type at runtime
Type personType = person.GetType();
Console.WriteLine($"Type: {personType.Name}");
Console.WriteLine($"Namespace: {personType.Namespace}");
Console.WriteLine($"Full Name: {personType.FullName}");
// Using typeof for compile-time known types
Type knownType = typeof(Person);
Console.WriteLine($"\nSame type? {personType == knownType}");
// Discover properties
Console.WriteLine("\nProperties:");
PropertyInfo[] properties = personType.GetProperties();
foreach (var prop in properties)
{
var value = prop.GetValue(person);
Console.WriteLine($" {prop.Name} ({prop.PropertyType.Name}): {value}");
}
// Discover methods
Console.WriteLine("\nPublic Methods:");
MethodInfo[] methods = personType.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
foreach (var method in methods)
{
Console.WriteLine($" {method.Name}");
}
// Check for attributes
var attributes = personType.GetCustomAttributes();
Console.WriteLine($"\nAttributes: {attributes.Count()}");
// Type checking
Console.WriteLine($"\nIs class? {personType.IsClass}");
Console.WriteLine($"Is value type? {personType.IsValueType}");
Console.WriteLine($"Is public? {personType.IsPublic}");
class Person
{
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
public string Email { get; set; }
public string GetDisplayName()
{
return $"{Name} (ID: {Id})";
}
}
GetProperties() and GetMethods() return arrays of metadata objects describing each member. BindingFlags control which members you see - public versus private, instance versus static, declared versus inherited. You can filter to exactly what you need.
⚠️ Native AOT and Trimming Considerations
Reflection doesn't work well with Native AOT compilation or aggressive trimming. The AOT compiler needs to know which types and members you'll access at compile time. Dynamic type discovery breaks this requirement. If you're targeting Native AOT, use source generators instead of reflection, or annotate your code with DynamicallyAccessedMembers attributes to preserve specific members.
Invoking Methods Dynamically
MethodInfo.Invoke lets you call methods when you don't know them at compile time. You pass the target object and an array of arguments. The runtime handles parameter matching and invokes the method. This enables plugin architectures and generic frameworks.
Dynamic invocation is much slower than direct calls because of parameter validation, boxing, and reflection overhead. For frequently called methods, consider creating delegates with CreateDelegate or using expression trees for better performance.
Here's how to invoke methods and access properties dynamically:
using System;
using System.Reflection;
var calculator = new Calculator();
Type calcType = typeof(Calculator);
// Invoke method dynamically
MethodInfo addMethod = calcType.GetMethod("Add");
object result = addMethod.Invoke(calculator, new object[] { 10, 20 });
Console.WriteLine($"Add(10, 20) = {result}");
// Invoke method with different parameters
MethodInfo multiplyMethod = calcType.GetMethod("Multiply");
result = multiplyMethod.Invoke(calculator, new object[] { 5, 7 });
Console.WriteLine($"Multiply(5, 7) = {result}");
// Get and set properties dynamically
var product = new Product { Id = 1, Name = "Laptop", Price = 999.99m };
Type productType = typeof(Product);
PropertyInfo nameProperty = productType.GetProperty("Name");
string name = (string)nameProperty.GetValue(product);
Console.WriteLine($"\nOriginal name: {name}");
nameProperty.SetValue(product, "Gaming Laptop");
Console.WriteLine($"Updated name: {product.Name}");
// Invoke private methods (for testing scenarios)
MethodInfo privateMethod = calcType.GetMethod("ValidateInput",
BindingFlags.NonPublic | BindingFlags.Instance);
bool isValid = (bool)privateMethod.Invoke(calculator, new object[] { 5 });
Console.WriteLine($"\nValidateInput(5): {isValid}");
class Calculator
{
public int Add(int a, int b)
{
return a + b;
}
public int Multiply(int a, int b)
{
return a * b;
}
private bool ValidateInput(int value)
{
return value >= 0;
}
}
class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
The Invoke method returns object, so you need to cast results to the correct type. Arguments must match parameter types exactly, or you'll get runtime exceptions. For better type safety, validate parameters before invocation or use try-catch blocks.
Reading Custom Attributes
Attributes add metadata to your code that reflection can read at runtime. You use attributes for configuration, validation, serialization hints, and framework integration. Custom attributes let you create your own metadata system.
GetCustomAttributes returns all attributes on a type or member. You can filter by attribute type to find specific metadata. This pattern enables declarative programming where attributes drive behavior without explicit code.
Here's how to define and read custom attributes:
using System;
using System.Reflection;
// Define custom attribute
[AttributeUsage(AttributeTargets.Property)]
public class ValidationAttribute : Attribute
{
public int MinLength { get; set; }
public int MaxLength { get; set; }
public bool Required { get; set; }
}
class User
{
[Validation(MinLength = 3, MaxLength = 50, Required = true)]
public string Username { get; set; }
[Validation(MinLength = 8, MaxLength = 100, Required = true)]
public string Password { get; set; }
public string Email { get; set; }
}
// Validate using reflection
var user = new User { Username = "ab", Password = "short" };
Type userType = typeof(User);
Console.WriteLine("Validation Results:");
foreach (var property in userType.GetProperties())
{
var validationAttr = property.GetCustomAttribute<ValidationAttribute>();
if (validationAttr == null) continue;
var value = (string)property.GetValue(user);
Console.WriteLine($"\n{property.Name}:");
if (validationAttr.Required && string.IsNullOrEmpty(value))
{
Console.WriteLine(" ✗ Required field is empty");
continue;
}
if (value?.Length < validationAttr.MinLength)
Console.WriteLine($" ✗ Too short (min: {validationAttr.MinLength})");
if (value?.Length > validationAttr.MaxLength)
Console.WriteLine($" ✗ Too long (max: {validationAttr.MaxLength})");
if (value?.Length >= validationAttr.MinLength && value?.Length <= validationAttr.MaxLength)
Console.WriteLine(" ✓ Valid");
}
Custom attributes combine well with reflection for building frameworks. Validation libraries, serializers, and ORMs all use this pattern. The attribute provides the what, while reflection provides the how to act on that metadata.
Try It Yourself
This complete example demonstrates a simple plugin system using reflection to discover and invoke plugin methods dynamically.
using System;
using System.Collections.Generic;
using System.Reflection;
// Plugin interface
public interface IPlugin
{
string Name { get; }
void Execute();
}
// Plugin implementations
public class LoggingPlugin : IPlugin
{
public string Name => "Logging Plugin";
public void Execute() => Console.WriteLine("[LOG] Plugin executed");
}
public class MetricsPlugin : IPlugin
{
public string Name => "Metrics Plugin";
public void Execute() => Console.WriteLine("[METRICS] Tracking execution");
}
// Plugin manager using reflection
public class PluginManager
{
private readonly List<IPlugin> _plugins = new();
public void DiscoverPlugins()
{
// Get all types in current assembly
Assembly assembly = Assembly.GetExecutingAssembly();
Type pluginInterface = typeof(IPlugin);
foreach (Type type in assembly.GetTypes())
{
// Check if type implements IPlugin
if (pluginInterface.IsAssignableFrom(type) && !type.IsInterface && !type.IsAbstract)
{
// Create instance using reflection
IPlugin plugin = (IPlugin)Activator.CreateInstance(type);
_plugins.Add(plugin);
Console.WriteLine($"Discovered: {plugin.Name}");
}
}
}
public void ExecuteAll()
{
Console.WriteLine("\nExecuting plugins:");
foreach (var plugin in _plugins)
{
plugin.Execute();
}
}
public void InspectPlugin(string pluginName)
{
var plugin = _plugins.Find(p => p.Name == pluginName);
if (plugin == null)
{
Console.WriteLine($"\nPlugin '{pluginName}' not found");
return;
}
Type type = plugin.GetType();
Console.WriteLine($"\n=== Plugin Details: {pluginName} ===");
Console.WriteLine($"Type: {type.FullName}");
Console.WriteLine("\nProperties:");
foreach (var prop in type.GetProperties())
{
Console.WriteLine($" {prop.Name}: {prop.PropertyType.Name}");
}
Console.WriteLine("\nMethods:");
foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly))
{
Console.WriteLine($" {method.Name}()");
}
}
}
// Main program
var manager = new PluginManager();
manager.DiscoverPlugins();
manager.ExecuteAll();
manager.InspectPlugin("Logging Plugin");
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
Running the example:
- Create a new folder and save both files
- Run
dotnet run to see plugin discovery
- Try adding your own plugin class implementing IPlugin
- Watch how the system discovers it automatically
- Experiment with inspecting different plugins
Common Pitfalls & How to Avoid Them
Performance degradation happens when you use reflection in tight loops. Reflection is expensive, and repeated calls compound the cost. Cache Type and MemberInfo objects, create delegates for repeated invocations, or reconsider whether you need reflection at all. Profile before optimizing.
Null reference exceptions occur when GetMethod or GetProperty returns null for non-existent members. Always check for null before invoking. Use BindingFlags explicitly to avoid confusion about which members you're searching for. Case sensitivity matters for member names.
Security risks emerge with reflection in untrusted environments. Reflection can access private members and bypass access modifiers. If you're loading external assemblies or accepting user input, validate types and members carefully. Consider sandboxing or restricted reflection contexts.
AOT and trimming incompatibility breaks applications using dynamic reflection. The compiler can't preserve types it doesn't see referenced statically. Use DynamicallyAccessedMembers attributes to preserve specific members, or migrate to source generators for AOT-compatible code generation.
Type confusion causes runtime errors when casting reflection results. Invoke returns object, and GetValue returns object. You need to cast to the correct type, which can fail at runtime. Use pattern matching or as operator with null checks for safer casting.