© GrandeDuc/shutterstock.com
Neue Tricks zum Generieren von Code in .NET und C#

C# Source Generators


C# Source Generators dienen der automatischen Erstellung von Code, was zum Beispiel bei Routinearbeiten hilfreich sein kann, wenn Codeteile nach einem fixen Schema erstellt werden sollen. In diesem Artikel wird gezeigt, wie C# Source Generators eingesetzt werden können und was sie von bestehenden Optionen zur Codegenerierung unterscheidet.

Ein wichtiger Trick, um beim Entwickeln von Code produktiver zu werden, ist, Routinetätigkeiten zu vermeiden. Code, den man ohne großes Nachdenken nach einem gewissen Schema schreiben kann, könnte man wahrscheinlich generieren, statt ihn manuell zu erstellen. In Visual Studio gibt es dafür ein Werkzeug, das recht weit verbreitet ist: T4 Templates [1]. T4 Templates können von Visual Studio zur Entwicklungszeit ausgeführt werden. Alternativ kann man T4 in sein eigenes Programm einbetten und zur Laufzeit Code generieren (z. B. SQL Statements). Leider hat T4 jedoch erhebliche Nachteile. Erstens ist es eng an Visual Studio gebunden. Die Zeit, in der jede C#-Entwicklerin zum Schreiben von C# Visual Studio verwendet hat, ist vorbei. Manche bevorzugen Visual Studio Code oder IDEs von Drittanbietern, wie beispielsweise Rider. Zweitens ist T4 veraltet, kann nur mit Mühe in .NET-Core-Projekten eingesetzt werden und wird nicht mehr aktiv weiterentwickelt. Es muss also eine Alternative her.

Wie auch beim Compiler geht Microsoft im Bereich Codegenerierung weg von Lösungsansätzen, die an eine bestimmte IDE gebunden sind. Wenn Code zur Entwicklungs- oder Übersetzungszeit generiert werden soll, gibt es eigentlich nur einen Platz, an den eine solche Komponente gut hinpasst: Roslyn. Microsoft hat für Roslyn vor einigen Monaten die neue C#-Source-Generator-Funktion als Preview vorgestellt. Sie löst Werkzeuge wie T4 zum Generieren von Code zur Übersetzungszeit ab. Code- oder Textgenerierung zur Laufzeit ist kein Thema für die neuen Source Generators.

Routinearbeiten automatisieren

Bevor wir uns ansehen, wie C# Source Generators funktionieren, behandeln wir mögliche Einsatzszenarien. Das erste und offensichtlichste Einsatzszenario ist, Codeteile, die keine originäre Logik abbilden, sondern nach einem fixen Schema erstellt werden, automatisch zu generieren. Ein typisches Beispiel ist das in C# gut bekannte INotifyPropertyChanged-Interface. Es ist langweilig und zeitraubend, für eine C#-Klasse mit normalen Properties INotifyPropertyChanged zu implementieren. Man braucht dafür keine Kreativität, es ist einfach nur eine Menge Tipparbeit. Diese Aufgabe könnte ein Generator übernehmen. Ein weiteres Beispiel ist die Generierung von Modellklassen auf Basis von strukturierten Dateien wie zum Beispiel CSV, XML oder JSON. Viele Leserinnen und Leser haben sicherlich schon die diversen Internetseiten zum Generieren von C#-Klassen aus solchen Dateien genutzt, weil man sich die Arbeit des manuellen Erstellens des entsprechenden Codes sparen will. Auch in einem solchen Szenario könnte ein C# Source Generator das Codegenerieren übernehmen.

Erfahrene C#-Entwicklerinnen und -Entwickler könnten an dieser Stelle einwenden, dass man schon jetzt Code generieren kann, unter anderem mit den Klassen aus dem Namespace System.Reflection.Emit oder mit Hilfe von Expression Trees. Ideal sind solche Ansätze aber nicht. Erstens sind sowohl Reflection.Emit als auch Expression Trees komplex zu verwenden und zweitens erfolgt die Codegenerierung zur Laufzeit. Das beeinflusst die Performance negativ.

Dependency Injection und AoT Compilation

Neben dem Automatisieren von Routinetätigkeiten gibt es noch weitere Einsatzbereiche von Codegenerierung, die nicht so offensichtlich sind. Ein Beispiel ist Dependency Injection (kurz DI). Die DI-Magie von ASP.NET basiert heute zu großen Teilen auf Reflection. Controller werden zur Laufzeit über Reflection gesucht. Die Konstruktorparameter werden mit Hilfe von Reflection untersucht und die notwendigen Werte werden zur Laufzeit bereitgestellt. Funktional ist das einwandfrei. Es hat jedoch zwei wesentliche Nachteile: Erstens braucht der Einsatz von Reflection zur Laufzeit CPU-Zeit und verlangsamt daher den Start einer Webanwendung. Dieser Performancenachteil ist zwar nicht riesig, er macht sich aber speziell bei Cloud-Native-Lösungen bemerkbar, bei denen häufig Serverinstanzen neu starten (z. B. Cold Start bei Serverless Functions, Scale Out bei Autoscaling).

Zweitens erschwert Reflection das Optimieren des Codes durch einen Linker beim Ahead-of-Time-Übersetzen (kurz AOT). Der Compiler könnte beim Einsatz von AOT Teile des Programms, die nicht verwendet werden, entfernen und dadurch die Größe der Anwendung reduzieren. Wenn aber Reflection zum Einsatz kommt, tut sich der Compiler schwer, zu erkennen, was zur Laufzeit benötigt wird und was nicht. Durch Generierung von Source Code könnte man einen grundlegend neuen Ansatz für DI verfolgen. Die Abhängigkeiten könnten zur Übersetzungszeit analysiert und der sich ergebende Code könnte generiert werden. Reflection würde zur Laufzeit komplett entfallen und der Compiler könnte unnötige Codeteile problemlos entfernen. In anderen Programmiersprachen wird dieser Ansatz z...

Neugierig geworden? Wir haben diese Angebote für dich:

Angebote für Gewinner-Teams

Wir bieten Lizenz-Lösungen für Teams jeder Größe: Finden Sie heraus, welche Lösung am besten zu Ihnen passt.

Das Library-Modell:
IP-Zugang

Das Company-Modell:
Domain-Zugang