CodeGym /Kurse /C# SELF /Einführung in Source Gener...

Einführung in Source Generators

C# SELF
Level 63 , Lektion 3
Verfügbar

1. Einführung

Wenn du Code schreibst, hast du dich sicher schon manchmal erwischt bei dem Gedanken: „Warum kopiere ich ständig dasselbe Code-Pattern für verschiedene Klassen?“ Oder: „Warum sind Serialization, Logging, Data-Mapping so viele sich wiederholende Zeilen?“ Manchmal wünscht man sich, dass jemand (oder etwas) diese mühsame Boilerplate für dich schreibt.

Hier kommen die Source Generators ins Spiel — eine neue Möglichkeit in C#, eingeführt in .NET 5 und seitdem aktiv weiterentwickelt. Ein Source Generator ist eine Bibliothek, die zur Compile-Zeit läuft und dynamisch C#-Code erzeugen kann, der vor der finalen Build in dein Projekt eingebunden wird.

Wozu das gut ist?

  • Automatisierung von Routine: Spart das Schreiben sich wiederholender Klassen/Methoden (boilerplate).
  • Vom Compiler überprüfbare Sicherheit: Der generierte Code wird zusammen mit deinem kompiliert (im Gegensatz zu T4 oder Reflection).
  • Hohe Performance: Serialization, DI, Mapping u.ä. ohne Reflection-Kosten zur Laufzeit.
  • Unterstützung moderner Patterns: Umsetzung von Ansätzen, die ohne Code-Generierung kompliziert oder teuer wären.

Wie arbeiten Source Generators „unter der Haube“?

Ein Source Generator ist eine .NET-Bibliothek (meist ein Projekt vom Typ Class Library), die das Interface ISourceGenerator implementiert. Während der Kompilierung startet Roslyn alle registrierten Generatoren und gibt ihnen Zugriff auf den Syntaxbaum deines Codes.

Der Generator analysiert deinen Code, entscheidet, was und wo generiert werden soll, und erstellt neue C#-Dateien, die der Compiler sofort kompiliert.

Automatische Generierung von ToString

Fangen wir einfach an. Stell dir vor, wir haben eine Klasse mit vielen Properties und müssen ToString implementieren. Handgeschrieben sieht das so aus:

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public override string ToString()
        => $"Person(Name={Name}, Age={Age})";
}

Wenn die Anzahl der Properties sehr groß wird, ist das lästig und man kann leicht vergessen, etwas zu aktualisieren. Ein Source Generator kann das für dich erledigen!

2. Wie erstellt man einen eigenen Source Generator?

Projekt erstellen

Öffne JetBrains Rider oder Visual Studio, erstelle ein neues Projekt vom Typ Class Library (.NET Standard) — genau solche Projekte eignen sich als Generatoren. Füge dann die NuGet-Pakete hinzu:

  • Microsoft.CodeAnalysis.CSharp
  • Microsoft.CodeAnalysis.Analyzers

Wichtige Attribute

  • [Generator] — markiert die Klasse als Source Generator.

Minimaler Generator-Template

Hier ein minimales funktionierendes Beispiel:

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using System.Text;

[Generator]
public class HelloWorldGenerator : ISourceGenerator
{
    public void Initialize(GeneratorInitializationContext context)
    {
        // Man kann zusätzliche Aktionen registrieren (optional)
    }

    public void Execute(GeneratorExecutionContext context)
    {
        var code = @"
namespace Generated
{
    public static class HelloWorld
    {
        public static string SayHello() => ""Privet, mir! Ja sgenerezovan!"";
    }
}";
        context.AddSource("HelloWorldGenerator", SourceText.From(code, Encoding.UTF8));
    }
}

Dieser einfache Generator fügt zur Kompilierzeit immer die statische Klasse HelloWorld mit der Methode SayHello hinzu.

Wie verwendet man Source Generators im Hauptprojekt?

Binde das Generator-Projekt als NuGet-Paket oder als Project Reference in der Analyzer-Sektion ein (mehr dazu in der offiziellen Dokumentation).

Der generierte Code ist sofort im Projekt verfügbar — du musst nichts weiter einbinden, einfach verwenden:

// Das wird automatisch generiert!
using Generated;

Console.WriteLine(HelloWorld.SayHello());

3. Reales Beispiel: automatische Generierung von ToString

Angenommen, wir wollen, dass alle Klassen, die mit dem Attribut [AutoToString] markiert sind, automatisch eine Implementierung der Methode ToString erhalten. Dazu brauchen wir:

  • Ein eigenes Attribut.
  • Analyse aller Klassen mit diesem Attribut.
  • Für jede gefundene Klasse die Generierung der Methode ToString.

Attribut

[AttributeUsage(AttributeTargets.Class)]
public class AutoToStringAttribute : Attribute
{
}

Verwendung im Code

[AutoToString]
public class Product
{
    public string Name { get; set; }
    public int Price { get; set; }
}

Einfache Generierungslogik

Der Generator sucht Klassen mit [AutoToString] und generiert ungefähr folgenden Code:

public override string ToString() 
    => $"Product(Name={Name}, Price={Price})";

Ein Stück realer Generator-Code

Die Grundidee ist das Traversieren des Syntaxbaums mit Roslyn:

public void Execute(GeneratorExecutionContext context)
{
    // Wir analysieren alle SyntaxTrees
    foreach (var tree in context.Compilation.SyntaxTrees)
    {
        var root = tree.GetRoot();
        // Wir suchen alle Klassen mit dem gewünschten Attribut (grob!)
        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;
            // Alle Properties der Klasse holen
            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));
        }
    }
}

Beachte: Für Produktionscode verwendet man eine präzisere Analyse über das SemanticModel von Roslyn.

4. Nützliche Feinheiten

Worauf man achten sollte

Source Generators können vorhandenen Quellcode nicht modifizieren — sie können nur neue Dateien erstellen (z. B. zusätzliche partial-Klassen, Methoden etc.). Das bedeutet: Wenn deine Klasse als partial deklariert ist, kannst du zusätzliche Methoden oder Properties dafür generieren.

Manchmal ist es schwierig, die Syntax korrekt zu parsen und alle Sprachnuancen zu berücksichtigen (verschachtelte Klassen, Generics, Modifier usw.). Der Autor des Generators muss darauf achten, dass der generierte Code kompilierbar ist und das Projekt nicht „bricht“.

Eine weitere Falle: Wenn du Methoden generierst, die ein Interface implementieren, stell sicher, dass die Dateien bei jedem Build neu erzeugt werden. Ansonsten können merkwürdige Kompilationsfehler auftreten. Moderne Tools lösen viele dieser Probleme, aber daran zu denken ist wichtig.

Source Generators vs. Reflection

Reflection: wird zur Laufzeit verwendet, ist ressourcenintensiv, nicht vom Compiler geprüft und oft langsam bei großen Datenmengen.

Source Generator: generiert Code zur Compile-Zeit. Alles wird statisch geprüft, die IDE sieht die Methoden, Autocomplete funktioniert und die Performance entspricht normalem C#-Code.

Praktischer Nutzen

  • System.Text.Json: Generierung von Serialization/Deserialization ohne Reflection.
  • Design von DI-Containern: z. B. Microsoft.Extensions.DependencyInjection mit Generierung des Dependency-Graphs.
  • Mapper wie Mapster: Wechsel von Reflection zu compile-time generiertem Mapping-Code.
  • Test-Frameworks: Autogenerierung von Test-Methoden basierend auf Attributen.
  • ASP.NET Minimal APIs (ab .NET 7): Generierung von Endpoint-Handlern.

Konfiguration, Parameter und Optionen

Generatoren lassen sich über MSBuild-Parameter, zusätzliche Dateien und Konventionen konfigurieren. Du kannst z. B. verschiedenes ToString abhängig von der Umgebung (Debug/Release) oder App-Konfiguration generieren.

Wie Source Generators mit realen Aufgaben zusammenhängen

Für Entwickler, die sauberen und performanten Code wollen, ist das ein tolles Werkzeug: weniger Boilerplate, mehr Compile-Time-Checks, IDE-Unterstützung und klarer Refactoring-Pfad. Kenntnisse über Generatoren werden immer öfter in Interviews verlangt — von Serialization und DI bis hin zu Mapping.

Lebenszyklus eines Source Generators

Phase Was passiert
1. Projekt eingebunden Dein Generator ist als analyzer/reference hinzugefügt
2. Roslyn kompiliert den Quellcode Der Generator erhält das AST (Abstract Syntax Tree)
3. Generator wird ausgeführt Fügt neue .cs-Dateien zur Kompilierung hinzu
4. Alles wird kompiliert Die generierten Dateien werden Teil deiner Assembly
5. Code ist ready! Generierte Methoden/Klassen sind aufrufbar

5. Debugging und typische Fehler

Einer der häufigsten Fehler bei Anfängern ist, das Schlüsselwort partial an der Klasse zu vergessen, in die du Code ergänzen willst. Ohne partial sieht der Compiler deine Ergänzungen nicht. Manchmal wird die generierte Datei von der IDE erst nach einem Rebuild erkannt — keine Panik.

Achte auf die Benennung generierter Dateien: Wenn du allen denselben Namen gibst, überschreiben sie sich gegenseitig. Life-Hack: Verwende den Klassennamen im Dateinamen, z. B. context.AddSource($"{className}_ToString", ...).

Ein Fehler mit doppelten Attribut-Importen kann auftreten — wenn du eine Klasse oder ein Attribut generierst, das bereits im Hauptprojekt existiert, entsteht ein Konflikt. Besser ist es, benötigte Attribute in ein gemeinsames Projekt auszulagern oder den generierten Code nur bei Bedarf hinzuzufügen.

Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION