Skip to main content

Reflection and Metaprogramming

Overview​

Metaprogramming is a technique where a program manipulates or generates itself or other programs. .NET provides powerful metaprogramming features such as Reflection, Expression Trees, and the recently introduced Source Generators.

Reflection​

Reflection allows you to inspect and manipulate assemblies, modules, types, and members at runtime.

Basic Usage​

using System.Reflection;

public class Person
{
public string Name { get; set; }
public void SayHello() => Console.WriteLine($"Hello, {Name}!");
}

// Get type information
Type type = typeof(Person);
// or
// Person p = new Person();
// Type type = p.GetType();

// Create instance
object instance = Activator.CreateInstance(type);

// Set property
PropertyInfo prop = type.GetProperty("Name");
prop.SetValue(instance, "Alice");

// Invoke method
MethodInfo method = type.GetMethod("SayHello");
method.Invoke(instance, null); // Output: Hello, Alice!

Performance Impact and Mitigation​

Reflection is powerful but slower than normal code execution.

  • Caching: PropertyInfo and MethodInfo have high retrieval costs, so cache and reuse them.
  • Delegates: Use Delegate.CreateDelegate to convert reflection calls into delegates for better performance.

Source Generators​

Source Generators allow you to inspect code at compile time and generate new C# source files that are added to the compilation. Introduced in .NET 5.

Difference from Reflection​

  • Runtime vs Compile-time: Reflection resolves at runtime, whereas Source Generators generate code at compile time, resulting in no runtime overhead.
  • AOT (Ahead-of-Time) Support: Since they don't rely on reflection, they are suitable for environments like Native AOT.

Use Cases​

  • System.Text.Json: Faster JSON serialization
  • ILogger: Generation of high-performance logging methods
  • INotifyPropertyChanged: Automatic generation of boilerplate code

Expression Trees​

Expression Trees represent code as a data structure (tree). This allows code to be inspected, modified, or compiled into executable code at runtime.

Basics​

using System.Linq.Expressions;

// Treat lambda expression as data
Expression<Func<int, int, int>> addExpr = (a, b) => a + b;

// Compile into executable delegate
Func<int, int, int> addFunc = addExpr.Compile();
Console.WriteLine(addFunc(1, 2)); // 3

Usage in ORM​

LINQ providers like Entity Framework Core use Expression Trees to translate C# query expressions into SQL.

Dynamic Type and ExpandoObject​

dynamic type​

Introduced in C# 4.0, the dynamic keyword allows you to defer type checking until runtime.

dynamic d = 1;
Console.WriteLine(d.GetType()); // System.Int32
d = "Hello";
Console.WriteLine(d.GetType()); // System.String

ExpandoObject​

Using System.Dynamic.ExpandoObject, you can create objects where members can be dynamically added or removed at runtime.

dynamic person = new System.Dynamic.ExpandoObject();
person.Name = "Bob";
person.Age = 30;

// Can be cast to dictionary
var dict = (IDictionary<string, object>)person;
Console.WriteLine(dict["Name"]); // Bob

Best Practices​

  1. Prefer Source Generators over Reflection: Use compile-time code generation whenever possible for better performance and AOT compatibility.
  2. Cache Reflection Results: Since type and member information rarely changes, cache retrieved MethodInfo or PropertyInfo in static fields or dictionaries.
  3. Minimize use of dynamic: Avoid it unless necessary (e.g., COM interoperability, dynamic JSON processing) as you lose static typing safety and IDE support (like IntelliSense).
  4. Consider Security: Accessing private members via reflection is possible but can introduce security risks and future compatibility issues, so do it cautiously.

Summary​

  • Reflection: Highly versatile but requires attention to performance.
  • Source Generators: Perform code generation at compile time, excelling in performance and AOT support. Often recommended as a replacement for reflection in modern .NET development.
  • Expression Trees: Essential for LINQ providers and dynamic query construction.
  • Dynamic: Since you lose the benefits of static typing, use it only when necessary, such as for COM interoperability or working with dynamic languages.