Generatori di origine

Questo articolo offre una panoramica dei generatori di origine forniti come parte dell'SDK di .NET Compiler Platform ("Roslyn"). I generatori di origine consentono agli sviluppatori C# di esaminare il codice utente durante la compilazione. Il generatore può creare nuovi file di origine C# in tempo reale che vengono aggiunti alla compilazione dell'utente. In questo modo, si dispone di codice che viene eseguito durante la compilazione. Controlla il programma per produrre file di origine aggiuntivi compilati insieme al resto del codice.

Un generatore di origine è un nuovo tipo di componente che gli sviluppatori C# possono scrivere che consentono di eseguire due operazioni principali:

  1. Recuperare un oggetto di compilazione che rappresenta tutto il codice utente che viene compilato. Questo oggetto può essere esaminato ed è possibile scrivere codice che funziona con la sintassi e i modelli semantici per il codice compilato, proprio come con gli analizzatori.

  2. Generare file di origine C# che possono essere aggiunti a un oggetto di compilazione durante la compilazione. In altre parole, è possibile fornire codice sorgente aggiuntivo come input a una compilazione durante la compilazione del codice.

In combinazione, questi due elementi sono ciò che rende i generatori di origine così utili. È possibile esaminare il codice utente con tutti i metadati avanzati compilati dal compilatore durante la compilazione. Il generatore genera quindi il codice C# nella stessa compilazione basata sui dati analizzati. Se si ha familiarità con Gli analizzatori Roslyn, è possibile considerare generatori di origine come analizzatori in grado di generare codice sorgente C#.

I generatori di origine vengono eseguiti come fase di compilazione visualizzata di seguito:

Immagine che descrive le diverse parti della generazione di origine

Un generatore di origine è un assembly .NET Standard 2.0 caricato dal compilatore insieme a qualsiasi analizzatore. È utilizzabile negli ambienti in cui i componenti .NET Standard possono essere caricati ed eseguiti.

Importante

Attualmente solo gli assembly .NET Standard 2.0 possono essere usati come generatori di origine.

Scenari comuni

Esistono tre approcci generali per esaminare il codice utente e generare informazioni o codice in base a tale analisi usata dalle tecnologie attualmente:

  • Reflection di runtime.
  • Eseguire il juggling delle attività di MSBuild.
  • Tessitura del linguaggio intermedio (IL) (non descritta in questo articolo).

I generatori di origine possono essere un miglioramento rispetto a ogni approccio.

Reflection di runtime

La reflection di runtime è una tecnologia potente che è stata aggiunta a .NET molto tempo fa. Esistono innumerevoli scenari per usarlo. Uno scenario comune consiste nell'eseguire alcune analisi del codice utente all'avvio e all'uso di tali dati per generare elementi.

Ad esempio, ASP.NET Core usa la reflection quando il servizio Web viene eseguito per la prima volta per individuare i costrutti definiti in modo che possa "collegare" elementi come controller e pagine razor. Anche se ciò consente di scrivere codice semplice con astrazioni potenti, comporta una riduzione delle prestazioni in fase di esecuzione: quando il servizio Web o l'app viene avviata per la prima volta, non può accettare richieste fino a quando non viene completato l'esecuzione di tutto il codice di reflection di runtime che individua informazioni sul codice. Anche se questa riduzione delle prestazioni non è enorme, si tratta di un costo fisso che non è possibile migliorare se stessi nella propria app.

Con un generatore di origine, la fase di individuazione del controller di avvio potrebbe invece verificarsi in fase di compilazione. Un generatore può analizzare il codice sorgente e generare il codice necessario per collegare l'app. L'uso dei generatori di origine potrebbe comportare tempi di avvio più rapidi, perché un'azione in fase di esecuzione può essere inserita in fase di compilazione.

Eseguire il juggling delle attività di MSBuild

I generatori di origine possono migliorare le prestazioni in modi non limitati alla reflection in fase di esecuzione per individuare anche i tipi. Alcuni scenari implicano la chiamata dell'attività C# di MSBuild (denominata CSC) più volte in modo da poter esaminare i dati da una compilazione. Come si può immaginare, la chiamata al compilatore più volte influisce sul tempo totale necessario per compilare l'app. Stiamo esaminando il modo in cui i generatori di origine possono essere usati per evitare la necessità di destreggiare le attività di MSBuild in questo modo, poiché i generatori di origine non offrono solo alcuni vantaggi in termini di prestazioni, ma consentono anche agli strumenti di operare al livello corretto di astrazione.

Un'altra funzionalità che i generatori di origine possono offrire è l'obviazione dell'uso di alcune API "tipizzate in modo stringa", ad esempio il funzionamento del routing ASP.NET Core tra controller e pagine razor. Con un generatore di origine, il routing può essere fortemente tipizzato con le stringhe necessarie generate come dettaglio in fase di compilazione. In questo modo si riduce il numero di volte in cui un valore letterale stringa tipizzato non genera una richiesta che non raggiunge il controller corretto.

Introduzione ai generatori di origine

In questa guida si esaminerà la creazione di un generatore di origine usando l'API ISourceGenerator .

  1. Creare un'applicazione console .NET. Questo esempio usa .NET 6.

  2. Sostituire la classe Program con il codice seguente. Il codice seguente non usa istruzioni di primo livello. Il modulo classico è obbligatorio perché il primo generatore di origine scrive un metodo parziale in tale Program classe:

    namespace ConsoleApp;
    
    partial class Program
    {
        static void Main(string[] args)
        {
            HelloFrom("Generated Code");
        }
    
        static partial void HelloFrom(string name);
    }
    

    Nota

    È possibile eseguire questo esempio così come è, ma non succederà ancora nulla.

  3. Verrà quindi creato un progetto generatore di origine che implementerà la controparte del partial void HelloFrom metodo.

  4. Creare un progetto di libreria .NET Standard destinato al netstandard2.0 moniker del framework di destinazione (TFM). Aggiungere i pacchetti NuGet Microsoft.CodeAnalysis.Analyzers e Microsoft.CodeAnalysis.CSharp:

    <Project Sdk="Microsoft.NET.Sdk">
    
      <PropertyGroup>
        <TargetFramework>netstandard2.0</TargetFramework>
      </PropertyGroup>
    
      <ItemGroup>
        <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" PrivateAssets="all" />
        <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
      </ItemGroup>
    
    </Project>
    

    Suggerimento

    Il progetto generatore di origine deve avere come destinazione il netstandard2.0 tfm, altrimenti non funzionerà.

  5. Creare un nuovo file C# denominato HelloSourceGenerator.cs che specifica il proprio generatore di origine in questo modo:

    using Microsoft.CodeAnalysis;
    
    namespace SourceGenerator
    {
        [Generator]
        public class HelloSourceGenerator : ISourceGenerator
        {
            public void Execute(GeneratorExecutionContext context)
            {
                // Code generation goes here
            }
    
            public void Initialize(GeneratorInitializationContext context)
            {
                // No initialization required for this one
            }
        }
    }
    

    Un generatore di origine deve implementare l'interfaccia Microsoft.CodeAnalysis.ISourceGenerator e avere .Microsoft.CodeAnalysis.GeneratorAttribute Non tutti i generatori di origine richiedono l'inizializzazione e questo è il caso di questa implementazione di esempio, dove ISourceGenerator.Initialize è vuoto.

  6. Sostituire il contenuto del ISourceGenerator.Execute metodo con l'implementazione seguente:

    using Microsoft.CodeAnalysis;
    
    namespace SourceGenerator
    {
        [Generator]
        public class HelloSourceGenerator : ISourceGenerator
        {
            public void Execute(GeneratorExecutionContext context)
            {
                // Find the main method
                var mainMethod = context.Compilation.GetEntryPoint(context.CancellationToken);
    
                // Build up the source code
                string source = $@"// <auto-generated/>
    using System;
    
    namespace {mainMethod.ContainingNamespace.ToDisplayString()}
    {{
        public static partial class {mainMethod.ContainingType.Name}
        {{
            static partial void HelloFrom(string name) =>
                Console.WriteLine($""Generator says: Hi from '{{name}}'"");
        }}
    }}
    ";
                var typeName = mainMethod.ContainingType.Name;
    
                // Add the source code to the compilation
                context.AddSource($"{typeName}.g.cs", source);
            }
    
            public void Initialize(GeneratorInitializationContext context)
            {
                // No initialization required for this one
            }
        }
    }
    

    Dall'oggetto context è possibile accedere al punto di ingresso o Main al metodo delle compilazioni. L'istanza mainMethod è un IMethodSymbologgetto e rappresenta un metodo o un simbolo simile al metodo (incluso costruttore, distruttore, operatore o funzione di accesso property/event). Il Microsoft.CodeAnalysis.Compilation.GetEntryPoint metodo restituisce l'oggetto IMethodSymbol per il punto di ingresso del programma. Altri metodi consentono di trovare qualsiasi simbolo di metodo in un progetto. Da questo oggetto è possibile ragionare sullo spazio dei nomi contenitore (se presente) e sul tipo . In source questo esempio è una stringa interpolata che modelli il codice sorgente da generare, in cui i fori interpolati vengono riempiti con lo spazio dei nomi e le informazioni sul tipo contenenti. L'oggetto sourcecontext viene aggiunto a con un nome di hint. Per questo esempio, il generatore crea un nuovo file di origine generato che contiene un'implementazione del partial metodo nell'applicazione console. È possibile scrivere generatori di origine per aggiungere qualsiasi origine desiderata.

    Suggerimento

    Il hintName parametro del GeneratorExecutionContext.AddSource metodo può essere qualsiasi nome univoco. È comune fornire un'estensione di file C# esplicita, ".g.cs" ad esempio o ".generated.cs" per il nome. Il nome del file consente di identificare il file come generato dall'origine.

  7. A questo punto è disponibile un generatore funzionante, ma è necessario connetterlo all'applicazione console. Modificare il progetto di applicazione console originale e aggiungere quanto segue, sostituendo il percorso del progetto con quello del progetto .NET Standard creato in precedenza:

    <!-- Add this as a new ItemGroup, replacing paths and names appropriately -->
    <ItemGroup>
        <ProjectReference Include="..\PathTo\SourceGenerator.csproj"
                          OutputItemType="Analyzer"
                          ReferenceOutputAssembly="false" />
    </ItemGroup>
    

    Questo nuovo riferimento non è un riferimento al progetto tradizionale e deve essere modificato manualmente per includere gli OutputItemType attributi e ReferenceOutputAssembly . Per altre informazioni sugli OutputItemType attributi e ReferenceOutputAssembly di , vedere Elementi comuni delProjectReference progetto MSBuild: ProjectReference.

  8. Ora, quando si esegue l'applicazione console, si noterà che il codice generato viene eseguito e stampato sullo schermo. L'applicazione console stessa non implementa il HelloFrom metodo , ma l'origine viene generata durante la compilazione dal progetto Generatore di origine. Il testo seguente è un output di esempio dell'applicazione:

    Generator says: Hi from 'Generated Code'
    

    Nota

    Potrebbe essere necessario riavviare Visual Studio per visualizzare IntelliSense e sbarazzarsi degli errori man mano che l'esperienza di strumenti è in fase di miglioramento attivo.

  9. Se si usa Visual Studio, è possibile visualizzare i file generati dall'origine. Nella finestra Esplora soluzioni espandere Dependencies>Analyzers>SourceGenerator SourceGenerator.HelloSourceGenerator> e fare doppio clic sul file Program.g.cs.

    Visual Studio: Esplora soluzioni file generati dall'origine.

    Quando si apre questo file generato, Visual Studio indicherà che il file viene generato automaticamente e che non può essere modificato.

    Visual Studio: file Program.g.cs generato automaticamente.

  10. È anche possibile impostare le proprietà di compilazione per salvare il file generato e controllare dove vengono archiviati i file generati. Nel file di progetto dell'applicazione console aggiungere l'elemento a un <PropertyGroup>oggetto e impostarne il <EmitCompilerGeneratedFiles> valore su true. Compilare di nuovo il progetto. I file generati vengono ora creati in obj/Debug/net6.0/generated/SourceGenerator/SourceGenerator.HelloSourceGenerator. I componenti del percorso sono mappati alla configurazione della compilazione, al framework di destinazione, al nome del progetto del generatore di origine e al nome completo del tipo del generatore. È possibile scegliere una cartella di output più comoda aggiungendo l'elemento <CompilerGeneratedFilesOutputPath> al file di progetto dell'applicazione.

Passaggi successivi

Il Cookbook generatori di origine passa su alcuni di questi esempi con alcuni approcci consigliati per risolverli. Inoltre, è disponibile un set di esempi in GitHub che è possibile provare autonomamente.

Per altre informazioni sui generatori di origine, vedere questi articoli: