Skip to main content

Reflection

Reflection is the ability to inspect and interact with the metadata of types at runtime. It allows you to discover information about assemblies, modules, types, methods, properties, and attributes—and even invoke methods or access properties dynamically without knowing them at compile time.

The Problem

Sometimes you need to work with types that you don't know about until runtime. Consider these scenarios:

  • A plugin system that loads assemblies dynamically
  • Serialization frameworks that need to read/write any object's properties
  • Dependency injection containers that instantiate types based on configuration
  • Object mapping libraries that transform one type into another
  • Testing frameworks that discover and invoke test methods

Without reflection, you'd need to write specific code for every possible type, which is impractical or impossible in these scenarios.

How Reflection Works

Every type in .NET has metadata stored in the assembly. Reflection provides APIs to read this metadata:

// Get type information
Type platformType = typeof(Platform);

// Or from an instance
var platform = new Platform();
Type instanceType = platform.GetType();

// From a string (useful for dynamic loading)
Type dynamicType = Type.GetType("CommandAPI.Models.Platform");

Inspecting Type Information

Once you have a Type object, you can discover everything about it:

public class Platform
{
public int Id { get; set; }
public string PlatformName { get; set; }
public DateTime CreatedAt { get; set; }

public void UpdateName(string newName)
{
PlatformName = newName;
}
}

// Get all properties
Type type = typeof(Platform);
PropertyInfo[] properties = type.GetProperties();

foreach (var prop in properties)
{
Console.WriteLine($"{prop.Name} ({prop.PropertyType.Name})");
}
// Output:
// Id (Int32)
// PlatformName (String)
// CreatedAt (DateTime)

// Get all methods
MethodInfo[] methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);

foreach (var method in methods)
{
Console.WriteLine($"{method.Name}");
}
// Output includes: UpdateName, get_Id, set_Id, etc.

Creating Instances Dynamically

Reflection allows you to create instances without knowing the type at compile time:

// Using Activator
Type type = typeof(Platform);
object instance = Activator.CreateInstance(type);

// With constructor parameters
Type commandType = typeof(Command);
object command = Activator.CreateInstance(
commandType,
new object[] { "List files", "ls -la", 1 }
);

// Using ConstructorInfo
ConstructorInfo ctor = type.GetConstructor(Type.EmptyTypes);
object anotherInstance = ctor.Invoke(null);

Accessing Properties and Fields

Read and write property values dynamically:

var platform = new Platform 
{
Id = 1,
PlatformName = "Docker"
};

Type type = typeof(Platform);

// Get property value
PropertyInfo nameProp = type.GetProperty("PlatformName");
string name = (string)nameProp.GetValue(platform);
Console.WriteLine(name); // "Docker"

// Set property value
nameProp.SetValue(platform, "Kubernetes");
Console.WriteLine(platform.PlatformName); // "Kubernetes"

// Work with all properties
foreach (var prop in type.GetProperties())
{
object value = prop.GetValue(platform);
Console.WriteLine($"{prop.Name}: {value}");
}

Invoking Methods

Call methods dynamically:

var platform = new Platform { PlatformName = "Docker" };
Type type = typeof(Platform);

// Get the method
MethodInfo method = type.GetMethod("UpdateName");

// Invoke it
method.Invoke(platform, new object[] { "Kubernetes" });

Console.WriteLine(platform.PlatformName); // "Kubernetes"

// For methods with return values
MethodInfo toString = type.GetMethod("ToString");
string result = (string)toString.Invoke(platform, null);

Attributes and Metadata

Reflection is commonly used to read custom attributes:

[Required]
[StringLength(100)]
public string PlatformName { get; set; }

// Read attributes
PropertyInfo prop = typeof(Platform).GetProperty("PlatformName");
var attributes = prop.GetCustomAttributes();

foreach (var attr in attributes)
{
if (attr is RequiredAttribute)
{
Console.WriteLine("This property is required");
}
else if (attr is StringLengthAttribute stringLength)
{
Console.WriteLine($"Max length: {stringLength.MaximumLength}");
}
}

// Check for specific attribute
bool hasRequired = prop.GetCustomAttribute<RequiredAttribute>() != null;

Common Use Cases

Dependency Injection

DI containers use reflection to instantiate services:

public class SimpleContainer
{
public T Resolve<T>() where T : class
{
Type type = typeof(T);

// Get constructor
ConstructorInfo ctor = type.GetConstructors()[0];

// Get parameters
ParameterInfo[] parameters = ctor.GetParameters();

// Resolve dependencies recursively
object[] args = parameters
.Select(p => Resolve(p.ParameterType))
.ToArray();

// Create instance
return (T)ctor.Invoke(args);
}

private object Resolve(Type type)
{
// Simplified - real containers have registration logic
return Activator.CreateInstance(type);
}
}

Object Mapping

Mapping frameworks like Mapster and AutoMapper use reflection to copy properties:

public class SimpleMapper
{
public TDestination Map<TSource, TDestination>(TSource source)
where TDestination : new()
{
var destination = new TDestination();

Type sourceType = typeof(TSource);
Type destType = typeof(TDestination);

foreach (var sourceProp in sourceType.GetProperties())
{
// Find matching property in destination
var destProp = destType.GetProperty(sourceProp.Name);

if (destProp != null && destProp.CanWrite)
{
// Copy value
object value = sourceProp.GetValue(source);
destProp.SetValue(destination, value);
}
}

return destination;
}
}

// Usage
var dto = new PlatformReadDto { Id = 1, PlatformName = "Docker" };
var mapper = new SimpleMapper();
var entity = mapper.Map<PlatformReadDto, Platform>(dto);

Serialization

JSON serializers use reflection to read/write properties:

public class SimpleJsonSerializer
{
public string Serialize(object obj)
{
var type = obj.GetType();
var properties = type.GetProperties();

var pairs = properties.Select(p =>
{
var value = p.GetValue(obj);
var valueStr = value is string
? $"\"{value}\""
: value?.ToString() ?? "null";
return $"\"{p.Name}\": {valueStr}";
});

return "{" + string.Join(", ", pairs) + "}";
}
}

Plugin Systems

Loading and instantiating types from external assemblies:

public interface IPlugin
{
void Execute();
}

public class PluginLoader
{
public IEnumerable<IPlugin> LoadPlugins(string assemblyPath)
{
// Load assembly
Assembly assembly = Assembly.LoadFrom(assemblyPath);

// Find types implementing IPlugin
var pluginTypes = assembly.GetTypes()
.Where(t => typeof(IPlugin).IsAssignableFrom(t)
&& !t.IsInterface
&& !t.IsAbstract);

// Instantiate each plugin
foreach (var type in pluginTypes)
{
yield return (IPlugin)Activator.CreateInstance(type);
}
}
}

Performance Considerations

Reflection is powerful but comes with a significant performance cost:

The Overhead

const int iterations = 1_000_000;
var platform = new Platform { Id = 1, PlatformName = "Docker" };

// Direct access - fast
var sw = Stopwatch.StartNew();
for (int i = 0; i < iterations; i++)
{
var name = platform.PlatformName;
}
sw.Stop();
Console.WriteLine($"Direct: {sw.ElapsedMilliseconds}ms");

// Reflection - slow
Type type = typeof(Platform);
PropertyInfo prop = type.GetProperty("PlatformName");
sw.Restart();
for (int i = 0; i < iterations; i++)
{
var name = prop.GetValue(platform);
}
sw.Stop();
Console.WriteLine($"Reflection: {sw.ElapsedMilliseconds}ms");

// Typical results: Direct: 2ms, Reflection: 500ms+

Optimization Strategies

1. Cache Type Information

// Bad - repeated lookups
public void ProcessMany(List<Platform> platforms)
{
foreach (var platform in platforms)
{
Type type = platform.GetType(); // Repeated!
PropertyInfo prop = type.GetProperty("PlatformName"); // Repeated!
var value = prop.GetValue(platform);
}
}

// Good - cache the reflection objects
private static readonly Type PlatformType = typeof(Platform);
private static readonly PropertyInfo NameProperty = PlatformType.GetProperty("PlatformName");

public void ProcessMany(List<Platform> platforms)
{
foreach (var platform in platforms)
{
var value = NameProperty.GetValue(platform);
}
}

2. Use Compiled Expressions

For hot paths, compile reflection calls into delegates:

public class PropertyAccessor<T>
{
private readonly Func<T, object> _getter;
private readonly Action<T, object> _setter;

public PropertyAccessor(string propertyName)
{
PropertyInfo prop = typeof(T).GetProperty(propertyName);

// Compile getter
var instance = Expression.Parameter(typeof(T), "instance");
var property = Expression.Property(instance, prop);
var convert = Expression.Convert(property, typeof(object));
_getter = Expression.Lambda<Func<T, object>>(convert, instance).Compile();

// Compile setter
var value = Expression.Parameter(typeof(object), "value");
var convertValue = Expression.Convert(value, prop.PropertyType);
var assign = Expression.Assign(property, convertValue);
_setter = Expression.Lambda<Action<T, object>>(assign, instance, value).Compile();
}

public object GetValue(T instance) => _getter(instance);
public void SetValue(T instance, object value) => _setter(instance, value);
}

// Much faster than reflection after initial compilation
var accessor = new PropertyAccessor<Platform>("PlatformName");
var name = accessor.GetValue(platform); // Fast!

3. Consider Source Generators

For .NET 5+, source generators can generate code at compile time, avoiding reflection entirely:

// Instead of using reflection at runtime, generate code at compile time
// This is how modern serializers like System.Text.Json achieve high performance

BindingFlags

Control what members reflection returns:

Type type = typeof(Platform);

// Public instance members (default)
var publicProps = type.GetProperties();

// All properties including private
var allProps = type.GetProperties(
BindingFlags.Public |
BindingFlags.NonPublic |
BindingFlags.Instance
);

// Static members only
var staticMembers = type.GetMembers(
BindingFlags.Public |
BindingFlags.Static
);

// Declared on this type only (not inherited)
var declaredOnly = type.GetMethods(
BindingFlags.Public |
BindingFlags.Instance |
BindingFlags.DeclaredOnly
);

Best Practices

Avoid reflection in hot paths: Use it during initialization or configuration, not in frequently-called methods.

// Bad - reflection in loop
public void ProcessRequests(List<Request> requests)
{
foreach (var request in requests)
{
Type type = request.GetType(); // Reflected on every iteration
// ...
}
}

// Good - use polymorphism instead
public void ProcessRequests(List<IRequest> requests)
{
foreach (var request in requests)
{
request.Process(); // Virtual method call, much faster
}
}

Cache reflection results: Store Type, PropertyInfo, and MethodInfo objects rather than looking them up repeatedly.

Prefer generics over reflection when possible:

// If you can use generics, do
public T Create<T>() where T : new() => new T();

// Don't use reflection when generics work
public object Create(Type type) => Activator.CreateInstance(type);

Handle exceptions: Reflection operations can throw various exceptions:

try
{
Type type = Type.GetType("NonExistent.Type");
var instance = Activator.CreateInstance(type); // NullReferenceException
}
catch (ArgumentException)
{
// Invalid type name
}
catch (TypeLoadException)
{
// Type could not be loaded
}
catch (MissingMethodException)
{
// Constructor not found
}

Use nameof operator to avoid magic strings when you know the member at compile time:

// Good
PropertyInfo prop = type.GetProperty(nameof(Platform.PlatformName));

// Bad - typo-prone
PropertyInfo prop = type.GetProperty("PlatformName");

When NOT to Use Reflection

Reflection is often overused. Consider alternatives:

  • Interfaces and polymorphism: If you need different behavior for different types, use interfaces
  • Generics: If you need type safety with flexibility, use generics
  • Design patterns: Many problems that seem to require reflection can be solved with patterns like Strategy or Factory
  • Source generators: For compile-time code generation without runtime overhead

Reflection should be a tool of last resort, not your first choice.

Further Reading

For comprehensive documentation on reflection in .NET, see the official Microsoft documentation.