La metaprogrammazione è l’arte di scrivere codice che manipola codice.
In C#, questo concetto si concretizza grazie a strumenti potenti come la Reflection API e gli Expression Trees, che permettono di:
- analizzare tipi e membri a runtime,
- generare o trasformare funzioni al volo,
- automatizzare validazioni, mapping o query LINQ personalizzate,
- e perfino scrivere compilatori o framework interni.
Nel mondo enterprise (ORM, dependency injection, serializers, mapper, ecc.) questi strumenti sono ovunque — spesso senza che ce ne accorgiamo. In questo articolo vedremo come funzionano, quando usarli e come evitare i rischi più comuni di performance e manutenzione.
Reflection: introspezione del runtime
Cos’è la Reflection
La Reflection consente di esplorare e manipolare i tipi a runtime, accedendo a metadati come proprietà, metodi, attributi, campi e costruttori.
Esempio base:
var type = typeof(Customer);
foreach (var prop in type.GetProperties())
{
Console.WriteLine($"{prop.Name} : {prop.PropertyType}");
}
Puoi anche invocare metodi dinamicamente:
var method = type.GetMethod("CalculateDiscount");
var instance = Activator.CreateInstance(type);
method.Invoke(instance, new object[] { 100 });
Usi pratici di Reflection
- Serializer / Deserializer (es. Newtonsoft.Json, System.Text.Json)
- Dependency Injection Container (scansione automatica di assembly)
- ORM come Entity Framework (scoperta dinamica delle entità)
- Validatori o mappatori automatici (AutoMapper, FluentValidation)
- Framework di test (NUnit, xUnit, MSTest)
Performance consideration
Reflection è flessibile ma lenta, perché implica lookup dinamici e boxing/unboxing.
Per mitigare:
- Cache dei
PropertyInfooMethodInfo - Generazione dinamica di delegate (
CreateDelegate) - Uso combinato con Expression Trees
Expression Trees: codice come dati
Gli Expression Trees permettono di rappresentare il codice C# come un albero di espressioni, manipolabile a runtime.
A differenza della Reflection, non si limitano a leggere i metadati, ma descrivono la logica esecutiva.
Esempio di base
Expression<Func<int, int>> square = x => x * x;
Console.WriteLine(square); // Output: x => (x * x)
Dietro le quinte, square.Body è un albero di nodi (BinaryExpression, ParameterExpression, ecc.) che rappresentano la formula x * x.
Esempio pratico: creazione dinamica di un filtro
Expression<Func<Customer, bool>> BuildFilter(string propName, string value)
{
var param = Expression.Parameter(typeof(Customer), "c");
var prop = Expression.Property(param, propName);
var val = Expression.Constant(value);
var body = Expression.Equal(prop, val);
return Expression.Lambda<Func<Customer, bool>>(body, param);
}
// Uso:
var filter = BuildFilter("City", "Rome");
var customers = dbContext.Customers.Where(filter);
Questo approccio è ciò che permette a LINQ to Entities o LINQ to SQL di tradurre le query in SQL in modo dinamico.
Perché usare Expression Trees invece di Reflection
- Molto più performanti quando compilati in delegate (
Compile()). - Possono essere convertiti in codice SQL o JSON (vedi LINQ providers).
- Consentono analisi statica (es. per validazioni o generazione automatica di DTO).
Metaprogrammazione pratica
La metaprogrammazione combina entrambe le tecniche (Reflection + Expression Trees) per generare codice o comportamenti dinamici.
Validatore automatico
public static class Validator<T>
{
private static readonly List<Func<T, bool>> _rules = new();
public static void AddRule(Expression<Func<T, bool>> rule)
=> _rules.Add(rule.Compile());
public static bool Validate(T obj)
=> _rules.All(r => r(obj));
}
// Uso:
Validator<Customer>.AddRule(c => !string.IsNullOrEmpty(c.Email));
Validator<Customer>.AddRule(c => c.Age > 18);
var valid = Validator<Customer>.Validate(new Customer { Email = "x@x.com", Age = 20 });
Dynamic Mapping (mini AutoMapper)
public static class DynamicMapper
{
public static Func<TSource, TDest> Build<TSource, TDest>()
{
var src = Expression.Parameter(typeof(TSource), "src");
var bindings = typeof(TDest).GetProperties()
.Where(p => p.CanWrite)
.Select(destProp =>
{
var srcProp = typeof(TSource).GetProperty(destProp.Name);
if (srcProp == null) return null;
return Expression.Bind(destProp, Expression.Property(src, srcProp));
})
.Where(b => b != null)!;
var body = Expression.MemberInit(Expression.New(typeof(TDest)), bindings);
return Expression.Lambda<Func<TSource, TDest>>(body, src).Compile();
}
}
// Uso:
var map = DynamicMapper.Build<User, UserDto>();
var dto = map(new User { Name = "Filippo", Age = 35 });
Risultato: mapping ultraveloce senza riflessione ripetuta a ogni invocazione.
Source Generators vs Reflection
Con .NET 5+ è arrivato un nuovo paradigma: Source Generators, che spostano la metaprogrammazione a compile-time.
Differenze principali:
| Approccio | Momento | Performance | Tipico uso |
|---|---|---|---|
| Reflection | Runtime | Lento | DI, mapping, plugin |
| Expression Trees | Runtime (ma compilato) | Medio/alto | LINQ, filtri dinamici |
| Source Generator | Compile-time | Altissimo | Serialization, DTO, DI nativo |
Esempio: System.Text.Json usa generatori di sorgente per creare serializer ottimizzati a build time, riducendo la necessità di reflection.
Performance e Best Practice
| Problema | Soluzione consigliata |
|---|---|
| Reflection lenta | Cache dei metadati o delegate compilati |
| Expression Trees complessi | Precompilare e riutilizzare il delegate |
| Codice poco leggibile | Incapsulare in builder/factory |
| Uso eccessivo di metaprogrammazione | Applicarla solo dove la logica non è conoscibile a compile-time |
| Sicurezza (Reflection su assembly esterni) | Validare i tipi e limitare l’uso in ambienti sandboxed |
Risorse e Riferimenti
Documentazione ufficiale
- Microsoft Docs – Reflection in C#
- Microsoft Docs – Expression Trees
- Microsoft Docs – Source Generators
- Performance best practices for reflection
Letture consigliate
- “CLR via C#” – Jeffrey Richter
- “C# In Depth” – Jon Skeet
- “Metaprogramming in .NET” – Kevin Hazzard, Jason Bock
- Blog ufficiale di .NET e Roslyn SDK
Librerie open source
- FastMember – Accesso veloce ai membri via reflection
- ExpressionPowerTools – Manipolazione avanzata di expression trees
- AutoMapper – Mapping automatico con expression trees
FAQ
1. Reflection è ancora utile con .NET 8 e Source Generators?
Sì. I generatori sostituiscono molti casi di uso statico, ma la Reflection resta indispensabile per plugin, runtime extensibility e test dinamici.
2. Gli Expression Trees sono difficili da mantenere?
Solo se scritti “a mano”. Usa builder o librerie helper per generare espressioni complesse in modo leggibile.
3. Posso combinare Expression Trees e LINQ dinamico?
Sì. È ciò che fa Entity Framework: traduce espressioni lambda in SQL usando l’albero dell’espressione.
4. C’è un impatto sulle performance?
Sì, se usata male. Precompila, cache, evita reflection ripetute e misura sempre con BenchmarkDotNet.
5. Qual è il futuro della metaprogrammazione in .NET?
La direzione è ibrida: usare Source Generators per i casi noti a compile-time e Expression Trees per l’analisi e la manipolazione dinamica a runtime.
Conclusione
Reflection, Expression Trees e metaprogrammazione non sono solo “trucchi avanzati”: rappresentano la base invisibile di molti framework moderni.
Usate con criterio, possono ridurre enormemente il codice boilerplate, creare sistemi estendibili e adattivi, e aprire la strada a pattern evoluti come CQRS, Domain-Driven Design e validazione dinamica.
