CodeGym /Corsi /C# SELF /Source Generators e ...

Source Generators e System.Reflection.Emit

C# SELF
Livello 63 , Lezione 4
Disponibile

1. Introduzione

Diciamolo onestamente: la maggior parte delle persone pensa alla generazione di codice più o meno come a una macchina per affettare le uova — utile, ma raramente indispensabile. Però i progetti .NET moderni diventano sempre più complessi, e automatizzare il lavoro ripetitivo o template-based non è solo una "feature", ma uno strumento importante per migliorare qualità e produttività.

Source Generators è un meccanismo introdotto a partire da C# 9 e .NET 5. Sono come supereroi nella fase di compilazione: possono generare codice C# che poi viene compilato come parte del tuo progetto. Non interferiscono con il codice già compilato durante l'esecuzione, ma estendono il progetto con nuovi sorgenti prima della compilazione.

Di seguito vedremo scenari tipici di utilizzo di Source Generators.

Creazione automatica di codice boilerplate

In molti progetti bisogna scrivere codice ripetitivo: costruttori, metodi ToString, serializer, property change notification (INotifyPropertyChanged) ecc. Source Generators possono generare questo codice automaticamente, liberando gli sviluppatori dalla routine e dal rischio di errori di battitura.

Esempio: Generator per ToString

Immaginiamo che stiate sviluppando classi DTO (Data Transfer Object) per il trasferimento dati. Serve implementare per ogni classe un ToString sensato, che elenchi tutte le proprietà.

Invece di copiare manualmente, si può creare un Source Generator che per ogni classe con l'attributo [AutoToString] genererà l'implementazione del metodo ToString. Lo fanno, per esempio, librerie come AutoToString.


// La tua classe con l'attributo
[AutoToString] 
public partial class Person 
{
    public string Name { get; set; }
    public int Age { get; set; }
}

// Il Source Generator genererà (semplificato):
public partial class Person
{
    public override string ToString() => $"Person: Name={Name}, Age={Age}";
}

Pro: il codice è leggibile e sempre aggiornato quando si aggiungono nuove proprietà.

Semplificazione di serializzazione e deserializzazione

Source Generators sono usati intensamente nella libreria standard System.Text.Json per generare codice veloce di serializzazione e deserializzazione. Prima dei generatori la serializzazione spesso richiedeva reflection (costoso in termini di performance), mentre ora il codice generato rende tutto veloce e sicuro.

Cosa ottiene lo sviluppatore? Dichiari [JsonSerializable(typeof(MyType))] — e il generator crea codice performante di serializzazione per quel tipo.

Generazione di codice per configurazioni, mappers, container DI

  • Config: i generator creano automaticamente classi di configurazione basate su file JSON.
  • Mapper: per esempio, il progetto Mapster usa generator per creare mapping tra tipi senza copiare manualmente i campi.
  • Dependency Injection: alcuni container (es. StrongInject) usano generator per il codice di registrazione dei servizi.

Integrazione con infrastrutture esterne

Alcuni generator analizzano risorse esterne (descrizione API, schemi GraphQL, protocolli Thrift ecc.) e generano classi C# per interagire con esse. Questo elimina l'aggiornamento manuale del codice quando cambiano i contratti.

Verifica e diagnostica del codice a compile-time

Source Generators possono essere usati anche per la verifica del codice: "Se nel progetto compare il metodo X, ma non è soddisfatta la condizione Y — warning in compilazione!". Così funzionano molti linter e analyzer, ma il generator può aggiungere i propri messaggi o anche inserire uno stub di codice pronto all'uso.

2. Il tuo Source Generator per serializzazione automatica

Vediamo un piccolo esempio "diretto": un generator che per una classe con l'attributo [AutoJson] genera un metodo per serializzare in formato JSON.

Il codice qui sotto è solo illustrativo; nella pratica usate System.Text.Json.SourceGeneration.


// Scritto a mano:
[AutoJson]
public partial class Book
{
    public string Title { get; set; }
    public int Year { get; set; }
}

// Il generator aggiungerà:
public partial class Book
{
    public string ToJson() => $"{{ \"Title\": \"{Title}\", \"Year\": {Year} }}";
}

Schema di interazione tra Source Generator e progetto

flowchart TD
    A(Il tuo Source Code) -->|Il compilatore invoca| B(Source Generator)
    B -->|Codice aggiunto| C(Nuovi file .cs)
    C --> D(Compilazione del progetto)
  • Prima il compilatore (Roslyn) analizza i tuoi sorgenti.
  • Poi invoca il tuo generator (implementando ISourceGenerator).
  • Il generator aggiunge nuovi file .cs che diventano parte dell'albero di compilazione.
  • Alla fine l'assembly contiene sia il tuo codice sia quello generato.

Task specializzati: cos'altro si può generare

  • Binding per librerie native (wrapper per C code o WinAPI).
  • AOP: auto-logging delle chiamate, aspetti (alla Fody, PostSharp ecc.).
  • Descrizione API per generator di documentazione.
  • Gestione risorse esterne: SVG, SQL, Razor — il generator crea classi strongly-typed per accesso type-safe.

3. Generazione dinamica di codice con System.Reflection.Emit

Se Source Generators generano codice C# prima della build, System.Reflection.Emit è lo strumento per la magia durante l'esecuzione. Il tuo programma può creare nuovi tipi, metodi e persino assembly — al volo!

Sembra spaventoso? Un po'. Ma a volte è indispensabile: per esempio quando scrivi framework per proxy dinamici (AOP, profiling, mocking), generator di serializer basati su dati runtime, ORM dinamici ecc.

Quando serve Reflection.Emit

  • I tipi non sono noti a priori (l'utente definisce la struttura al volo).
  • Proxy dinamici (wrapper per intercettare chiamate).
  • Serializzazione ad alte prestazioni (es. protobuf-net).
  • Plugin e motori di scripting con scenari complessi di caricamento.

Cosa si può creare con Reflection.Emit

  • AssemblyBuilder — creare un nuovo assembly.
  • ModuleBuilder — modulo dentro l'assembly.
  • TypeBuilder — descrizione di un nuovo tipo.
  • MethodBuilder — metodo con IL.
  • PropertyBuilder, FieldBuilder, EventBuilder — proprietà, campi, eventi.

Mini-esempio: creare una classe al volo

using System;
using System.Reflection;
using System.Reflection.Emit;

public static class DynamicTypeGenerator
{
    public static Type GenerateSimpleType(string typeName)
    {
        // 1. Creiamo assembly e modulo
        var assemblyName = new AssemblyName("DynamicAssembly");
        var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
        var moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule");

        // 2. Creiamo una nuova classe
        var typeBuilder = moduleBuilder.DefineType(
            typeName, 
            TypeAttributes.Public | TypeAttributes.Class
        );

        // 3. Aggiungiamo una proprietà pubblica string Title
        var field = typeBuilder.DefineField("_title", typeof(string), FieldAttributes.Private);
        var prop = typeBuilder.DefineProperty("Title", PropertyAttributes.HasDefault, typeof(string), null);
        var getMethod = typeBuilder.DefineMethod("get_Title", MethodAttributes.Public, typeof(string), Type.EmptyTypes);
        var il = getMethod.GetILGenerator();
        il.Emit(OpCodes.Ldarg_0);      // this
        il.Emit(OpCodes.Ldfld, field); // _title
        il.Emit(OpCodes.Ret);
        prop.SetGetMethod(getMethod);

        var setMethod = typeBuilder.DefineMethod("set_Title", MethodAttributes.Public, null, new[] { typeof(string) });
        il = setMethod.GetILGenerator();
        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Ldarg_1);
        il.Emit(OpCodes.Stfld, field);
        il.Emit(OpCodes.Ret);
        prop.SetSetMethod(setMethod);

        // 4. Fatto! Creiamo il Type
        return typeBuilder.CreateTypeInfo();
    }
}

Ora puoi usare quel tipo come un normale oggetto C#, per esempio con reflection:

var dynamicType = DynamicTypeGenerator.GenerateSimpleType("Book");
var obj = Activator.CreateInstance(dynamicType);
dynamicType.GetProperty("Title").SetValue(obj, "C# nelle persone");
Console.WriteLine(dynamicType.GetProperty("Title").GetValue(obj)); // C# nelle persone

Così nascono ORM, proxy, serializer, profiler e anche alcuni framework di testing.

4. Note utili

Source Generators vs Reflection.Emit: chi vince?

Source Generators lavorano a compile-time: rendono il tuo sorgente più intelligente, e il risultato finisce nell'assembly compilato. Non puoi usarli per generare codice basato su dati ottenuti solo durante l'esecuzione.

Reflection.Emit lavora in runtime: permette di creare assembly, tipi e metodi dinamicamente, ma quel codice è più difficile da debuggare e mantenere.

Quando usare cosa?

Un'analogia quotidiana:

  • Source Generators — come una fabbrica che produce pezzi pronti prima di assemblare la macchina.
  • Reflection.Emit — come un ingegnere che salda un motore a razzo alla macchina mentre stai guidando.

Particolarità e insidie

  • Il codice generato può essere difficile da debugare. I generator spesso permettono di salvare i sorgenti su disco — cercali nella cartella obj\Generated.
  • Reflection.Emit crea assembly in memoria che non vengono scaricati dall'AppDomain. Usa AssemblyBuilderAccess.RunAndCollect per assembly temporanei (se supportato).
  • Non abusare di Reflection.Emit per task semplici — a volte un source generator o un semplice template è più facile e affidabile.
  • I generator richiedono conoscenza di Roslyn (per Source Generators) e IL (per Reflection.Emit).

Source Generators e Reflection.Emit

Criterio Source Generators Reflection.Emit
Quando si usa Alla compilazione In runtime
Risultato Sorgenti C#, parte dell'assembly IL, nuovi tipi/assembly
Scenari tipici Autogenerazione di codice boilerplate, DI, mapping, serializzazione Proxy, ORM dinamici, pipeline runtime speciali
Complessità d'uso Media, servono conoscenze di Roslyn Alta, servono conoscenze di IL
Supporto IDE e debugging Eccellente (sorgenti visibili) Complicato
Performance Molto alta Può essere alta

5. Scenari pratici

1. Generazione di API strongly-typed

Un'organizzazione fornisce una specifica OpenAPI. Un generator la analizza e crea classi controller, DTO e codice client per lavorare con l'API REST — type-safe e con supporto a IntelliSense.

Codice (pseudo):
// Spec: GET /users -> returns User[]
// Source Generator genererà:
public class ApiClient
{
    public Task<User[]> GetUsersAsync() { ... }
}

2. Injection/DI automatico (Compile-time IoC)

I generator creano builder per registrare le dipendenze e costruire il grafo degli oggetti. Non serve scrivere manualmente services.AddSingleton<IMyService, MyService>().

Codice (pseudo):
[Injectable]
public class MyService : IMyService { ... }

// Source Generator genererà:
partial class DIContainer
{
    public void RegisterServices()
    {
        AddSingleton<IMyService, MyService>();
    }
}

3. Proxy dinamici — esempio con Reflection.Emit

La libreria Castle DynamicProxy costruisce tipi proxy che intercettano le chiamate ai metodi — base per AOP, logging, tracing, mocking.

Codice (semplificato):
public interface IBookService { string GetBook(); }
public class BookService : IBookService { public string GetBook() => "C#"; }

var proxy = ProxyGenerator.CreateProxy<IBookService>(new BookService(), interceptor);
proxy.GetBook(); // La chiamata è intercettata, si può loggare/modificare il risultato

4. Serializzazione veloce senza reflection

Invece di costruire descrizioni di tipo a runtime tramite reflection (lento), Source Generators possono generare codice di (de)serializzazione in anticipo — il più veloce possibile senza overhead.

1
Sondaggio/quiz
Riflessione, livello 63, lezione 4
Non disponibile
Riflessione
Riflessione e tipi dinamici
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION