reflection expressiontrees metaprogrammazione

Reflection, Expression Trees e metaprogrammazione pratica in C#

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 PropertyInfo o MethodInfo
  • 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:

ApproccioMomentoPerformanceTipico uso
ReflectionRuntimeLentoDI, mapping, plugin
Expression TreesRuntime (ma compilato)Medio/altoLINQ, filtri dinamici
Source GeneratorCompile-timeAltissimoSerialization, 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

ProblemaSoluzione consigliata
Reflection lentaCache dei metadati o delegate compilati
Expression Trees complessiPrecompilare e riutilizzare il delegate
Codice poco leggibileIncapsulare in builder/factory
Uso eccessivo di metaprogrammazioneApplicarla 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

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


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.


Pubblicato

in

da

Tag: