C# Meta-programming
Introduction
Meta-programming is a powerful programming technique where one program can treat another program (or itself) as data. In C#, meta-programming allows you to write code that can inspect, generate, modify, or transform other code at runtime. This is fundamentally different from regular programming, where you write code that directly performs computations.
Think of meta-programming like this: instead of directly building something, you're creating tools that can build things for you based on specifications. It's like the difference between manually assembling furniture versus designing a robot that can assemble any furniture based on blueprints.
In C#, meta-programming is primarily achieved through:
- Reflection - Inspecting types, methods, and properties at runtime
- Expression Trees - Representing code as data structures
- Code Generation - Creating and executing code dynamically
- Attributes - Adding metadata to code elements
This guide will walk you through these concepts, showing you how meta-programming can make your C# applications more flexible and powerful.
Understanding Meta-programming Concepts
What Makes Meta-programming Powerful?
Meta-programming allows your program to:
- Adapt to changing conditions at runtime
- Generate specialized code based on input or configuration
- Extend functionality without modifying existing code
- Create domain-specific languages within C#
- Reduce redundancy by generating repetitive code
Let's explore the key mechanisms that enable meta-programming in C#.
Reflection-based Meta-programming
Reflection is the foundation of meta-programming in C#. It allows your program to examine its own metadata, types, and members at runtime.
Basic Type Inspection
using System;
using System.Reflection;
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public void Greet()
{
Console.WriteLine($"Hello, my name is {Name}");
}
}
// Examining a type using reflection
Type personType = typeof(Person);
Console.WriteLine($"Type name: {personType.Name}");
Console.WriteLine($"Properties: {personType.GetProperties().Length}");
Console.WriteLine($"Methods: {personType.GetMethods().Length}");
// Output:
// Type name: Person
// Properties: 2
// Methods: 5 (includes inherited methods like ToString())
Creating and Invoking Objects Dynamically
// Creating an instance dynamically
object personInstance = Activator.CreateInstance(typeof(Person));
// Setting properties dynamically
PropertyInfo nameProp = typeof(Person).GetProperty("Name");
nameProp.SetValue(personInstance, "John");
// Invoking methods dynamically
MethodInfo greetMethod = typeof(Person).GetMethod("Greet");
greetMethod.Invoke(personInstance, null);
// Output:
// Hello, my name is John
Expression Trees
Expression trees represent code as data structures. They're especially useful for translating C# code to other formats or for creating runtime-optimized code.
Creating an Expression Tree
using System;
using System.Linq.Expressions;
// Creating a simple expression tree for: x => x + 1
ParameterExpression parameter = Expression.Parameter(typeof(int), "x");
BinaryExpression operation = Expression.Add(
parameter,
Expression.Constant(1)
);
// Creating a lambda expression from the expression tree
Expression<Func<int, int>> lambdaExpression = Expression.Lambda<Func<int, int>>(
operation,
parameter
);
// Compiling and executing the expression
Func<int, int> compiledFunc = lambdaExpression.Compile();
int result = compiledFunc(5);
Console.WriteLine($"Result: {result}");
// Output:
// Result: 6
Visualizing Expression Trees
// Define a simple expression using a lambda
Expression<Func<int, bool>> isPositive = x => x > 0;
// Print the structure of the expression tree
static void PrintExpressionTree(Expression expression, string indent = "")
{
if (expression is BinaryExpression binary)
{
Console.WriteLine($"{indent}BinaryExpression: {binary.NodeType}");
Console.WriteLine($"{indent}Left:");
PrintExpressionTree(binary.Left, indent + " ");
Console.WriteLine($"{indent}Right:");
PrintExpressionTree(binary.Right, indent + " ");
}
else if (expression is ParameterExpression parameter)
{
Console.WriteLine($"{indent}ParameterExpression: {parameter.Name}");
}
else if (expression is ConstantExpression constant)
{
Console.WriteLine($"{indent}ConstantExpression: {constant.Value}");
}
else if (expression is LambdaExpression lambda)
{
Console.WriteLine($"{indent}LambdaExpression:");
PrintExpressionTree(lambda.Body, indent + " ");
}
else
{
Console.WriteLine($"{indent}Expression: {expression.NodeType}");
}
}
PrintExpressionTree(isPositive);
// Output:
// LambdaExpression:
// BinaryExpression: GreaterThan
// Left:
// ParameterExpression: x
// Right:
// ConstantExpression: 0
Dynamic Code Generation
C# allows you to generate and execute code at runtime using the System.Reflection.Emit
namespace or the Roslyn
compilation API.
Using Reflection.Emit
using System;
using System.Reflection;
using System.Reflection.Emit;
// Create assembly and module builders
AssemblyName assemblyName = new AssemblyName("DynamicAssembly");
AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(
assemblyName,
AssemblyBuilderAccess.Run);
ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("DynamicModule");
// Create a new type
TypeBuilder typeBuilder = moduleBuilder.DefineType("DynamicType",
TypeAttributes.Public);
// Add a method to the type
MethodBuilder methodBuilder = typeBuilder.DefineMethod(
"Add",
MethodAttributes.Public | MethodAttributes.Static,
typeof(int),
new Type[] { typeof(int), typeof(int) });
// Generate IL code for the method
ILGenerator ilGenerator = methodBuilder.GetILGenerator();
ilGenerator.Emit(OpCodes.Ldarg_0);
ilGenerator.Emit(OpCodes.Ldarg_1);
ilGenerator.Emit(OpCodes.Add);
ilGenerator.Emit(OpCodes.Ret);
// Create the type and invoke the method
Type dynamicType = typeBuilder.CreateType();
object result = dynamicType.GetMethod("Add").Invoke(null, new object[] { 5, 7 });
Console.WriteLine($"5 + 7 = {result}");
// Output:
// 5 + 7 = 12
Using Dynamic Language Runtime (DLR)
using System;
using System.Dynamic;
// Create a dynamic object
dynamic calculator = new ExpandoObject();
// Add properties and methods dynamically
calculator.Value = 10;
calculator.Add = (Func<int, int>)(x => calculator.Value + x);
calculator.Subtract = (Func<int, int>)(x => calculator.Value - x);
// Use the dynamic object
Console.WriteLine($"Initial value: {calculator.Value}");
Console.WriteLine($"Add 5: {calculator.Add(5)}");
Console.WriteLine($"Subtract 3: {calculator.Subtract(3)}");
// Output:
// Initial value: 10
// Add 5: 15
// Subtract 3: 7
Attributes for Meta-programming
Attributes add metadata to your code, which can be used by reflection to make runtime decisions.
Creating and Using Custom Attributes
using System;
// Define a custom attribute
[AttributeUsage(AttributeTargets.Property)]
public class ValidateRangeAttribute : Attribute
{
public int Min { get; }
public int Max { get; }
public ValidateRangeAttribute(int min, int max)
{
Min = min;
Max = max;
}
}
// Use the attribute in a class
public class Product
{
public string Name { get; set; }
[ValidateRange(0, 1000)]
public int Price { get; set; }
[ValidateRange(0, 100)]
public int Stock { get; set; }
}
// Use reflection to validate the object
public static class Validator
{
public static bool Validate(object obj, out string errorMessage)
{
errorMessage = null;
Type type = obj.GetType();
foreach (PropertyInfo property in type.GetProperties())
{
ValidateRangeAttribute attribute =
(ValidateRangeAttribute)property.GetCustomAttribute(typeof(ValidateRangeAttribute));
if (attribute != null)
{
int value = (int)property.GetValue(obj);
if (value < attribute.Min || value > attribute.Max)
{
errorMessage = $"Property {property.Name} value {value} is outside the range " +
$"{attribute.Min}-{attribute.Max}";
return false;
}
}
}
return true;
}
}
// Test the validation
Product product = new Product { Name = "Laptop", Price = 1200, Stock = 5 };
if (!Validator.Validate(product, out string error))
{
Console.WriteLine(error);
}
// Output:
// Property Price value 1200 is outside the range 0-1000
Real-World Applications
Application 1: Building a Simple ORM
One common application of meta-programming is building an Object-Relational Mapper (ORM) that can dynamically map database tables to C# classes.
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
// Attribute to mark properties that map to database columns
[AttributeUsage(AttributeTargets.Property)]
public class ColumnAttribute : Attribute
{
public string Name { get; }
public ColumnAttribute(string name)
{
Name = name;
}
}
// Attribute to mark the class as a table
[AttributeUsage(AttributeTargets.Class)]
public class TableAttribute : Attribute
{
public string Name { get; }
public TableAttribute(string name)
{
Name = name;
}
}
// Simple ORM implementation
public static class SimpleORM
{
public static string GenerateSelectQuery<T>() where T : class
{
Type type = typeof(T);
TableAttribute tableAttr = type.GetCustomAttribute<TableAttribute>();
if (tableAttr == null)
throw new ArgumentException($"Type {type.Name} is not marked with TableAttribute");
StringBuilder sb = new StringBuilder();
sb.Append($"SELECT ");
List<string> columns = new List<string>();
foreach (PropertyInfo prop in type.GetProperties())
{
ColumnAttribute colAttr = prop.GetCustomAttribute<ColumnAttribute>();
if (colAttr != null)
{
columns.Add(colAttr.Name);
}
}
sb.Append(string.Join(", ", columns));
sb.Append($" FROM {tableAttr.Name}");
return sb.ToString();
}
public static T MapDataToObject<T>(Dictionary<string, object> data) where T : new()
{
T obj = new T();
Type type = typeof(T);
foreach (PropertyInfo prop in type.GetProperties())
{
ColumnAttribute colAttr = prop.GetCustomAttribute<ColumnAttribute>();
if (colAttr != null && data.ContainsKey(colAttr.Name))
{
prop.SetValue(obj, Convert.ChangeType(data[colAttr.Name], prop.PropertyType));
}
}
return obj;
}
}
// Example usage
[Table("Users")]
public class User
{
[Column("user_id")]
public int Id { get; set; }
[Column("username")]
public string Username { get; set; }
[Column("email")]
public string Email { get; set; }
}
// Generate a SQL query
string sql = SimpleORM.GenerateSelectQuery<User>();
Console.WriteLine(sql);
// Simulate mapping data from a database
Dictionary<string, object> userData = new Dictionary<string, object>
{
{ "user_id", 1 },
{ "username", "johndoe" },
{ "email", "[email protected]" }
};
User user = SimpleORM.MapDataToObject<User>(userData);
Console.WriteLine($"User: {user.Id}, {user.Username}, {user.Email}");
// Output:
// SELECT user_id, username, email FROM Users
// User: 1, johndoe, [email protected]
Application 2: Plugin System
Meta-programming is excellent for creating plugin systems that can discover and load components at runtime.
using System;
using System.Collections.Generic;
using System.Reflection;
// Interface that all plugins must implement
public interface IPlugin
{
string Name { get; }
void Execute();
}
// Attribute to mark plugin classes
[AttributeUsage(AttributeTargets.Class)]
public class PluginAttribute : Attribute
{
public string Description { get; }
public PluginAttribute(string description)
{
Description = description;
}
}
// Plugin manager
public class PluginManager
{
private List<IPlugin> _plugins = new List<IPlugin>();
public void DiscoverPlugins(Assembly assembly)
{
foreach (Type type in assembly.GetTypes())
{
// Check if the type has our plugin attribute and implements our interface
if (type.GetCustomAttribute<PluginAttribute>() != null &&
typeof(IPlugin).IsAssignableFrom(type) &&
!type.IsInterface &&
!type.IsAbstract)
{
// Create an instance and add it to our plugins
IPlugin plugin = (IPlugin)Activator.CreateInstance(type);
_plugins.Add(plugin);
Console.WriteLine($"Discovered plugin: {plugin.Name}");
}
}
}
public void ExecuteAll()
{
foreach (var plugin in _plugins)
{
Console.WriteLine($"Executing {plugin.Name}...");
plugin.Execute();
}
}
}
// Example plugins
[Plugin("Sends email notifications")]
public class EmailNotifier : IPlugin
{
public string Name => "Email Notifier";
public void Execute()
{
Console.WriteLine("Sending email notifications...");
}
}
[Plugin("Logs to system console")]
public class ConsoleLogger : IPlugin
{
public string Name => "Console Logger";
public void Execute()
{
Console.WriteLine("Logging to console...");
}
}
// Using the plugin system
PluginManager manager = new PluginManager();
manager.DiscoverPlugins(Assembly.GetExecutingAssembly());
manager.ExecuteAll();
// Output:
// Discovered plugin: Email Notifier
// Discovered plugin: Console Logger
// Executing Email Notifier...
// Sending email notifications...
// Executing Console Logger...
// Logging to console...
Best Practices and Considerations
While meta-programming is powerful, it comes with some challenges:
-
Performance Overhead: Reflection and dynamic code execution are slower than statically compiled code. Use caching where possible.
-
Type Safety: Meta-programming often bypasses compile-time type checking, which can lead to runtime errors.
-
Maintainability: Code that generates other code can be harder to debug and maintain.
-
Security: Dynamically executing code can pose security risks if the code source isn't trusted.
When to Use Meta-programming
Consider meta-programming when:
- You need to build frameworks or libraries that must adapt to user code
- You're working with serialization, mapping, or data conversion
- Creating domain-specific languages embedded in C#
- Implementing plugins or extension systems
- Reducing repetitive boilerplate code
Avoid meta-programming when:
- A simpler design would suffice
- Performance is critical
- The code needs to be easily understood by less experienced developers
- Type safety is a major concern
Summary
Meta-programming in C# allows you to write code that manipulates other code at runtime. Through reflection, expression trees, code generation, and attributes, you can create flexible and powerful applications.
We've explored how to:
- Inspect and manipulate types using reflection
- Build and execute expression trees
- Generate code dynamically
- Use attributes for declarative meta-programming
- Apply these techniques in real-world scenarios like ORMs and plugin systems
These techniques enable you to create more adaptable and concise code by focusing on the patterns and structures in your code rather than writing everything explicitly.
Further Resources
- Microsoft Docs: Reflection in .NET
- Expression Trees in C#
- Dynamic Language Runtime Overview
- CodeDOM Overview
Exercise
- Create a simple serializer that can convert objects to JSON format using reflection.
- Build a command pattern implementation that can discover and execute commands marked with custom attributes.
- Use expression trees to build a simple LINQ provider that can translate expressions to a custom query format.
- Create a plugin system that can load plugins from external DLL files using reflection.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)