CodeGym /Corsi /C# SELF /Introduzione a Source Gene...

Introduzione a Source Generators

C# SELF
Livello 63 , Lezione 3
Disponibile

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.

2
Compito
C# SELF, livello 63, lezione 3
Bloccato
Generazione automatica del metodo ToString
Generazione automatica del metodo ToString
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION