Metaprogramming in C#: When Code Writes Code
Imagine you are an architect who builds not just a house, but a robot builder that erects walls, lays wiring, and paints facades on its own. In the world of C#, this "robot" is metaprogramming — the art of writing programs that create, analyze, or modify other programs (or themselves) during execution.
Why is this needed? Imagine you are writing dozens of similar classes for working with JSON, creating complex plugin systems, or are too lazy to manually call hundreds of methods via reflection. Metaprogramming in C# allows you to automate routine tasks, create flexible frameworks, and speed up development manifold. Today we will break down the three pillars of this technique: reflection, expression trees, and dynamic code generation.
This article is for those who already write confidently in C# but want to look under the hood of the CLR and learn the magic that distinguishes a senior from a mid-level developer. Let's go!
Reflection: An X-Ray for Your Code
Reflection is the foundation of metaprogramming in C#. It allows you to obtain information about types, methods, properties, and fields at runtime, as well as dynamically invoke them. Imagine looking at an object like an open book, able to read any page.
Getting Type Information
The simplest way is to use the typeof keyword or the GetType() method:
Type stringType = typeof(string);Console.WriteLine($"Name: {stringType.Name}"); // StringConsole.WriteLine($"Namespace: {stringType.Namespace}"); // SystemConsole.WriteLine($"Is it a class? {stringType.IsClass}"); // TrueDynamic Method Invocation
Suppose you have a Calculator class, and you want to call its Add method, whose name is only known at runtime (e.g., from configuration):
public class Calculator{ public int Add(int a, int b) => a + b;}
// Using reflectionCalculator calc = new Calculator();Type calcType = typeof(Calculator);MethodInfo? addMethod = calcType.GetMethod("Add");
if (addMethod != null){ int result = (int)addMethod.Invoke(calc, new object[] { 5, 3 }); Console.WriteLine($"Result: {result}"); // 8}When is Reflection Useful?
- Serialization / Deserialization — libraries like Newtonsoft.Json actively use reflection.
- Plugins — loading external assemblies and calling their methods.
- Testing — accessing private fields and methods (though it's better not to do this).
Important! Reflection is a slow operation. If you call the same method thousands of times, consider caching MethodInfo or using delegates.
Expression Trees: Code as Data
Expression trees are a way to represent code as a data structure (a tree). You can analyze, modify, and even compile this code "on the fly." This is a powerful tool for creating ORMs (Entity Framework), validators, and dynamic queries.
Creating a Simple Tree
Instead of writing the lambda x => x * 2, we build it manually:
using System.Linq.Expressions;
// Create parameter xParameterExpression param = Expression.Parameter(typeof(int), "x");// Create constant 2ConstantExpression constant = Expression.Constant(2);// Create multiplication x * 2BinaryExpression body = Expression.Multiply(param, constant);// Assemble into lambda: x => x * 2Expression<Func<int, int>> lambda = Expression.Lambda<Func<int, int>>(body, param);
// Compile and executeFunc<int, int> compiled