Quellgeneratoren

Dieser Artikel bietet eine Übersicht über Quellcode-Generatoren, die als Teil des .NET Compiler Platform („Roslyn“) SDK bereitgestellt werden. Quellgeneratoren ermöglichen C#-Entwicklern, Benutzercode während der Kompilierung zu überprüfen. Der Generator kann im laufenden Betrieb neue C#-Quelldateien erstellen, die der Kompilierung des Benutzers hinzugefügt werden. Auf diese Weise verfügen Sie über Code, der während der Kompilierung ausgeführt wird. Er untersucht Ihr Programm, um zusätzliche Quelldateien zu erzeugen, die zusammen mit dem Rest Ihres Codes kompiliert werden.

Ein Quellcode-Generator ist eine neue Art von Komponente, die C#-Entwickler schreiben und mit der Sie zwei wichtige Aufgaben ausführen können:

  1. Abrufen eines Kompilierungsobjekts, das den gesamten Benutzercode darstellt, der gerade kompiliert wird. Dieses Objekt kann untersucht werden, und Sie können Code schreiben, der mit den Syntax- und semantischen Modellen für den zu kompilierenden Code arbeitet, genau wie bei heutigen Analysetools.

  2. Generieren Sie C#-Quelldateien, die einem Kompilierungsobjekt während der Kompilierung hinzugefügt werden können. Mit anderen Worten können Sie zusätzlichen Quellcode als Eingabe für eine Kompilierung bereitstellen, während der Code kompiliert wird.

In der Kombination sind es diese beiden Aspekte, die Quellcode-Generatoren so nützlich machen. Sie können den Benutzercode mit allen umfassenden Metadaten untersuchen, die der Compiler während der Kompilierung erstellt. Ihr Generator gibt dann wieder C#-Code in dieselbe Kompilierung aus, die auf den analysierten Daten basiert. Wenn Sie mit Roslyn-Analysetools vertraut sind, können Sie sich Quellcode-Generatoren als Analysetools vorstellen, die C#-Quellcode ausgeben können.

Quellcode-Generatoren werden in einer unten dargestellten Kompilierungsphase ausgeführt:

Grafik mit Beschreibung der verschiedenen Teile der Quellcodegenerierung

Ein Quellcode-Generator ist eine .NET Standard 2.0-Assembly, die vom Compiler zusammen mit allen Analysetools geladen wird. Er lässt sich in Umgebungen einsetzen, in denen .NET Standard-Komponenten geladen und ausgeführt werden können.

Wichtig

Derzeit können nur .NET Standard 2.0-Assemblys als Quellcode-Generatoren verwendet werden.

Häufige Szenarien

Es gibt drei allgemeine Ansätze, um Benutzercode zu überprüfen und Informationen oder Code basierend auf dieser Analyse zu generieren, die von den heutigen Technologien verwendet werden:

  • Reflexion zur Laufzeit
  • Jonglieren mit MSBuild-Aufgaben
  • IL Weaving (Intermediate Language, in diesem Artikel nicht erläutert)

Quellcode-Generatoren können eine Verbesserung gegenüber den einzelnen Ansätzen sein.

Reflexion zur Laufzeit

Die Reflexion zur Laufzeit ist eine leistungsstarke Technologie, die .NET vor langer Zeit hinzugefügt wurde. Es gibt unzählige Szenarien für ihren Einsatz. Ein gängiges Szenario ist die Analyse des Benutzercodes beim Starten einer App und die Verwendung dieser Daten, um etwas zu generieren.

ASP.NET Core verwendet beispielsweise Reflexion, wenn Ihr Webdienst zum ersten Mal ausgeführt wird, um von Ihnen definierte Konstrukte zu erkennen, damit er Elemente wie Controller und Razor Pages miteinander verbinden kann. Sie können auf diese Weise zwar einfachen Code mit leistungsstarken Abstraktionen schreiben, aber dies geht zur Runtime mit einer Leistungsbeeinträchtigung einher: Wenn Ihr Webdienst oder Ihre App zum ersten Mal gestartet wird, kann er/sie keine Anforderungen empfangen, bis der gesamte Code für die Reflexion zur Runtime, der Informationen über Ihren Code ermittelt, ausgeführt wurde. Obwohl diese Leistungseinbuße nicht enorm ist, handelt es sich um eine Art Fixkosten, die Sie in Ihrer eigenen App nicht selbst beeinflussen können.

Bei einem Quellgenerator kann die Controllerermittlungsphase des Startvorgangs stattdessen zur Kompilierungszeit erfolgen. Ein Generator kann Ihren Quellcode analysieren und den Code ausgeben, der zum Optimieren Ihrer App benötigt wird. Die Verwendung von Quellgeneratoren kann zu schnelleren Startzeiten führen, da eine Aktion, die heute zur Runtime stattfindet, in die Kompilierzeit verlagert werden kann.

Jonglieren mit MSBuild-Aufgaben

Quellcode-Generatoren können die Leistung auf eine Weise verbessern, die sich nicht auf Reflexion zur Laufzeit beschränkt, um auch Typen zu ermitteln. In einigen Szenarien wird die MSBuild-C#-Aufgabe (genannt CSC) mehrfach aufgerufen, um Daten aus einer Kompilierung zu prüfen. Wie Sie sich vorstellen können, wirkt sich der mehrfache Aufruf des Compilers auf die Gesamtzeit aus, die für die Erstellung Ihrer App benötigt wird. Wir untersuchen, wie Quellcode-Generatoren eingesetzt werden können, um das Jonglieren mit MSBuild-Aufgaben wie dieser zu vermeiden, da Quellcode-Generatoren nicht nur einige Leistungsvorteile bieten, sondern es auch Tools ermöglichen, auf der richtigen Abstraktionsebene zu arbeiten.

Eine weitere Fähigkeit von Quellcode-Generatoren ist die Vermeidung der Verwendung einiger APIs des Typs „String“, z. B. wie das ASP.NET Core-Routing zwischen Controllern und Razor Pages funktioniert. Mit einem Quellcode-Generator kann das Routing stark typisiert werden, wobei die erforderlichen Zeichenfolgen als Detail zur Kompilierzeit generiert werden. Dies würde die Anzahl der Fälle reduzieren, in denen eine falsch eingegebene Zeichenfolge dazu führt, dass eine Anforderung nicht den richtigen Controller erreicht.

Erste Schritte mit Quellcode-Generatoren

In diesem Leitfaden untersuchen Sie die Erstellung eines Quellcode-Generators mithilfe der ISourceGenerator-API.

  1. Erstellen einer .NET-Konsolenanwendung In diesem Beispiel wird .NET 6 verwendet.

  2. Ersetzen Sie die Program -Klasse durch den folgenden Code. Der folgende Code verwendet keine Anweisungen der obersten Ebene. Das klassische Formular ist erforderlich, da dieser erste Quellgenerator eine partielle Methode in diese Program-Klasse schreibt:

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

    Hinweis

    Sie können dieses Beispiel so ausführen, wie es ist, aber es wird noch nichts passieren.

  3. Als Nächstes erstellen wir ein Quellcode-Generatoren-Projekt, das das Gegenstück zur Methode partial void HelloFrom implementiert.

  4. Erstellen Sie ein .NET-Standardbibliotheksprojekt, das auf den netstandard2.0-Zielframeworkmoniker (Target Framework Moniker, TFM) abzielt. Fügen Sie die NuGet-Pakete Microsoft.CodeAnalysis.Analyzers und Microsoft.CodeAnalysis.CSharp hinzu:

    <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>
    

    Tipp

    Das Quellcode-Generatoren-Projekt muss auf den netstandard2.0-Zielframeworkmoniker ausgerichtet sein, andernfalls funktioniert es nicht.

  5. Erstellen Sie eine neue C#-Datei mit dem Namen HelloSourceGenerator.cs, die Ihren eigenen Quellcode-Generator wie folgt angibt:

    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
            }
        }
    }
    

    Ein Quellcode-Generator muss sowohl die Microsoft.CodeAnalysis.ISourceGenerator-Schnittstelle implementieren als auch über Microsoft.CodeAnalysis.GeneratorAttribute verfügen. Nicht alle Quellcode-Generatoren erfordern eine Initialisierung, und dies gilt auch für die vorliegende Beispielimplementierung, bei der ISourceGenerator.Initialize leer ist.

  6. Ersetzen Sie den Inhalt der ISourceGenerator.Execute-Methode durch die folgende Implementierung:

    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
            }
        }
    }
    

    Über das context-Objekt können wir auf den Einstiegspunkt der Kompilierung zugreifen, also auf die Main-Methode. Die mainMethod-Instanz ist ein IMethodSymbol und steht für eine Methode oder ein methodenähnliches Symbol (einschließlich Konstruktor, Destruktor, Operator oder Eigenschafts-/Ereignis-Accessor). Die Microsoft.CodeAnalysis.Compilation.GetEntryPoint-Methode gibt das IMethodSymbol für den Einstiegspunkt des Programms zurück. Mit anderen Methoden können Sie beliebige Methodensymbole in einem Projekt finden. Anhand dieses Objekts können wir Rückschlüsse auf den übergeordneten Namespace (falls vorhanden) und den Typ ziehen. source ist in diesem Beispiel eine interpolierte Zeichenfolge, die den zu generierenden Quellcode vorgibt, wobei die interpolierten Löcher mit Informationen zum übergeordneten Namespace und mit Typinformationen gefüllt werden. source wird context mit einem Hinweisnamen hinzugefügt. In diesem Beispiel erstellt der Generator eine neu generierte Quelldatei, die eine Implementierung der partial-Methode in der Konsolenanwendung enthält. Sie können Quellgeneratoren schreiben, um beliebige Quellen hinzuzufügen.

    Tipp

    Der Parameter hintName der Methode GeneratorExecutionContext.AddSource kann ein beliebiger eindeutiger Name sein. Es ist üblich, eine explizite C#-Dateierweiterung wie ".g.cs" oder ".generated.cs" als Namen anzugeben. Anhand des Dateinamens ist erkennbar, dass es sich um eine aus dem Quellcode generierte Datei handelt.

  7. Wir verfügen nun über einen funktionierenden Generator, müssen ihn aber mit unserer Konsolenanwendung verbinden. Bearbeiten Sie das ursprüngliche Konsolenanwendungsprojekt, und fügen Sie Folgendes hinzu. Ersetzen Sie dabei den Projektpfad durch den Pfad im .NET Standard-Projekt, das Sie zuvor erstellt haben:

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

    Weil dieser neue Verweis kein herkömmlicher Projektverweis ist, muss er manuell bearbeitet werden, um die Attribute OutputItemType und ReferenceOutputAssembly aufzunehmen. Weitere Informationen zu den Attributen OutputItemType und ReferenceOutputAssembly von ProjectReference finden Sie unter Gemeinsame MSBuild-Projektelemente: ProjectReference.

  8. Wenn Sie nun die Konsolenanwendung ausführen, sollten Sie sehen, dass der generierte Code ausgeführt und auf dem Bildschirm ausgegeben wird. Die Konsolenanwendung selbst implementiert die HelloFrom-Methode nicht, sondern ist die Quelle, die während der Kompilierung aus dem Quellcode-Generator-Projekt generiert wird. Der folgende Text ist eine Beispielausgabe der Anwendung:

    Generator says: Hi from 'Generated Code'
    

    Hinweis

    Sie müssen Visual Studio möglicherweise neu starten, um IntelliSense anzuzeigen und Fehlerkorrekturen zu erhalten, da die Toolfunktionalität kontinuierlich überarbeitet wird.

  9. Wenn Sie Visual Studio verwenden, werden die aus dem Quellcode generierten Dateien angezeigt. Erweitern Sie im Fenster Projektmappen-Explorer nacheinander Abhängigkeiten>Analysetools>SourceGenerator>SourceGenerator.HelloSourceGenerator, und doppelklicken Sie auf die Datei Program.g.cs.

    Visual Studio: Aus dem Quellcode generierte Dateien im Projektmappen-Explorer

    Wenn Sie diese generierte Datei öffnen, weist Visual Studio darauf hin, dass die Datei automatisch generiert wurde und nicht bearbeitet werden kann.

    Visual Studio: Automatisch generierte Datei „Program.g.cs“

  10. Sie können auch Buildeigenschaften festlegen, um die generierte Datei zu speichern und zu steuern, wo die generierten Dateien gespeichert werden. Fügen Sie in der Projektdatei der Konsolenanwendung das <EmitCompilerGeneratedFiles>-Element einer <PropertyGroup> hinzu, und legen Sie seinen Wert auf true fest. Erstellen Sie das Projekt neu. Nun werden die generierten Dateien unter obj/Debug/net6.0/generated/SourceGenerator/SourceGenerator.HelloSourceGenerator erstellt. Die Komponenten des Pfads sind der Buildkonfiguration, dem Zielframework, dem Projektnamen des Quellgenerators und dem vollqualifizierten Typnamen des Generators zugeordnet. Sie können einen praktischeren Ausgabeordner auswählen, indem Sie das <CompilerGeneratedFilesOutputPath>-Element der Projektdatei der Anwendung hinzufügen.

Nächste Schritte

Im Source Generators Cookbook werden einige dieser Beispiele mit empfohlenen Lösungsansätzen behandelt. Zusätzlich bieten wir eine Reihe von Beispielen auf GitHub, die Sie selbst ausprobieren können.

Weitere Informationen zu Quellgeneratoren finden Sie in diesen Artikeln: