1. Introduzione
Quando scrivi codice, probabilmente ti è capitato di pensare: «Perché continuo a copiare lo stesso pattern di codice per diverse classi?» Oppure: «Perché la serializzazione, il logging, il mapping dei dati sono così tante righe ripetitive?» A volte vorresti che qualcuno (o qualcosa) scrivesse quel boilerplate per te.
Ed è qui che entrano in scena i Source Generators — una funzionalità nuova in C#, introdotta in .NET 5 e ancora in evoluzione. Un Source Generator è una libreria che viene eseguita durante la compilazione e può generare dinamicamente codice C# che viene automaticamente incluso nel tuo progetto prima della build finale.
Perché serve?
- Automazione della routine: Ti libera dal scrivere classi/metodi ripetitivi (boilerplate).
- Sicurezza verificata dal compilatore: Il codice generato viene compilato insieme al tuo (a differenza di T4 o reflection).
- Alta performance: Serializzazione, DI, mapping ecc. senza costi di reflection a runtime.
- Supporto per pattern moderni: Implementazione di approcci che sarebbero complessi o costosi senza generazione di codice.
Come funzionano i Source Generators "sotto il cofano"?
Un Source Generator è una libreria .NET (di solito un progetto di tipo Class Library) che implementa l'interfaccia ISourceGenerator. Durante la compilazione Roslyn esegue tutti i generator collegati, fornendo loro accesso all'albero sintattico del tuo codice.
Il generator analizza il tuo codice, decide cosa e dove generare, e crea nuovi file C# che il compilatore subito include nella compilazione.
Generazione automatica di ToString
Cominciamo con qualcosa di semplice. Immaginiamo di avere una classe con molte proprietà e di dover implementare ToString. Scriverlo a mano potrebbe essere così:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public override string ToString()
=> $"Person(Name={Name}, Age={Age})";
}
Ma se le proprietà diventano tante diventa noioso e si rischia di dimenticare di aggiornare qualcosa. Un Source Generator può farlo per te!
2. Come creare il tuo Source Generator?
Creazione del progetto
Apri JetBrains Rider o Visual Studio, crea un nuovo progetto di tipo Class Library (.NET Standard) — sono proprio questi i progetti che possono funzionare come generator. Poi aggiungi i pacchetti NuGet:
- Microsoft.CodeAnalysis.CSharp
- Microsoft.CodeAnalysis.Analyzers
Attributi importanti
- [Generator] — indica che quella classe è un Source Generator.
Template minimo del generator
Ecco un esempio minimo funzionante:
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using System.Text;
[Generator]
public class HelloWorldGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context)
{
// È possibile registrare azioni aggiuntive (opzionale)
}
public void Execute(GeneratorExecutionContext context)
{
var code = @"
namespace Generated
{
public static class HelloWorld
{
public static string SayHello() => ""Privet, mir! Ja sgenierirovan!"";
}
}";
context.AddSource("HelloWorldGenerator", SourceText.From(code, Encoding.UTF8));
}
}
Questo semplice generator aggiunge sempre una classe statica HelloWorld con il metodo SayHello durante la compilazione.
Come usare i Source Generators nell'app principale?
Collega il progetto-generator come pacchetto NuGet o come Project Reference nella sezione Analyzer (per i dettagli — documentazione ufficiale).
Il codice generato è immediatamente disponibile nel tuo progetto — non serve altro, basta usare:
// Questo verrà generato automaticamente!
using Generated;
Console.WriteLine(HelloWorld.SayHello());
3. Esempio reale: generazione automatica di ToString
Supponiamo di voler fare in modo che tutte le classi marcate con l'attributo [AutoToString] ricevano automaticamente l'implementazione del metodo ToString. Per farlo serve:
- Creare il proprio attributo.
- Analizzare tutte le classi con quell'attributo.
- Per ciascuna di queste classi generare il metodo ToString.
Attributo
[AttributeUsage(AttributeTargets.Class)]
public class AutoToStringAttribute : Attribute
{
}
Uso nel codice
[AutoToString]
public class Product
{
public string Name { get; set; }
public int Price { get; set; }
}
Logica semplice di generazione
Il generator cercherà classi con [AutoToString] e genererà più o meno questo codice:
public override string ToString()
=> $"Product(Name={Name}, Price={Price})";
Pezzetto di codice reale del generator
L'idea di base è attraversare l'albero sintattico con Roslyn:
public void Execute(GeneratorExecutionContext context)
{
// Analizziamo tutti gli alberi sintattici
foreach (var tree in context.Compilation.SyntaxTrees)
{
var root = tree.GetRoot();
// Cerchiamo tutte le classi con l'attributo necessario (esempio!)
var classes = root.DescendantNodes()
.OfType<ClassDeclarationSyntax>()
.Where(c => c.AttributeLists
.SelectMany(al => al.Attributes)
.Any(a => a.Name.ToString().Contains("AutoToString")));
foreach (var @class in classes)
{
var className = @class.Identifier.Text;
// Prendiamo tutte le proprietà della classe
var props = @class.Members
.OfType<PropertyDeclarationSyntax>()
.Select(p => p.Identifier.Text)
.ToArray();
var toStringCode = string.Join(", ", props.Select(p => $"{p}={{this.{p}}}"));
var generated = $@"
partial class {className}
{{
public override string ToString() => $""{className}({toStringCode})"";
}}";
context.AddSource($"{className}_ToString", SourceText.From(generated, Encoding.UTF8));
}
}
}
Nota: per codice di produzione si usa un'analisi più corretta tramite il SemanticModel di Roslyn.
4. Dettagli utili
A cosa prestare attenzione
I Source Generators non possono modificare il codice sorgente esistente — possono solo creare nuovi file (per esempio partial class aggiuntive, metodi ecc.). Questo significa che se la tua classe è dichiarata come partial, puoi generare metodi o proprietà aggiuntive per essa.
A volte è complicato analizzare correttamente la sintassi e considerare tutte le sottigliezze del linguaggio (classi annidate, generici, modifier ecc.). L'autore del generator deve assicurarsi che il codice generato sia compilabile e non "rompa" il progetto.
Un'altra trappola: se generi metodi che implementano un'interfaccia, assicurati che i file vengano generati ad ogni build. Altrimenti possono comparire errori strani di compilazione. Gli strumenti moderni risolvono spesso questi problemi, ma è bene tenerne conto.
Source Generators vs reflection
Reflection: eseguita a runtime, costosa in termini di risorse, non verificata dal compilatore, spesso lenta su grandi volumi di dati.
Source Generator: genera codice a compile-time. Tutto viene verificato staticamente, l'IDE vede i metodi, funziona l'autocomplete, e le prestazioni sono quelle del codice C# normale.
Utilità pratica
- System.Text.Json: generazione di serializzazione/deserializzazione senza reflection.
- Progettazione di DI-container: per esempio Microsoft.Extensions.DependencyInjection con generazione del grafo delle dipendenze.
- Mapper come Mapster: passaggio da reflection a mapping code generato a compile-time.
- Framework di test: autogenerazione di test method basati su attributi.
- ASP.NET Minimal APIs (da .NET 7): generazione di endpoint handler.
Configurazione, parametri e opzioni
I generator possono essere configurati tramite parametri MSBuild, file aggiuntivi e convenzioni. Puoi, per esempio, generare diversi ToString a seconda dell'ambiente (Debug/Release) o della configurazione dell'app.
Come i Source Generators si collegano a problemi reali
Per lo sviluppatore che punta a codice pulito e veloce, è uno strumento eccellente: meno routine, più verifiche a compile-time, suggerimenti dell'IDE e refactoring chiaro. La conoscenza dei generator è sempre più richiesta nei colloqui — dalla serializzazione e DI al mapping.
Ciclo di vita di un Source Generator
| Fase | Cosa succede |
|---|---|
| 1. Progetto collegato | Il tuo generator è aggiunto come analyzer/reference |
| 2. Roslyn compila il sorgente | Il generator riceve l'AST (albero sintattico astratto) |
| 3. Il generator viene eseguito | Aggiunge nuovi file .cs alla compilazione |
| 4. Tutto viene compilato | I file generati diventano parte dell'assembly |
| 5. Il codice è pronto! | I metodi/classi generate sono disponibili per l'uso |
5. Debugging e errori tipici
Uno degli errori più frequenti per i principianti che scrivono generator è dimenticare la parola chiave partial nella classe a cui vuoi aggiungere codice. Se non metti partial, il compilatore semplicemente non vedrà le tue aggiunte. A volte il file generato potrebbe non essere riconosciuto dall'IDE fino alla prima ricompilazione — non spaventarti.
Stai attento alla nomenclatura dei file generati: se dai a tutti lo stesso Name, si sovrascriveranno a vicenda. Un trucco utile: mettere nel nome del file il nome della classe per cui stai generando il codice — per esempio context.AddSource($"{className}_ToString", ...).
Errore di import doppio di attributi — se generi una classe con un attributo che esiste già nel progetto principale, si avrà un conflitto. È meglio spostare gli attributi necessari in un progetto comune o generare il codice solo quando serve.
GO TO FULL VERSION