May 2017

Volume 32 Number 5

[.NET Core]

Cross-Platform Code Generation with Roslyn and .NET Core

By Alessandro Del

.NET Core is the modular, open source and cross-platform set of tools that allows you to build next-generation .NET applications, which run on Windows, Linux and macOS (microsoft.com/net/core/platform). It can also be installed on the Windows 10 for IoT distribution, and it runs on devices such as the Raspberry PI. .NET Core is a powerful platform that includes the runtime, libraries, and compilers, with full support for languages such as C#, F#, and Visual Basic. This means you can code in C# not only on Windows, but also on different OSes because the .NET Compiler Platform (github.com/dotnet/roslyn), also referred to as “Project Roslyn,” provides open source, cross-platform compilers with rich code analysis APIs. As an important implication, you can leverage the Roslyn APIs to perform many code-related operations on different OSes, such as code analysis, code generation and compilation. This article walks through the necessary steps to set up a C# project on .NET Core to use the Roslyn APIs and explains some interesting code-generation and compilation scenarios. It also discusses some basic Reflection techniques to invoke and run code compiled with Roslyn on .NET Core. If you’re unfamiliar with Roslyn, you’ll want to read the following articles first:

Installing the .NET Core SDK

The first step is installing .NET Core and the SDK. If you work on Windows and you’ve installed Visual Studio 2017, .NET Core is already included if the .NET Core cross-platform development workload was selected at installation time in the Visual Studio Installer. If not, simply open the Visual Studio Installer, select the workload and click Modify. If you’re working on Windows but not relying on Visual Studio 2017, or you’re working on Linux or macOS, you can install .NET Core manually and use Visual Studio Code as the development environment (code.visualstudio.com). The latter is the scenario I’ll discuss in this article, as Visual Studio Code is cross-platform itself; thus, it’s a great companion for .NET Core. Also, remember to install the C# extension for Visual Studio Code (bit.ly/29b1Ppl). The steps to install .NET Core are different depending on the OS, so follow the instructions at bit.ly/2mJArWx. Make sure you install the latest release. It’s worth mentioning that the latest releases of .NET Core no longer support the project.json file format, but instead support the more common .csproj file format with MSBuild.

Scaffolding a .NET Core Application in C#

With .NET Core, you can create Console applications and Web applications. For Web applications, Microsoft is making more templates available, besides the ASP.NET Core template, as .NET Core goes forward on its roadmap. Because Visual Studio Code is a lightweight editor, it doesn’t provide project templates as Visual Studio does. This means you need to create an application from the command line inside a folder whose name will also be the application name. The following example is based on instructions for Windows, but the same concepts apply to macOS and Linux. To get started, open a command prompt and move to a folder on your disk. For instance, say you have a folder called C:\Apps, go to this folder and create a new subfolder called RoslynCore, using the following commands:

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

Therefore, RoslynCore will be the name of the sample application discussed in this article. It will be a Console application, which is perfect for instructional purposes and simplifies the approach to coding with Roslyn. You can also use the same techniques in ASP.NET Core Web applications. To create a new, empty project for a Console application, simply type the following command line:

> dotnet new console

In this way, .NET Core scaffolds a C# project for a Console application called RoslynCore. Now you can open the project’s folder with Visual Studio Code. The easiest way is typing the following command line:

> code .

Of course, you could open Visual Studio Code from the Windows Start menu and then open a project folder manually. Once you enter any C# code file, it will ask your permission to generate some required assets and to restore some NuGet packages (see Figure 1).

Visual Studio Code Needs to Update the Project
Figure 1 Visual Studio Code Needs to Update the Project

The next step is adding the NuGet packages necessary for working with Roslyn.

Adding the Roslyn NuGet Packages

As you might know, the Roslyn APIs can be consumed by installing some NuGet packages from the Microsoft.CodeAnalysis hierarchy. Before installing these packages, it’s important to clarify how the Roslyn APIs fit into the .NET Core system. If you’ve ever worked with Roslyn on the .NET Framework, you might be used to taking advantage of the full set of Roslyn APIs. .NET Core relies on .NET Standard libraries, which means that only the Roslyn libraries that support .NET Standard can be consumed in .NET Core. At the time of this writing, most of the Roslyn APIs are already available to .NET Core, including (but not limited to) the Compiler APIs  (with the Emit and Diagnostic APIs) and the Workspaces APIs. Only a few APIs are not yet portable, but Microsoft is investing hugely in Roslyn and .NET Core, so it’s reasonable to expect full .NET Standard compatibility in future releases. A real-world example of a cross-platform application that runs on .NET Core is OmniSharp (bit.ly/2mpcZeF), which leverages the Roslyn APIs to empower most of the code editor features, such as completion lists and syntax highlighting.

In this article, you’ll see how to leverage the Compiler and the Diagnostic APIs. To accomplish this, you need to add the Microsoft.CodeAnalysis.CSharp NuGet package to your project. With the new .NET Core project system based on MSBuild, the list of NuGet packages is now included in the .csproj project file. In Visual Studio 2017, you can use the client UI for NuGet to download, install and manage packages, but in Visual Studio Code there’s no equivalent option. Fortunately, you can simply open the .csproj file and locate the <ItemGroup> node that contains <PackageReference> elements, each representing a required NuGet package. Modify the node as follows:

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

Note that adding a reference to the Microsoft.CodeAnalysis.C­Sharp package allows you to access the C# compiler’s APIs, and that the System.Runtime.Loader package is required for Reflection and will be used later in the article.

When you save your changes, Visual Studio Code will detect missing NuGet packages and will offer to restore them.

Code Analysis: Parsing Source Text and Generating Syntax Nodes

The first example is about code analysis, and demonstrates how to parse source code text and generate new syntax nodes. For instance, imagine you have the following simple business object and you want to generate a View Model class based on it:

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

The text for this business object might come from different sources, such as a C# code file or a string in your code or even from user input. With the code analysis APIs, you can parse the source text and generate a new syntax node that the compiler can understand and manipulate. For example, consider the code shown in Figure 2, which parses a string containing a class definition, gets its corresponding syntax node and calls a new static method that generates a View Model from the syntax node.

Figure 2 Parsing Source Code and Retrieving a Syntax Node

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();
  }
}

The GenerateViewModel method is going to be defined in a static class called ViewModelGeneration, so add a new file called ViewModelGeneration.cs to the project. The method looks for a class definition in the input syntax node (for demonstration purposes, the first instance of a ClassDeclarationSyntax object), then constructs a new View Model based on the class’s name and members. Figure 3 demonstrates this.

Figure 3 Generating a New Syntax Node

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

In the first part of the code in Figure 3, you can see how the View Model is first represented as a string, with string interpolation that makes it easy to specify object and member names based on the original class name. In this sample scenario, plurals are generated just by adding an “s” to the object/member name; in real-world code you should use more specific pluralization algorithms.

In the second part of Figure 3, the code invokes CSharpSyntaxTree.ParseText to parse the source text into a SyntaxTree. GetRoot is invoked to retrieve the SyntaxNode for the new tree; with DescendantNodes().OfType<ClassDeclarationSyntax>(), the code retrieves only the syntax nodes that represent a class, selecting only the first one with FirstOrDefault. Retrieving the first class in the syntax node is enough to get the parent namespace where the new View Model class will be inserted. Obtaining a namespace is possible by casting the Parent property of a ClassDeclarationSyntax into a NamespaceDeclarationSyntax object. Because a class could be nested into another class, the code first checks for this possibility by verifying that Parent is of type NamespaceDeclarationSyntax. The final piece of the code adds the new syntax node for the View Model class to the parent namespace, returning this as a syntax node. If you now press F5, you’ll see the result of the code generation in the Debug Console, as shown in Figure 4.

The View Model Class Has Been Correctly Generated
Figure 4 The View Model Class Has Been Correctly Generated

The generated View Model class is a SyntaxNode that the C# compiler can work with, so it can be further manipulated, analyzed for diagnostic information, compiled into an assembly with the Emit APIs and utilized via Reflection.

Getting Diagnostic Information

Whether source text comes from a string, a file or user input, you can take advantage of the Diagnostic APIs to retrieve diagnostic information about code issues such as errors and warnings. Remember that the Diagnostic APIs not only allow for retrieving errors and warnings, they also allow writing analyzers and code refactorings. Continuing the previous example, it’s a good idea to check for syntactic errors in the original source text before attempting to generate a View Model class. To accomplish this, you can invoke the SyntaxNode.GetDiagnostics method, which returns an IEnumerable<Microsoft.CodeAnalysis.Diagnostic> object, if any. Take a look at Figure 5, which offers an extended version of the ViewModelGeneration class. The code checks if the result of invoking GetDiagnostics contains any diagnostics. If not, the code generates the View Model class. If instead the result contains a collection of diagnostics, the code shows information for each diagnostic and returns null. The Diagnostic class provides very granular information about each code issue; for example, the Id property returns a diagnostic id; the GetMessage method returns the full diagnostic message; GetLineSpan returns the diagnostic position in the source code; and the Severity property returns the diagnostic severity, such as Error, Warning or Information.

Figure 5 Checking for Code Issues with the Diagnostic 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;
      }
    }
  }
}

Now, if you introduce some intentional errors into the source text contained in the models variable, inside the GenerateSample­ViewModel method in Program.cs, and then run the application, you’ll be able to see how the C# compiler returns full details about every code issue. Figure 6 shows an example.

Detecting Code Issues with the Diagnostic APIs
Figure 6 Detecting Code Issues with the Diagnostic APIs

It’s worth noting that the C# compiler produces a syntax tree even if it contains diagnostics. Not only does this result in full fidelity with the source text, it also gives developers an option to fix those issues with new syntax nodes.

Executing Code: the Emit APIs

The Emit APIs allow for compiling source code into assemblies. Then, with Reflection you can invoke and execute code. The next example is a combination of code generation, emit and diagnostic detection. Add a new file called EmitDemo.cs to the project, then consider the code listing shown in Figure 7. As you can see, a SyntaxTree is generated from source text that defines a Helper class containing a static method that calculates the area of a circle. The goal is to produce a .dll from this class and execute the CalculateCircleArea method, passing the radius as an argument.

Figure 7 Compiling and Executing Code with Emit APIs and Reflection

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

In the first part, the code creates a new compilation that represents a single, immutable invocation to the C# compiler. The CSharpCompilation object allows the creation of an assembly through its Create method, and WithOptions lets you specify what kind of output to produce, in this case DynamicallyLinkedLibrary. AddReferences is used to add any references your code might need; to accomplish this, you must supply a type that has the same references needed by your code. In this particular case, you simply need the same references on which the object type relies. With Get­TypeInfo().Assembly.Location you retrieve the assembly name for the reference, then MetadataReference.CreateFromFile creates a reference to the assembly inside the compilation. In the end, the syntax tree is added to the compilation with AddSyntaxTrees.

In the second part of the code, an invocation to CSharpCompilation.Emit attempts to produce the binary and returns an object of type EmitResult. The latter is very interesting: It exposes a Success property of type bool that indicates whether or not the compilation succeeded, and it also exposes a property called Diagnostics, which returns an immutable array of Diagnostic objects that can be very useful to understand why the compilation failed. In Figure 7, you can easily see how the Diagnostics property is iterated if the compilation fails. It’s important to mention that the output assembly is a .NET Standard Library, so compiling source text will succeed only if the code parsed with Roslyn relies on APIs that are included in .NET Standard.

Now let’s look at what happens if the compilation succeeds. The System.Runtime.Loader namespace, included in the same-named NuGet package you imported at the beginning of the article, exposes a singleton class called Assembly­LoadContext, which exposes a method called LoadFromAssemblyPath. This method returns an instance of the Assembly class, which allows you to use Reflection to first get a reference to the Helper class, then to get a reference to the CalculateCircleArea method, which you can invoke by passing a value for the radius parameter. The MethodInfo.Invoke method receives null as the first argument because CalculateCircleArea is a static method; therefore, you don’t need to pass any type instance. If you now call the GenerateAssembly method from Main in Program.cs, you’ll see the result of this work, as demonstrated in Figure 8, where the result of the calculation is visible in the Debug Console.

The Result of the Invocation via Reflection of Roslyn-Generated Code
Figure 8 The Result of the Invocation via Reflection of Roslyn-Generated Code

As you can imagine, Emit APIs plus Reflection in .NET Core give you great power and flexibility, because you can generate, analyze and execute C# code regardless of the OS. In fact, all of the examples discussed in this article will certainly run not only on Windows, but also on macOS and most of the Linux distributions. Additionally, invoking code from a library can be accomplished using the Roslyn Scripting APIs, so you’re not limited to Reflection.

Wrapping Up

.NET Core allows you to write C# code to create cross-platform applications that run on multiple OSes and devices, and this is because compilers are cross-platform themselves. Roslyn, the .NET Compiler Platform, empowers the C# compiler on .NET Core and allows developers to leverage the rich code analysis APIs to perform code generation, analysis and compilation. This implies that you can automate tasks by generating and executing code on the fly, analyze source text for code issues, and perform a large number of activities on source code—on Windows, macOS, and Linux.


Alessandro Del Sole has been a Microsoft MVP since 2008. Awarded MVP of the Year five times, he has authored many books, eBooks, instructional videos and articles about .NET development with Visual Studio. Del Sole works as a senior .NET developer, focusing on .NET and mobile app development, training and consulting. You can follow him on Twitter: @progalex.

Thanks to the following Microsoft technical expert for reviewing this article: Dustin Campbell
Dustin Campbell is a principal engineer at Microsoft and a member of the C# language design team. Dustin worked on Roslyn from its inception and is currently responsible for the C# extension for Visual Studio Code.


Discuss this article in the MSDN Magazine forum