Juni 2016

Band 31, Nummer 6

.NET Compiler Platform – Sprachagnostische Codegenerierung mit Roslyn

Von Alessandro Del Del

Die Roslyn-Codebasis bietet leistungsfähige APIs, mit deren Hilfe Sie Ihren Quellcode umfassend analysieren können. Sie können z. B. auf einen Teil des Quellcodes Analyzer und Refactorings anwenden und einen oder mehrere Syntaxknoten durch neuen Code ersetzen, den Sie mit den Roslyn-APIs generieren. Eine gängige Möglichkeit zur Codegenerierung stellt die „SyntaxFactory“-Klasse dar, die Factorymethoden zum Generieren von Syntaxknoten auf eine Weise bietet, die Compiler verstehen können. Die „SyntaxFactory“-Klasse ist in der Tat sehr leistungsstark, da sie das Generieren aller möglichen Syntaxelemente zulässt. Es gibt allerdings zwei verschiedene Implementierungen von „SyntaxFactory“: Microsoft.CodeAnalysis.CSharp.SyntaxFactory und Microsoft.Code­Analysis.VisualBasic.SyntaxFactory. Dies hat eine wichtige Auswirkung, wenn Sie einen Analyzer mit einer Codekorrektur für sowohl C# als auch Visual Basic schreiben möchten. Sie müssen nämlich zwei verschiedene Analyzer (einen für C# und einen für Visual Basic) unter Verwendung der beiden Implementierungen von „SyntaxFactory“ schreiben. Dies liegt daran, dass diese Sprachen einige Konstrukte unterschiedlich behandeln. Dies bedeutet einen höheren Zeitaufwand, da der Analyzer zweimal geschrieben werden muss, und auch die Pflege wird schwieriger. Zum Glück bieten die Roslyn-APIs auch den Microsoft.CodeAnalysis.Editing.SyntaxGenerator, der eine sprachagnostische Generierung von Code erlaubt. Mit SyntaxGenerator müssen Sie demnach Ihre Codegenerierungslogik nur einmal schreiben, die Sie dann sowohl auf C# als auch Visual Basic anwenden können. In diesem Artikel erläutere ich die sprachagnostische Codegenerierung mit SyntaxGenerator sowie die Roslyn-Arbeitsbereichs-APIs.

Starten mit Code

Lassen Sie uns mit etwas Quellcode anfangen, der mit SyntaxGenerator erzeugt wird. Sehen Sie sich im folgenden Beispiel die einfache Klasse „Person“ an, die die „ICloneable“-Schnittstelle in C# (Abbildung 1) und Visual Basic (Abbildung 2) implementiert.

Abbildung 1: Die einfache Klasse „Person“ in C#

public abstract class Person : ICloneable
{
  // Not using auto-props is intentional for demo purposes
  private string _lastName;
  public string LastName
  {
    get
    {
      return _lastName;
    }
    set
    {
      _lastName = value;
    }
  }
  private string _firstName;
  public string FirstName
  {
    get
    {
      return _firstName;
    }
    set
    {
      _firstName = value;
    }
  }
  public Person(string LastName, string FirstName)
  {
    _lastName = LastName;
    _firstName = FirstName;
  }
  public virtual object Clone()
  {
    return MemberwiseClone();
  }
}

Abbildung 2: Die einfache Klasse „Person“ in Visual Basic

Public MustInherit Class Person
  Implements ICloneable
  'Not using auto-props is intentional for demo purposes
  Private _lastName As String
  Private _firstName As String
  Public Property LastName As String
    Get
      Return _lastName
    End Get
    Set(value As String)
      _lastName = value
    End Set
  End Property
  Public Property FirstName As String
    Get
      Return _firstName
    End Get
    Set(value As String)
      _firstName = value
    End Set
  End Property
  Public Sub New(LastName As String, FirstName As String)
    _lastName = LastName
    _firstName = FirstName
  End Sub
  Public Overridable Function Clone() As Object Implements ICloneable.Clone
    Return MemberwiseClone()
  End Function
End Class

Sie werden wahrscheinlich einwenden, dass das Deklarieren automatisch implementierter Eigenschaften dieselbe Auswirkung hätte und der Code in diesem besonderen Fall dadurch aufgeräumter bliebe. Doch später werden Sie erkennen, warum ich diese erweiterte Form wähle.

Diese Implementierung der „Person“-Klasse ist sehr einfach. Doch sie enthält eine brauchbare Anzahl von Syntaxelementen, mit denen das Generieren von Code mit SyntaxGenerator gut veranschaulicht werden kann. Lassen Sie uns diese Klasse mit Roslyn generieren.

Erstellen eines Codeanalysetools

Der erste Schritt ist das Anlegen eines neuen Projekts in Visual Studio 2015 mit Verweisen auf die Roslyn-Bibliotheken. Aufgrund des allgemeinen Charakters dieses Artikels wähle ich weder eine Analyzer- noch ein Refactoring-, sondern eine andere im .NET Compiler Platform SDK verfügbare Projektvorlage aus: „Stand-Alone Code Analysis Tool“, die im Dialogfeld „New Project“ im Knoten „Extensibility“ verfügbar ist (siehe Abbildung 3).

Die Projektvorlage „Stand-Alone Code Analysis Tool“
Abbildung 3: Die Projektvorlage „Stand-Alone Code Analysis Tool“

Diese Projektvorlage dient zum Erstellen einer Konsolenanwendung und automatischen Hinzufügen der ordnungsgemäßen NuGet-Pakete für die Roslyn-APIs für die Sprache Ihrer Wahl. Da es um das Erfüllen der Anforderungen von sowohl C# als auch Visual Basic geht, müssen zunächst die NuGet-Pakete für die zweite Sprache hinzugefügt werden. Wenn Sie beispielsweise anfänglich ein C#-Projekt erstellt haben, müssen Sie die folgenden Visual Basic-Bibliotheken aus NuGet herunterladen und installieren:

  • Microsoft.CodeAnalysis.VisualBasic.dll
  • Microsoft.CodeAnalysis.VisualBasic.Workspaces.dll
  • Microsoft.CodeAnalysis.VisualBasic.Workspaces.Common.dll

Sie müssen bloß die letztgenannte DLL aus NuGet herunterladen und installieren, woraufhin Abhängigkeiten für die anderen beiden erforderlichen Bibliotheken automatisch aufgelöst werden. Das Auflösen von Abhängigkeiten ist immer dann wichtig, wenn Sie vorhaben, die „SyntaxGenerator“-Klasse zu verwenden, und zwar unabhängig von der verwendeten Projektvorlage. Wenn dies nicht erfolgt, kann es zur Laufzeit zu Ausnahmen kommen.

SyntaxGenerator und die Arbeitsbereichs-APIs

Die „SyntaxGenerator“-Klasse macht die statische Methode „GetGenerator“ verfügbar, die eine Instanz von SyntaxGenerator zurückgibt. Sie nutzen diese zurückgegebene Instanz zur Codegenerierung. GetGenerator hat die drei folgenden Überladungen:

public static SyntaxGenerator GetGenerator(Document document)
public static SyntaxGenerator GetGenerator(Project project)
public static SyntaxGenerator GetGenerator(Workspace workspace, string language)

Die ersten beiden überladen Aufgaben für eine „Document“- bzw. eine „Project“-Klasse. Die „Document“-Klasse stellt eine Codedatei in einem Projekt dar, während die „Project“-Klasse ein gesamtes Visual Studio-Projekt darstellt. Diese Überladungen erkennen automatisch die Sprache (C# oder Visual Basic) der „Document“- oder „Project“-Zielklasse. Die „Document“-, „Project“- und „Solution“-Klasse (eine zusätzliche Klasse, die eine SLN-Projektmappe in Visual Studio darstellt) sind Teil eines Arbeitsbereichs. Dieser dient zur verwalteten Interaktion mit allen Elementen einer MSBuild-Projektmappe mit Projekten, Codedateien, Metadaten und Objekten. Die Arbeitsbereichs-APIs machen mehrere Klassen zum Verwalten von Arbeitsbereichen verfügbar, wie z. B. die „MSBuildWorkspace“-Klasse. Diese Klasse wird auf eine SLN-Projektmappe oder die „AdhocWorkspace“-Klasse angewendet. Dies ist sehr nützlich, wenn Sie sie nicht auf eine vorhandene MSBuild-Projektmappe anwenden, sondern einen Arbeitsbereich im Arbeitsspeicher wünschen, der eine solche darstellt. Im Fall von Analyzern und Refactorings von Code verfügen Sie bereits über einen MSBuild-Arbeitsbereich, der das Arbeiten mit Codedateien mithilfe von Instanzen von „Document“-, „Project“- und „Solution“-Klassen erlaubt. Im aktuellen Beispielprojekt gibt es keinen Arbeitsbereich, weshalb wir einen mithilfe der dritten Überladung von SyntaxGenerator erstellen. Zum Erstellen eines neuen leeren Arbeitsbereichs können Sie die „AdhocWorkspace“-Klasse nutzen:

// Get a workspace
var workspace = new AdhocWorkspace();

Sie können nun eine Instanz von SyntaxGenerator abrufen und die Arbeitsbereichsinstanz und gewünschte Sprache als Argumente übergeben:

// Get the SyntaxGenerator for the specified language
var generator = SyntaxGenerator.GetGenerator(workspace, LanguageNames.CSharp);

Der Name der Sprache kann CSharp oder VisualBasic lauten, die beide Konstanten in der „LanguageNames“-Klasse sind. Lassen Sie uns mit C# beginnen. Später erfahren Sie, wie Sie den Sprachnamen in VisualBasic ändern. Sie haben nun alle Tools, die Sie brauchen, und können mit dem Generieren von Syntaxknoten beginnen.

Generieren von Syntaxknoten

Die „SyntaxGenerator“-Klasse macht Instanzfactorymethoden verfügbar, die ordnungsgemäße Syntaxknoten auf eine Weise erzeugen, die mit der Grammatik und Semantik von C# und Visual Basic kompatibel sind. Beispiele: Methoden mit Namen, die mit dem Suffix „Expression“ enden, generieren Ausdrücke. Methoden mit Namen, die mit dem Suffix „Statement“ enden, generieren Anweisungen. Methoden mit Namen, die mit dem Suffix „Declaration“ enden, generieren Deklarationen. Für jede Kategorie gibt es spezifische Methoden zum Erzeugen spezifischer Syntaxknoten. Beispiele: „MethodDeclaration“ erzeugt einen Methodenblock, „PropertyDeclaration“ eine Eigenschaft, „FieldDeclaration“ ein Feld usw. (und wie immer ist IntelliSense Ihr bester Freund). Das Besondere an diesen Methoden ist, dass jede „SyntaxNode“ anstelle eines spezialisierten Typs zurückgibt, der von „SyntaxNode“ abgeleitet ist, was bei der „SyntaxFactory“-Klasse der Fall ist. Dies ermöglicht eine große Flexibilität, insbesondere beim Generieren komplexer Knoten.

Basierend auf der Beispielklasse „Person“ muss zunächst eine „using/Imports“-Direktive für den „System“-Namespace generiert werden, der die „ICloneable“-Schnittstelle verfügbar macht. Dies kann mithilfe der „NamespaceImportDeclaration“-Methode wie folgt erreicht werden:

// Create using/Imports directives
var usingDirectives = generator.NamespaceImportDeclaration("System");

Diese Methode verwendet ein Zeichenfolgenargument, das den zu importierenden Namespace darstellt. Lassen Sie uns nun zwei Felder deklarieren, was mithilfe der „FieldDeclaration“-Methode erfolgt:

// Generate two private fields
var lastNameField = generator.FieldDeclaration("_lastName",
  generator.TypeExpression(SpecialType.System_String),
  Accessibility.Private);
var firstNameField = generator.FieldDeclaration("_firstName",
  generator.TypeExpression(SpecialType.System_String),
  Accessibility.Private);

„FieldDeclaration“ verwendet den Feldnamen, Feldtyp und die Zugriffsebene als Argumente. Zum Angeben des ordnungsgemäßen Typs rufen Sie die „TypeExpression“-Methode auf, die einen Wert aus der „SpecialType“-Enumeration verwendet, in diesem Fall System_String (vergessen Sie nicht, mithilfe von IntelliSense andere Werte erkennen zu lassen). Die Zugriffsebene wird mit einem Wert aus der „Accessibility“-Enumeration festgelegt. Beim Aufrufen von Methoden aus der „SyntaxGenerator“-Klasse ist es üblich, Aufrufe anderer Methoden aus derselben Klasse zu schachteln, wie dies bei „TypeExpression“ der Fall ist. Der nächste Schritt ist das Generieren von zwei Eigenschaften, was durch Aufrufen der „PropertyDeclaration“-Methode erreicht wird, die in Abbildung 4 gezeigt wird.

Abbildung 4: Generieren von zwei Eigenschaften mit der „PropertyDeclaration“-Methode

// Generate two properties with explicit get/set
var lastNameProperty = generator.PropertyDeclaration("LastName",
  generator.TypeExpression(SpecialType.System_String), Accessibility.Public,
  getAccessorStatements:new SyntaxNode[]
  { generator.ReturnStatement(generator.IdentifierName("_lastName")) },
  setAccessorStatements:new SyntaxNode[]
  { generator.AssignmentStatement(generator.IdentifierName("_lastName"),
  generator.IdentifierName("value"))});
var firstNameProperty = generator.PropertyDeclaration("FirstName",
  generator.TypeExpression(SpecialType.System_String),
  Accessibility.Public,
  getAccessorStatements: new SyntaxNode[]
  { generator.ReturnStatement(generator.IdentifierName("_firstName")) },
  setAccessorStatements: new SyntaxNode[]
  { generator.AssignmentStatement(generator.IdentifierName("_firstName"),
  generator.IdentifierName("value")) });

Wie Sie sehen, ist das Generieren eines Syntaxknotens für eine Eigenschaft komplexer. Hier müssen Sie weiter eine Zeichenfolge mit einem Eigenschaftsnamen, dann einen „TypeExpression“-Ausdruck für den Eigenschaftstyp und schließlich die Zugriffsebene übergeben. Bei einer Eigenschaft müssen Sie meist auch den „Get“- und „Set“-Accessor angeben, insbesondere in den Situationen, in denen Sie Code ausführen müssen, mit dem nicht nur der Eigenschaftswert festgelegt oder zurückgegeben wird (z. B. zum Auslösen des „OnPropertyChanged“-Ereignisses beim Implementieren der „INotifyPropertyChanged“-Schnittstelle). Der „Get“- und „Set“-Accessor werden von einem Array von „SyntaxNode“-Objekten dargestellt. Im „Get“-Accessor geben Sie üblicherweise den Eigenschaftswert zurück. Der folgende Code ruft also die „ReturnStatement“-Methode auf, die die Rückgabeanforderungen sowie den Wert oder das Objekt darstellt, den/das sie zurückgibt. In diesem Fall ist der zurückgegebene Wert ein Bezeichner des Felds. Ein Syntaxknoten für einen Bezeichner wird abgerufen, indem Sie die „IdentifierName“-Methode aufrufen. Diese verwendet ein Argument des Typs „string“, gibt aber weiter „SyntaxNode“ zurück. Der „Set“-Accessor speichert hingegen den Eigenschaftswert über eine Zuweisung in einem Feld. Zuweisungen werden von der „AssignmentStatement“-Methode dargestellt, die zwei Argumente verwendet: die linke und rechte Seite der Zuweisung. Im aktuellen Fall erfolgt die Zuweisung zwischen zwei Bezeichnern. Deshalb ruft der Code „IdentifierName“ zweimal auf: einmal für die linke Seite der Zuweisung (den Feldnamen) und einmal für die rechte Seite (den Eigenschaftswert). Da der Eigenschaftswert vom Wertbezeichner in C# und Visual Basic dargestellt wird, kann er hartcodiert werden.

Der nächste Schritt besteht in der Codegenerierung für die „Clone“-Methode, die von der Implementierung der „ICloneable“-Schnittstelle angefordert wird. Im Allgemeinen besteht eine Methode aus der Deklaration, die die Signatur und die Blocktrennzeichen einschließt, und verschiedenen Anweisungen, die den Hauptteil der Methode bilden. Beim aktuellen Beispiel muss „Clone“ auch die „ICloneable.Clone“-Methode implementieren. Aus diesem Grund wird der Ansatz empfohlen, die Codegenerierung für die Methode in drei kleinere Syntaxknoten zu unterteilen. Der erste Syntaxknoten ist der Methodentext, der wie folgt aussieht:

// Generate the method body for the Clone method
var cloneMethodBody = generator.ReturnStatement(generator.
  InvocationExpression(generator.IdentifierName("MemberwiseClone")));

In diesem Fall gibt die „Clone“-Methode das Ergebnis des Aufrufs der „MemberwiseClone“-Methode zurück, die sie von „System.Object“ geerbt hat. Aus diesem Grund ist der Methodentext (wie bereits zuvor) bloß ein Aufruf von „ReturnStatement“. Hier ist das Argument von „ReturnStatement“ ein Aufruf der „InvocationExpression“-Methode, die einen Methodenaufruf darstellt. Deren Parameter ist ein Bezeichner, der den Namen der aufgerufenen Methode darstellt. Da das „InvocationExpression“-Argument den Typ „SyntaxNode“ hat, empfiehlt sich das Angeben des Bezeichners mithilfe der „IdentifierName“-Methode, indem die Zeichenfolge übergeben wird, die den Bezeichner der aufzurufenden Methode darstellt. Bei einer Methode mit komplexeren Methodentext müssten Sie ein Array von „SyntaxNode“-Elementen erstellen, wobei jeder Knoten einen bestimmten Code im Methodentext darstellt.

Im nächsten Schritt wird die Deklaration der „Clone“-Methode erzeugt, was folgendermaßen erfolgt:

// Generate the Clone method declaration
var cloneMethoDeclaration = generator.MethodDeclaration("Clone", null,
  null,null,
  Accessibility.Public,
  DeclarationModifiers.Virtual,
  new SyntaxNode[] { cloneMethodBody } );

Sie genieren mit der „MethodDeclaration“-Methode eine Methode. Diese verwendet verschiedene Argumente wie z. B.:

  • den Methodenname, Typ „String“
  • die Methodenparameter, Typ „IEnumerable<SyntaxNode>“ (NULL in diesem Fall)
  • die Typparameter für generische Methoden, Typ „IEnumerable<SyntaxNode>“ (NULL in diesem Fall)
  • den Rückgabetyp, Typ „SyntaxNode“ (NULL in diesem Fall)
  • die Zugriffsebene, mit einem Wert aus der „Accessibility“-Enumeration
  • die Deklarationsmodifizierer, mit mindestens einem Wert aus der „DeclarationModifiers“-Enumeration; in diesem Fall ist der Modifizierer virtuell (in Visual Basic überschreibbar)
  • die Anweisungen für den Methodentext, Typ „SyntaxNode“; in diesem Fall enthält das Array ein Element, nämlich die zuvor definierte Rückgabeanweisung

In Kürze zeige ich ein Beispiel des Hinzufügens von Methodenparametern mit der spezialisierteren „ConstructorDeclaration“-Methode. Die „Clone“-Methode muss ihr Gegenstück aus der „ICloneable“-Schnittstelle implementieren, was behandelt werden muss. Jetzt brauchen Sie einen Syntaxknoten, der den Schnittstellennamen darstellt. Dieser ist zudem nützlich, wenn die Implementierung der Schnittstelle der „Person“-Klasse hinzugefügt wird. Dies wird durch Aufrufen der „IdentifierName“-Methode erreicht, die einen ordnungsgemäßen Namen aus der angegebenen Zeichenfolge zurückgibt:

// Generate a SyntaxNode for the interface's name you want to implement
var ICloneableInterfaceType = generator.IdentifierName("ICloneable");

Wenn Sie den vollqualifizierten Namen„ System.ICloneable“ importieren möchten, verwenden Sie „DottedName“ anstelle von „IdentifierName“, um einen ordnungsgemäß qualifizierten Namen zu generieren. Doch beim aktuellen Beispiel war eine „NamespaceImportDeclaration“ bereits hinzugefügt worden. An dieser Stelle können Sie alles zusammenführen. SyntaxGenerator weist die Methoden „AsPublicInterfaceImplementation“ und „AsPrivateInterfaceImplementation“ auf, mit deren Hilfe Sie den Compiler informieren, dass eine Methodendefinition wie folgt eine Schnittstelle implementiert:

// Explicit ICloneable.Clone implemenation
var cloneMethodWithInterfaceType = generator.
  AsPublicInterfaceImplementation(cloneMethoDeclaration,
  ICloneableInterfaceType);

Dies ist bei Visual Basic besonders wichtig, das explizit die „Implements“-Klausel erfordert. „AsPublicInterfaceImplementation“ ist das Äquivalent einer impliziten Schnittstellenimplementierung in C#, während „AsPrivateInterfaceImplementation“ das Äquivalent einer expliziten Schnittstellenimplementierung ist. Beide funktionieren für Methoden, Eigenschaften und Indexer.

Im nächsten Schritt geht es um die Generierung des Konstruktors, was mit der „ConstructorDeclaration“-Methode erreicht wird. Wie bei der „Clone“-Methode muss die Definition des Konstruktors für ein einfacheres Verständnis und einen übersichtlicheren Code in kleinere Teile unterteilt werden. Wie Sie bereits aus Abbildung 1 und Abbildung 2 wissen, verwendet der Konstruktor zwei Parameter des Typs „string“, die für die Eigenschafteninitialisierung erforderlich sind. Deshalb empfiehlt es sich, zunächst die Syntaxknoten für beide Parameter zu erstellen:

// Generate parameters for the class' constructor
var constructorParameters = new SyntaxNode[] {
  generator.ParameterDeclaration("LastName",
  generator.TypeExpression(SpecialType.System_String)),
  generator.ParameterDeclaration("FirstName",
  generator.TypeExpression(SpecialType.System_String)) };

Die Parameter werden mit der „ParameterDeclaration“-Methode generiert, die eine Zeichenfolge, die den Parameternamen darstellt, und einen Ausdruck verwendet, der den Parametertyp darstellt. Beide Parameter haben den Typ „String“, weshalb der Code, wie bereits erwähnt, die „TypeExpression“-Methode nutzt. Der Grund für das Packen beider Parameter in ein „SyntaxNode“-Objekt ist, dass „ConstructorDeclaration“ ein Objekt dieses Typs verlangt, um Parameter darzustellen.

Sie müssen nun den Methodentext erstellen, wozu die bereits zuvor vorgestellte „AssignmentStatement“-Methode wie folgt verwendet wird:

// Generate the constructor's method body
var constructorBody = new SyntaxNode[] {
  generator.AssignmentStatement(generator.IdentifierName("_lastName"),
  generator.IdentifierName("LastName")),
  generator.AssignmentStatement(generator.IdentifierName("_firstName"),
  generator.IdentifierName("FirstName"))};

In diesem Fall gibt es zwei Anweisungen, die in einem „SyntaxNode“-Objekt gruppiert wurden. Schließlich können Sie den Konstruktor erzeugen, indem Sie die Parameter und den Methodentext zusammenfügen:

// Generate the class' constructor
var constructor = generator.ConstructorDeclaration("Person",
  constructorParameters, Accessibility.Public,
  statements:constructorBody);

„ConstructorDeclaration“ ist vergleichbar mit „MethodDeclaration“, ist jedoch speziell darauf ausgelegt, eine „.ctor“-Methode in C# und eine „Sub New“-Methode in Visual Basic zu generieren.

Generieren einer CompilationUnit

Bislang haben Sie erfahren, wie Code für jedes Element der „Person“-Klasse erstellt wird. Nun müssen Sie diese Elemente zusammenführen und ein ordnungsgemäßes „SyntaxNode“-Objekt für die Klasse erstellen. Klassenelemente müssen in Form eines „SyntaxNode“-Objekts angegeben werden. Das folgende Beispiel veranschaulicht, wie Sie alle zuvor genannten Elemente zusammenführen:

// An array of SyntaxNode as the class members
var members = new SyntaxNode[] { lastNameField,
  firstNameField, lastNameProperty, firstNameProperty,
  cloneMethodWithInterfaceType, constructor };

Sie können nun schließlich die „Person“-Klasse erstellen und die „ClassDeclaration“-Methode wie folgt nutzen:

// Generate the class
var classDefinition = generator.ClassDeclaration(
  "Person", typeParameters: null,
  accessibility: Accessibility.Public,
  modifiers: DeclarationModifiers.Abstract,
  baseType: null,
  interfaceTypes: new SyntaxNode[] { ICloneableInterfaceType },
  members: members);

Wie bei anderen Arten von Deklarationen erfordert diese Methode das Angeben des Namens (NULL in diesem Fall), der Zugriffsebene, der Modifizierer („Abstract“ in diesem Fall, oder „MustInherit“ in Visual Basic), der Basistypen (NULL in diesem Fall) und der implementierten Schnittstellen (in diesem Fall ein „SyntaxNode“-Objekt, das den Schnittstellenamen enthält, der zuvor als Syntaxknoten erstellt wurde). Sie können die Klasse auch in einem Namespace kapseln. Die „SyntaxGenerator“-Klasse enthält die „NamespaceDeclaration“-Methode, die den Namespacenamen und das enthaltene „SyntaxNode“-Objekt verwendet. Sie verwenden sie folgendermaßen:

// Declare a namespace
var namespaceDeclaration = generator.NamespaceDeclaration("MyTypes", classDefinition);

Compiler wissen bereits, wie der generierte Syntaxknoten für den vollständigen Namespace und die geschachtelten Elemente verarbeitet werden und die Codeanalyse für die Syntax erfolgen soll. Doch mitunter benötigen Sie dieses Ergebnis in Form einer CompilationUnit, also eines Typs, der eine Codedatei darstellt. Dies ist typisch für Analyzer und Refactorings von Code. Es folgt der Code, den Sie schreiben, um eine CompilationUnit zurückzugeben:

// Get a CompilationUnit (code file) for the generated code
var newNode = generator.CompilationUnit(usingDirectives, namespaceDeclaration).
  NormalizeWhitespace();

Diese Methode akzeptiert eine oder mehrere „SyntaxNode“-Instanzen als Argument.

Die Ausgabe in C# und Visual Basic

Nach dieser ganzen Arbeit ist es nun Zeit, sich das Ergebnis anzusehen. Abbildung 5 zeigt den generierten C#-Code für die „Person“-Klasse.

Der mithilfe von Roslyn generierte C#-Code für die „Person“-Klasse
Abbildung 5: Der mithilfe von Roslyn generierte C#-Code für die „Person“-Klasse

Ändern Sie nun einfach in der Codezeile, die einen neuen „AdhocWorkspace“ erstellt, die Sprache in VisualBasic:

generator = SyntaxGenerator.GetGenerator(workspace, LanguageNames.VisualBasic);

Wenn Sie den Code erneut ausführen, erhalten Sie eine Visual Basic-Klassendefinition entsprechend Abbildung 6.

Der mithilfe von Roslyn generierte Visual Basic-Code für die „Person“-Klasse
Abbildung 6: Der mithilfe von Roslyn generierte Visual Basic-Code für die „Person“-Klasse

Der wichtige Punkt hier ist, dass Sie mit SyntaxGenerator Code einmal geschrieben haben und sowohl C#- als auch Visual Basic-Code generieren konnten, mit dem die Roslyn-Analyse-APIs arbeiten können. Wenn Sie fertig sind, vergessen Sie nicht die „Dispose“-Methode für die „AdhocWorkspace“-Instanz aufzurufen, oder schließen Sie einfach Ihren Code in eine „using“-Anweisung ein. Da niemand perfekt ist und der generierte Code Fehler enthalten kann, können Sie auch die „ContainsDiagnostics“-Eigenschaft auf Diagnose-Informationen im Code überprüfen und mit der „GetDiagnostics“-Methode detaillierte Informationen zu Problemen im Code abrufen.

Sprachagnostische Analyzer und Refactorings

Sie können die Roslyn-APIs und die „SyntaxGenerator“-Klasse immer dann nutzen, wenn Sie Ihren Quellcode umfassend analysieren wollen. Doch dieser Ansatz empfiehlt sich auch für Analyzer und Refactorings von Code. Für Analyzer, Codekorrekturen und Refactorings gibt es die Attribute „DiagnosticAnalyzer“, „ExportCodeFixProvider“ und „ExportCodeRefactoringProvider“, die alle die primäre und sekundäre unterstützte Sprache akzeptieren. Bei Verwenden von SyntaxGenerator anstelle von SyntaxFactory können Sie für C# und Visual Basic gleichzeitig entwickeln.

Zusammenfassung

Die „SyntaxGenerator“-Klasse aus dem „Microsoft.CodeAnalysis.Editing“-Namespace bietet eine sprachagnostische Möglichkeit, mittels einer Codebasis Syntaxknoten für C# und Visual Basic zu entwickeln. Mit dieser leistungsfähigen Klasse können Sie alle möglichen Syntaxelemente so generieren, dass sie mit beiden Compilern kompatibel sind, was Zeit spart und die Pflege von Code erleichtert.


Alessandro Del Soleist seit 2008 ein Microsoft MVP. Er wurde bereits fünf Mal als MVP des Jahres ausgezeichnet und ist der Autor zahlreicher Bücher, eBooks, Videoanleitungen und Artikel zur .NET-Entwicklung mit Visual Studio. Del Sole arbeitet als Experte für die Lösungsentwicklung für Brain-Sys mit dem Schwerpunkt .NET-Entwicklung, Training und Consulting. Sie können ihm auf Twitter folgen: @progalex.

Unser Dank gilt den folgenden technischen Experten von Microsoft für die Durchsicht dieses Artikels: Anthony D. Green und Matt Warren
Anthony D. Green ist der Programm-Manager für Visual Basic. Er war auch fünf Jahre lang an der Entwicklung von Roslyn beteiligt. Er stammt aus Chicago. Auf Twitter finden Sie ihn unter @ThatVBGuy.