Mai 2017

Band 32, Nummer 5

.NET Core: Plattformübergreifende Codegenerierung mit Roslyn und .NET Core

Von Alessandro Del Del

.NET Core ist eine modulare und plattformübergreifende Open Source-Sammlung von Tools, die das Erstellen von .NET-Anwendungen der nächsten Generation ermöglichen, die unter Windows, Linux und macOS (microsoft.com/net/core/platform) ausgeführt werden können. .NET Core kann auch unter der Windows 10 IoT-Distribution installiert und auf Geräten wie Raspberry PI ausgeführt werden. .NET Core ist eine leistungsfähige Plattform, die die Laufzeit, Bibliotheken und Compiler umfasst und vollständige Unterstützung für Sprachen wie C#, F# und Visual Basic bietet. Dies bedeutet, dass Sie Code in C# nicht nur unter Windows, sondern auch unter anderen Betriebssystemen schreiben können, weil die .NET-Compilerplattform (github.com/dotnet/roslyn), die auch als „Project Roslyn“ bezeichnet wird, plattformübergreifende Open Source-Compiler mit leistungsfähigen Codeanalyse-APIs bereitstellt. Ein wichtiger Aspekt dabei ist, dass Sie die Roslyn-APIs zum Ausführen zahlreicher codebezogener Vorgänge unter verschiedenen Betriebssystemen nutzen können, z. B. für Codeanalyse, Codegenerierung und Kompilierung. Dieser Artikel stellt die erforderlichen Schritte zum Einrichten eines C#-Projekts unter .NET Core für die Verwendung der Roslyn-APIs vor und erläutert einige interessante Codegenerierungs- und Kompilierungsszenarien. Außerdem werden einige grundlegende Reflektionstechniken zum Aufrufen und Ausführen von Code beschrieben, der mit Roslyn unter .NET Core kompiliert wurde. Wenn Sie nicht mit Roslyn vertraut sind, sollten Sie zuerst die folgenden Artikel lesen:

Installieren des .NET Core SDK

Im ersten Schritt installieren Sie .NET Core und das SDK. Wenn Sie unter Windows arbeiten und Visual Studio 2017 installiert haben, ist .NET Core bereits enthalten, wenn die plattformübergreifende .NET Core-Entwicklungsworkload zur Installationszeit im Visual Studio-Installer ausgewählt wurde. War dies nicht der Fall, öffnen Sie einfach den Visual Studio-Installer, wählen Sie die Workload aus, und klicken Sie dann auf „Ändern“. Wenn Sie unter Windows arbeiten, ohne Visual Studio 2017 zu nutzen, oder wenn Linux oder macOS verwendet wird, können Sie .NET Core manuell installieren und Visual Studio Code als Entwicklungsumgebung (code.visualstudio.com) einsetzen. Das letztgenannte Szenario werde ich in diesem Artikel behandeln, weil Visual Studio Code selbst plattformübergreifend und daher ein guter Begleiter für .NET Core ist. Denken Sie außerdem daran, die C#-Erweiterung für Visual Studio Code (bit.ly/29b1Ppl) zu installieren. Die zum Installieren von .NET Core erforderlichen Schritte sind je nach Betriebssystem unterschiedlich. Befolgen Sie daher die Anleitungen unter bit.ly/2mJArWx. Installieren Sie unbedingt das aktuelle Release. Es muss noch erwähnt werden, dass die aktuellen Releases von .NET Core das project.json-Dateiformat nicht mehr unterstützen. Stattdessen wird das gängigere CSPROJ-Dateiformat mit MSBuild unterstützt.

Gerüstbau einer .NET Core-Anwendung in C#

Mit .NET Core können Sie Konsolen- und Webanwendungen erstellen. Für Webanwendungen stellt Microsoft im Lauf der Zeit zusätzlich zur ASP.NET Core-Vorlage weitere Vorlagen zur Verfügung. Da Visual Studio Code ein Lightweight-Editor ist, stellt dieser keine Projektvorlagen zur Verfügung, wie es bei Visual Studio der Fall ist. Dies bedeutet, dass Sie eine Anwendung über die Befehlszeile in einem Ordner erstellen, dessen Name auch der Anwendungsname sein wird. Das folgende Beispiel basiert auf den Anleitungen für Windows. Für macOS und Linux gelten jedoch die gleichen Prinzipien. Öffnen Sie als ersten Schritt eine Eingabeaufforderung, und navigieren Sie zu einem Ordner auf Ihrem Datenträger. Angenommen, Sie verwenden z. B. einen Ordner namens „C:\Apps“. Navigieren Sie zu diesem Ordner, und erstellen Sie einen neuen Unterordner namens „RoslynCore“. Verwenden Sie zu diesem Zweck die folgenden Befehle:

> cd C:\Apps
> md RoslynCore
> cd RoslynCore

„RoslynCore“ wird also der Name der Beispielanwendung sein, die in diesem Artikel beschrieben wird. Es handelt sich um eine Konsolenanwendung, die perfekt für Unterweisungszwecke geeignet ist und die Codierung mit Roslyn vereinfacht. Sie können die gleichen Techniken auch in ASP.NET Core-Webanwendungen verwenden. Geben Sie einfach Folgendes in die Befehlszeile ein, um ein neues, leeres Projekt für eine Konsolenanwendung zu erstellen:

> dotnet new console

Auf diese Weise schafft .NET Core das Gerüst für ein C#-Projekt für eine Konsolenanwendung mit dem Namen „RoslynCore“. Nun können Sie den Ordner des Projekts mit Visual Studio Code öffnen. Am einfachsten geschieht dies, indem Sie Folgendes in die Befehlszeile eingeben:

> code .

Natürlich können Sie Visual Studio Code auch aus dem Windows-Startmenü öffnen und dann manuell einen Projektordner öffnen. Sobald Sie eine C#-Codedatei eingeben, wird Ihre Erlaubnis zum Generieren einiger erforderlicher Ressourcen sowie zum Wiederherstellen einiger NuGet-Pakete eingeholt (siehe Abbildung 1).

Visual Studio Code muss das Projekt aktualisieren
Abbildung 1: Visual Studio Code muss das Projekt aktualisieren

Im nächsten Schritt werden die NuGet-Pakete hinzugefügt, die für das Arbeiten mit Roslyn erforderlich sind.

Hinzufügen der Roslyn NuGet-Pakete

Wie Sie vielleicht wissen, können die Roslyn-APIs durch Installieren einiger NuGet-Pakete aus der Microsoft.CodeAnalysis-Hierarchie genutzt werden. Vor der Installation dieser Pakete ist die Abklärung wichtig, wie die Roslyn-APIs in das .NET Core-System passen. Wenn Sie schon einmal mit Roslyn und .NET Framework gearbeitet haben, sind Sie möglicherweise gewohnt, die vollständige Sammlung der Roslyn-APIs zu nutzen. .NET Core verwendet .NET Standard-Bibliotheken. Dies bedeutet, dass nur die Roslyn-Bibliotheken, die .NET Standard unterstützen, in .NET Core genutzt werden können. Zurzeit sind die meisten Roslyn-APIs bereits für .NET Core verfügbar, auch die Compiler-APIs (mit den Ausgabe- und Diagnose-APIs) und die Arbeitsbereich-APIs. Nur einige wenige APIs sind noch nicht portierbar. Microsoft investiert jedoch intensiv in Roslyn und .NET Core. Es kann daher davon ausgegangen werden, dass vollständige Kompatibilität mit .NET Standard in zukünftigen Releases verfügbar sein wird. Ein echtes Beispiel für eine plattformübergreifende Anwendung, die unter .NET Core ausgeführt wird, ist OmniSharp (bit.ly/2mpcZeF). Die Anwendung nutzt die Roslyn-APIs für die meisten Features des Code-Editors, z. B. Vervollständigungslisten und Syntaxhervorhebung.

In diesem Artikel zeige ich, wie der Compiler und die Diagnose-APIs genutzt werden können. Zu diesem Zweck müssen Sie Ihrem Projekt das NuGet-Paket „Microsoft.CodeAnalysis.CSharp“ hinzufügen. Da das neue NET Core-Projektsystem auf MSBuild basiert, ist die Liste der NuGet-Pakete nun in der CSPROJ-Projektdatei enthalten. In Visual Studio 2017 können Sie die Clientbenutzeroberfläche für NuGet zum Herunterladen, Installieren und Verwalten von Paketen verwenden. In Visual Studio Code ist jedoch keine äquivalente Option vorhanden. Glücklicherweise können Sie die CSPROJ-Datei einfach öffnen und dann zum <ItemGroup>-Knoten navigieren, der <PackageReference>-Elemente enthält, die jeweils ein erforderliches NuGet-Paket darstellen. Ändern Sie den Knoten wie folgt:

<ItemGroup>
  ...
  <PackageReference Include="Microsoft.CodeAnalysis.CSharp"
    Version="2.0.0 " />
  <PackageReference Include="System.Runtime.Loader" Version="4.3.0" />
</ItemGroup>

Beachten Sie, dass das Hinzufügen eines Verweises zum Paket „Microsoft.CodeAnalysis.C­Sharp“ Ihnen den Zugriff auf die APIs des C#-Compilers ermöglicht und dass das Paket „System.Runtime.Loader“ für Reflektion erforderlich ist und an späterer Stelle in diesem Artikel zum Einsatz kommen wird.

Wenn Sie Ihre Änderungen speichern, erkennt Visual Studio Code fehlende NuGet-Pakete und bietet deren Wiederherstellung an.

Codeanalyse: Analysieren von Quelltext und Generieren von Syntaxknoten

Das erste Beispiel bezieht sich auf Codeanalyse und zeigt, wie Quellcodetext analysiert wird und neue Syntaxknoten generiert werden. Angenommen, Sie verwenden z. B. das folgende einfache Geschäftsobjekt und möchten basierend auf diesem eine View Model-Klasse generieren:

namespace Models
{
  public class Item
  {
    public string ItemName { get; set }
  }
}

Der Text für dieses Geschäftsobjekt kann aus verschiedenen Quellen stammen, z. B. aus einer C#-Codedatei, einer Zeichenfolge in Ihrem Code oder sogar aus einer Benutzereingabe. Mit den Codeanalyse-APIs können Sie den Quelltext analysieren und einen neuen Syntaxknoten generieren, den der Compiler verstehen und ändern kann. Sehen Sie sich z. B. den Code in Abbildung 2 an, der eine Zeichenfolge analysiert, die eine Klassendefinition enthält, den entsprechenden Syntaxknoten abruft und dann eine neue statische Methode abruft, die ein View Model aus dem Syntaxknoten generiert.

Abbildung 2: Analysieren von Quellcode und Abrufen eines Syntaxknotens

using System;
using RoslynCore;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
class Program
{
  static void Main(string[] args)
  {
    GenerateSampleViewModel();
  }
  static void GenerateSampleViewModel()
  {
    const string models = @"namespace Models
{
  public class Item
  {
    public string ItemName { get; set }
  }
}
";
    var node = CSharpSyntaxTree.ParseText(models).GetRoot();
    var viewModel = ViewModelGeneration.GenerateViewModel(node);
    if(viewModel!=null)
      Console.WriteLine(viewModel.ToFullString());
    Console.ReadLine();
  }
}

Die GenerateViewModel-Methode wird in einer statischen Klasse namens „ViewModelGeneration“ definiert. Fügen Sie dem Projekt daher eine Datei namens „ViewModelGeneration.cs“ hinzu. Die Methode sucht im Eingabesyntaxknoten (für Demonstrationszwecke die erste Instanz eines ClassDeclarationSyntax-Objekts) nach einer Klassendefinition und generiert dann eine neues View Model basierend auf dem Namen und den Membern der Klasse. Abbildung 3 zeigt dies.

Abbildung 3: Generieren eines neuen Syntaxknotens

using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis;
using System.Linq;
using Microsoft.CodeAnalysis.CSharp;
namespace RoslynCore
{
  public static class ViewModelGeneration
  {
    public static SyntaxNode GenerateViewModel(SyntaxNode node)
    {
      // Find the first class in the syntax node
      var classNode = node.DescendantNodes()
       .OfType<ClassDeclarationSyntax>().FirstOrDefault();
      if(classNode!=null)
      {
        // Get the name of the model class
        string modelClassName = classNode.Identifier.Text;
        // The name of the ViewModel class
        string viewModelClassName = $"{modelClassName}ViewModel";
        // Only for demo purposes, pluralizing an object is done by
        // simply adding the "s" letter. Consider proper algorithms
        string newImplementation =
          $@"public class {viewModelClassName} : INotifyPropertyChanged
{{
public event PropertyChangedEventHandler PropertyChanged;
// Raise a property change notification
protected virtual void OnPropertyChanged(string propname)
{{
  PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propname));
}}
private ObservableCollection<{modelClassName}> _{modelClassName}s;
public ObservableCollection<{modelClassName}> {modelClassName}s
{{
  get {{ return _{modelClassName}s; }}
  set
  {{
    _{modelClassName}s = value;
    OnPropertyChanged(nameof({modelClassName}s));
  }}
}}
public {viewModelClassName}() {{
// Implement your logic to load a collection of items
}}
}}
";
          var newClassNode =
            CSharpSyntaxTree.ParseText(newImplementation).GetRoot()
            .DescendantNodes().OfType<ClassDeclarationSyntax>()
            .FirstOrDefault();
          // Retrieve the parent namespace declaration
          if(!(classNode.Parent is NamespaceDeclarationSyntax)) return null;
          var parentNamespace = (NamespaceDeclarationSyntax)classNode.Parent;
          // Add the new class to the namespace and adjust the white spaces
          var newParentNamespace =
            parentNamespace.AddMembers(newClassNode).NormalizeWhitespace();
          return newParentNamespace;
        }
      }
      else
      {
        return null;
      }
    }
  }
}

Im ersten Teil des Codes in Abbildung 3 können Sie sehen, wie das View Model zuerst als eine Zeichenfolge dargestellt wird. Dabei wird Zeichenfolgeninterpolation verwendet, durch die das Angeben von Objekt- und Membernamen basierend auf dem ursprünglichen Klassennamen vereinfacht wird. In diesem Beispielszenario werden die Pluralformen einfach durch Hinzufügen des Buchstaben „s“ zum Objekt-/Membernamen generiert. In echtem Code sollten Sie spezifischere Algorithmen für die Pluralbildung verwenden.

Im zweiten Teil von Abbildung 3 ruft der Code „CSharpSyntaxTree.ParseText“ zum Analysieren des Quelltexts in einen „SyntaxTree“ auf. „GetRoot“ wird zum Abrufen des „SyntaxNode“ für den neuen Baum aufgerufen. Mit „DescendantNodes().OfType<ClassDeclarationSyntax>()“ ruft der Code nur die Syntaxknoten ab, die eine Klasse darstellen, und wählt dabei mit „FirstOrDefault“ nur den ersten Knoten aus. Das Abrufen der ersten Klasse im Syntaxknoten reicht aus, um den übergeordneten Namespace zu ermitteln, in den die neue View Model-Klasse eingefügt wird. Das Abrufen eines Namespaces ist durch Umwandeln der Parent-Eigenschaft einer „ClassDeclarationSyntax“ in ein NamespaceDeclarationSyntax-Objekt möglich. Da eine Klasse in eine andere Klasse geschachtelt sein kann, überprüft der Code zuerst diese Möglichkeit, indem bestätigt wird, dass „Parent“ vom Typ „NamespaceDeclarationSyntax“ ist. Der letzte Codeausschnitt fügt den neuen Syntaxknoten für die View Model-Klasse dem Parent-Namespace hinzu und gibt ihn als Syntaxknoten zurück. Wenn Sie jetzt F5 drücken, sehen Sie das Ergebnis der Codegenerierung in der Debugkonsole. Abbildung 4 zeigt dies.

Die View Model-Klasse wurde ordnungsgemäß erstellt
Abbildung 4: Die View Model-Klasse wurde ordnungsgemäß erstellt

Die generierte View Model-Klasse ist ein „SyntaxNode“, mit dem der C#-Compiler arbeiten kann. Er kann daher weiter geändert, zum Abrufen von Diagnoseinformationen analysiert, in eine Assembly mit den Ausgabe-APIs kompiliert und über Reflektion genutzt werden.

Abrufen von Diagnoseinformationen

Unabhängig davon, ob Quelltext aus einer Zeichenfolge, einer Datei oder einer Benutzereingabe stammt: Sie können die Diagnose-APIs zum Abrufen von Diagnoseinformationen zu Codeproblemen wie Fehlern und Warnungen verwenden. Denken Sie daran, dass die Diagnose-APIs nicht nur das Abrufen von Fehlern und Warnungen ermöglichen, sondern auch das Schreiben von Analysen und Coderefactorings ermöglichen. Im Beispiel oben empfiehlt es sich, auf syntaktische Fehler im ursprünglichen Quelltext zu prüfen, bevor Sie versuchen, eine View Model-Klasse zu generieren. Sie können zu diesem Zweck die SyntaxNode.GetDiagnostics-Methode aufrufen, die ggf. ein IEnumerable<Microsoft.CodeAnalysis.Diagnostic>-Objekt zurückgibt. Abbildung 5 zeigt eine erweiterte Version der ViewModelGeneration-Klasse. Der Code überprüft, ob das Ergebnis des Aufrufs von „GetDiagnostics“ Diagnoseinformationen enthält. Wenn dies nicht der Fall ist, generiert der Code die View Model-Klasse. Wenn das Ergebnis stattdessen eine Sammlung von Diagnosedaten enthält, zeigt der Code Informationen für jeden Diagnosewert an und gibt NULL zurück. Die Diagnostic-Klasse stellt sehr differenzierte Informationen zu jedem Codeproblem bereit. Die Id-Eigenschaft gibt z. B. eine Diagnose-ID zurück. Die GetMessage-Methode gibt die vollständige Diagnosemeldung zurück. „GetLineSpan“ gibt die Diagnoseposition im Quellcode zurück und die Severity-Eigenschaft den Diagnoseschweregrad, z. B. „Error“, „Warning“ oder „Information“.

Abbildung 5: Prüfen auf Codeprobleme mit den Diagnose-APIs

using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis;
using System.Linq;
using Microsoft.CodeAnalysis.CSharp;
using System;
namespace RoslynCore
{
  public static class ViewModelGeneration
  {
    public static SyntaxNode GenerateViewModel(SyntaxNode node)
    {
      // Find the first class in the syntax node
      var classNode =
        node.DescendantNodes().OfType<ClassDeclarationSyntax>().FirstOrDefault();
      if(classNode!=null)
      {
        var codeIssues = node.GetDiagnostics();
        if(!codeIssues.Any())
        {
          // Get the name of the model class
          var modelClassName = classNode.Identifier.Text;
          // The name of the ViewModel class
          var viewModelClassName = $"{modelClassName}ViewModel";
          // Only for demo purposes, pluralizing an object is done by
          // simply adding the "s" letter. Consider proper algorithms
          string newImplementation =
            $@"public class {viewModelClassName} : INotifyPropertyChanged
{{
public event PropertyChangedEventHandler PropertyChanged;
// Raise a property change notification
protected virtual void OnPropertyChanged(string propname)
{{
  PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propname));
}}
private ObservableCollection<{modelClassName}> _{modelClassName}s;
public ObservableCollection<{modelClassName}> {modelClassName}s
{{
  get {{ return _{modelClassName}s; }}
  set
  {{
    _{modelClassName}s = value;
    OnPropertyChanged(nameof({modelClassName}s));
  }}
}}
public {viewModelClassName}() {{
// Implement your logic to load a collection of items
}}
}}
";
            var newClassNode =
              SyntaxFactory.ParseSyntaxTree(newImplementation).GetRoot()
              .DescendantNodes().OfType<ClassDeclarationSyntax>()
              .FirstOrDefault();
            // Retrieve the parent namespace declaration
            if(!(classNode.Parent is NamespaceDeclarationSyntax)) return null;
            var parentNamespace = (NamespaceDeclarationSyntax)classNode.Parent;
            // Add the new class to the namespace
            var newParentNamespace =
              parentNamespace.AddMembers(newClassNode).NormalizeWhitespace();
            return newParentNamespace;
          }
          else
          {
            foreach(Diagnostic codeIssue in codeIssues)
          {
            string issue = $"ID: {codeIssue.Id}, Message: {codeIssue.GetMessage()},
              Location: {codeIssue.Location.GetLineSpan()},
              Severity: {codeIssue.Severity}";
            Console.WriteLine(issue);
          }
          return null;
        }
      }
      else
      {
        return null;
      }
    }
  }
}

Wenn Sie nun innerhalb der GenerateSample­ViewModel-Methode in „Program.cs“ einige beabsichtigte Fehler in den Quelltext einfügen, der in der models-Variablen enthalten ist, und die Anwendung dann ausführen, können Sie sehen, wie der C#-Compiler vollständige Details zu jedem Codeproblem zurückgibt. Abbildung 6 zeigt ein Beispiel.

Erkennen von Codeproblemen mit den Diagnose-APIs
Abbildung 6: Erkennen von Codeproblemen mit den Diagnose-APIs

Beachten Sie, dass der C#-Compiler selbst dann einen Syntaxbaum generiert, wenn Diagnosedaten enthalten sind. Dies führt nicht nur zu voller Originaltreue mit dem Quelltext, sondern bietet Entwicklern auch die Option, diese Probleme mit neuen Syntaxknoten zu beheben.

Ausführen von Code: die Ausgabe-APIs

Die Ausgabe-APIs ermöglichen das Kompilieren von Quellcode in Assemblys. Anschließend können Sie mit Reflektion Code aufrufen und ausführen. Das nächste Beispiel zeigt eine Kombination aus Codegenerierung, Ausgabe und Diagnoseermittlung. Fügen Sie dem Projekt eine neue Datei namens „EmitDemo.cs“ hinzu, und befassen Sie sich dann mit der in Abbildung 7 gezeigten Codeauflistung. Wie Sie sehen können, wird ein „SyntaxTree“ aus dem Quelltext generiert, der eine Hilfsklasse definiert, die eine statische Methode enthält, die den Flächeninhalt eines Kreises berechnet. Das Ziel besteht darin, eine DLL-Datei aus dieser Klasse zu generieren und die CalculateCircleArea-Methode auszuführen, wobei der Radius als Argument übergeben wird.

Abbildung 7: Kompilieren und Ausführen von Code mit Ausgabe-APIs und Reflektion

using System;
using System.IO;
using System.Reflection;
using System.Runtime.Loader;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit;
namespace RoslynCore
{
  public static class EmitDemo
  {
    public static void GenerateAssembly()
    {
      const string code = @"using System;
using System.IO;
namespace RoslynCore
{
 public static class Helper
 {
  public static double CalculateCircleArea(double radius)
  {
    return radius * radius * Math.PI;
  }
  }
}";
      var tree = SyntaxFactory.ParseSyntaxTree(code);
      string fileName="mylib.dll";
      // Detect the file location for the library that defines the object type
      var systemRefLocation=typeof(object).GetTypeInfo().Assembly.Location;
      // Create a reference to the library
      var systemReference = MetadataReference.CreateFromFile(systemRefLocation);
      // A single, immutable invocation to the compiler
      // to produce a library
      var compilation = CSharpCompilation.Create(fileName)
        .WithOptions(
          new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))
        .AddReferences(systemReference)
        .AddSyntaxTrees(tree);
      string path = Path.Combine(Directory.GetCurrentDirectory(), fileName);
      EmitResult compilationResult = compilation.Emit(path);
      if(compilationResult.Success)
      {
        // Load the assembly
        Assembly asm =
          AssemblyLoadContext.Default.LoadFromAssemblyPath(path);
        // Invoke the RoslynCore.Helper.CalculateCircleArea method passing an argument
        double radius = 10;
        object result = 
          asm.GetType("RoslynCore.Helper").GetMethod("CalculateCircleArea").
          Invoke(null, new object[] { radius });
        Console.WriteLine($"Circle area with radius = {radius} is {result}");
      }
      else
      {
        foreach (Diagnostic codeIssue in compilationResult.Diagnostics)
        {
          string issue = $"ID: {codeIssue.Id}, Message: {codeIssue.GetMessage()},
            Location: {codeIssue.Location.GetLineSpan()},
            Severity: {codeIssue.Severity}";
          Console.WriteLine(issue);
        }
      }
    }
  }
}

Im ersten Teil erstellt der Code eine neue Kompilierung, die einen einzigen, unveränderlichen Aufruf des C#-Compilers darstellt. Das CSharpCompilation-Objekt ermöglicht das Erstellen einer Assembly mithilfe seiner Create-Methode, und mit „WithOptions“ können Sie angeben, welche Art von Ausgabe generiert werden soll. In diesem Fall handelt es sich um „DynamicallyLinkedLibrary“. „AddReferences“ wird zum Hinzufügen von Verweisen verwendet, die Ihr Code ggf. benötigt. Zu diesem Zweck müssen Sie einen Typ angeben, der die gleichen Verweise besitzt, die Ihr Code benötigt. In diesem besonderen Fall benötigen Sie einfach die gleichen Verweise, die der Objekttyp verwendet. Mit „Get­TypeInfo().Assembly.Location“ rufen Sie den Assemblynamen für den Verweis ab. Anschließend erstellt „MetadataReference.CreateFromFile“ einen Verweis auf die Assembly in der Kompilierung. Zum Schluss wird der Syntaxbaum der Kompilierung mit „AddSyntaxTrees“ hinzugefügt.

Im zweiten Teil des Codes versucht ein Aufruf von „CSharpCompilation.Emit“, die Binärdatei zu generieren, und gibt ein Objekt vom Typ „EmitResult“ zurück. Letzteres ist sehr interessant: Es wird eine Success-Eigenschaft vom Typ „bool“ bereitgestellt, die angibt, ob die Kompilierung erfolgreich war. Außerdem wird eine Eigenschaft namens „Diagnostics“ bereitgestellt, die ein unveränderliches Array von Diagnoseobjekten zurückgibt, das beim Verständnis sehr hilfreich sein kann, warum bei der Kompilierung ein Fehler aufgetreten ist. In Abbildung 7 können Sie leicht erkennen, wie die Diagnostics-Eigenschaft iteriert wird, wenn ein Kompilierungsfehler auftritt. Es muss unbedingt erwähnt werden, dass es sich bei der Ausgabeassembly um eine .NET Standard-Bibliothek handelt. Die Kompilierung des Quelltexts ist daher nur erfolgreich, wenn der mit Roslyn analysierte Code APIs verwendet, die in .NET Standard enthalten sind.

Sehen wir uns nun an, was passiert, wenn die Kompilierung erfolgreich ist. Der System.Runtime.Loader-Namespace, der im gleich benannten NuGet-Paket enthalten ist, das Sie am Anfang dieses Artikels importiert haben, stellt eine Singletonklasse namens „Assembly­LoadContext“ bereit, die eine Methode namens „LoadFromAssemblyPath“ bereitstellt. Diese Methode gibt eine Instanz der Assembly-Klasse zurück, die es ermöglicht, Reflektion zu verwenden, um zuerst einen Verweis auf die Hilfsklasse und dann einen Verweis auf die CalculateCircleArea-Methode abzurufen, die Sie durch Übergeben eines Werts für den radius-Parameter aufrufen können. Die MethodInfo.Invoke-Methode empfängt NULL als erstes Argument, weil „CalculateCircleArea“ eine statische Methode ist. Daher müssen Sie keine Typinstanz übergeben. Wenn Sie jetzt die GenerateAssembly-Methode aus „Main“ in „Program.cs“ aufrufen, sehen Sie das Ergebnis dieser Vorgehensweise wie in Abbildung 8 gezeigt. Das Ergebnis der Berechnung wird in der Debugkonsole angezeigt.

Das Ergebnis des Aufrufs über Reflektion von von Roslyn generiertem Code
Abbildung 8: Das Ergebnis des Aufrufs über Reflektion von von Roslyn generiertem Code

Wie Sie sich sicher vorstellen können, führen Ausgabe-APIs plus Reflektion in .NET Core zu großer Leistungsfähigkeit und Flexibilität, weil Sie C#-Code unabhängig vom Betriebssystem generieren, analysieren und ausführen können. Alle in diesem Artikel vorgestellten Beispiele können in der Tat nicht nur unter Windows, sondern auch unter macOS und den meisten Linux-Distributionen ausgeführt werden. Außerdem kann der Aufruf von Code aus einer Bibliothek mithilfe der Roslyn-Skripting-APIs erfolgen. Sie sind also nicht auf Reflektion eingeschränkt.

Zusammenfassung

.NET Core ermöglicht das Schreiben von C#-Code, um plattformübergreifende Anwendungen zu erstellen, die unter mehreren Betriebssystemen und auf mehreren Geräten ausgeführt werden können, weil die Compiler selbst plattformübergreifend sind. Roslyn, die .NET-Compilerplattform, dient als Grundlage für den C#-Compiler unter .NET Core und ermöglicht Entwicklern, die leistungsfähigen Codeanalyse-APIs zum Ausführen von Codegenerierung, -analyse und -kompilierung zu verwenden. Dies bedeutet, dass Sie Tasks automatisieren können, indem Sie Code bei Bedarf erstellen und ausführen, Quelltext bezüglich Codeproblemen analysieren und zahlreiche Aktivitäten für Quellcode ausführen: unter Windows, macOS und Linux.


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 Senior .NET-Entwickler mit dem Schwerpunkt auf .NET und der Entwicklung mobiler Apps, Training und Beratung. Sie können ihm auf Twitter folgen: @progalex.

Unser Dank gilt dem folgenden technischen Experten bei Microsoft für die Durchsicht dieses Artikels: Dustin Campbell
Dustin Campbell ist Principal Engineer bei Microsoft und Mitglied des Entwurfsteams für die Sprache C#. Dustin hat von Beginn an an Roslyn gearbeitet und ist zurzeit für die C#-Erweiterung für Visual Studio Code verantwortlich.