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.