left-icon

Roslyn Succinctly®
by Alessandro Del Sole

Previous
Chapter

of
A
A
A

CHAPTER 8

Workspaces, Code Generation, Emit

Workspaces, Code Generation, Emit


The .NET Compiler Platform is not just about analyzers and refactorings. In Chapter 3, you were introduced to the Workspaces APIs and the compiler pipeline, which includes the emit phase. This chapter provides a short introduction to the Workspaces APIs, and how you can implement code generation and emit an assembly.

Getting Started with Workspaces

The Workspaces APIs allow you to interact with everything that makes up an MSBuild solution, including projects, code files, and metadata, with objects exposed by the .NET Compiler Platform. Most of the necessary types are defined inside the Microsoft.CodeAnalysis.Workspaces.dll assembly, plus libraries that are tailored for a specific language, such as Microsoft.CodeAnalysis.CSharp.Workspaces.dll and Microsoft.CodeAnalysis.VisualBasic.Workspaces.dll.

In this chapter, you’ll create a sample project that opens a solution (.sln file) and lists all the projects in that solution, plus all the code files (documents) and references of each project. For an easier starting point, you can take advantage of a project template called Stand-Alone Code Analysis Tool, which generates a console application with all the necessary references to the Roslyn APIs. Of course, you can manually add a reference to the Microsoft.CodeAnalysis.dll library in any project. This project template is located in the Extensibility node under the language of your choice, as shown in Figure 56.

Creating a stand-alone code analysis application

Figure 56: Creating a stand-alone code analysis application

To interact with a solution, you need to create an instance of the Microsoft.CodeAnalysis.MSBuild.MSBuildWorkspace class, which represents a workspace that can be populated with MSBuild solutions and projects. This class exposes a method called Create, which generates the workspace, and a number of methods to interact with solutions and projects, such as OpenSolutionAsync and OpenProjectAsync.

Tip: Using IntelliSense to discover the full list of available methods is almost always the best approach. The method names are self-explanatory, so understanding the purpose of each one will not be difficult.

Code Listing 19 demonstrates how to open an existing solution on disk, but remember to change the solution path to one existing on your machine.

Code Listing 19 (C#)

        static void Main(string[] args)

        {

            // Path to an existing solution

            string solutionPath =

                   "C:\\temp\\RoslynSolution\\RoslynSolution.sln";

            // Create a workspace

            var ws = Microsoft.CodeAnalysis.

                MSBuild.MSBuildWorkspace.

                Create();

            // Open a solution

            var solution =

                ws.OpenSolutionAsync(solutionPath).Result;

            // Invoke code to iterate items

            // in the solution

            // using a program-defined method

            IterateSolution(solution,

                solutionPath);

        }

Code Listing 19 (VB)

    Sub Main()

        'Path to an existing solution

        Dim solutionPath =

            "C:\temp\RoslynSolution\RoslynSolution.sln"

        'Create a workspace

        Dim ws =

            MSBuild.

            MSBuildWorkspace.Create()

        'Open a solution

        Dim solution =

            ws.OpenSolutionAsync(

            solutionPath).Result

        'Invoke code to iterate

        'items in the solution

        'using a program-defined method

        IterateSolution(

            solution, solutionPath)

    End Sub

The invocation to OpenSolutionAsync returns an object of type Solution, which represents an MSBuild solution. Similarly, invoking OpenProjectAsync would result in an object of type Project. Solution exposes a property called Result, of the same type, which returns the actual instance of the solution. The next step is iterating the list of projects in the solution and the list of documents and references for each project. To understand what the next piece of code will do, first take a look at Figure 57, which shows the result based on a sample C# WPF application.

Iterating properties of an MSBuild solution

Figure 57: Iterating properties of an MSBuild solution

As you can see, you need some code that displays information about the solution, the list of projects, and the code files and references of each project. The code required to perform this is shown in Code Listing 20.

Code Listing 20 (C#)

        static void IterateSolution(Solution solution,

                    string solutionPath)

        {

            // Print solution's path and version

            Console.WriteLine(

                $"Solution {System.IO.Path.GetFileName(solutionPath)},

                  version {solution.Version.ToString()}");

            // For each project...

            foreach (var prj in

                     solution.Projects)

            {

                // Print the name and version

                Console.WriteLine(

                    $"Project name: {prj.Name}, version:

                    {prj.Version.ToString()}");

                // Then print the number of code files

                Console.WriteLine(

                    $" {prj.Documents.Count()} code files:");

                // For each code file, print the file name

                foreach (var codeFile in

                         prj.Documents)

                {

                    Console.

                    WriteLine($"     {codeFile.Name}");

                }

                Console.WriteLine(" References:");

                // For each reference in the project

                // Print the name

                foreach (var reference in

                         prj.MetadataReferences)

                {

                    Console.WriteLine(

                 $"    {System.IO.Path.GetFileName(reference.Display)}");

                }

            }

            Console.ReadLine();

        }

Code Listing 20 (VB)

    Private Sub IterateSolution(

                solution As Solution,

                solutionPath As String)

        'Print solution's path and version

        Console.WriteLine(

            $"Solution {IO.Path.

            GetFileName(solutionPath)},

            version {solution.Version.ToString}")

        'For each project...

        For Each prj In solution.Projects

            'Print the name and version

            Console.

            WriteLine(

            $"Project name: {prj.Name}, version: {prj.Version.ToString}")

            'Then print the number of code files

            Console.

            WriteLine($" {prj.Documents.Count} code files:")

            'For each code file, print the file name

            For Each codeFile In

                prj.Documents

                Console.

                WriteLine($"     {codeFile.Name}")

            Next

            Console.

            WriteLine(" References:")

            'For each reference in the project

            'Print the name

            For Each ref In

                prj.MetadataReferences

                Console.

                WriteLine($"    {IO.Path.GetFileName(ref.Display)}")

            Next

            Console.WriteLine("")

        Next

        Console.ReadLine()

    End Sub

Among others, the Solution class exposes the Version property, which represents the solution’s file version number. It also exposes the Projects property, which is a collection of Project objects, each representing a project in the solution. The Project class exposes a property called Documents, a collection of Document objects, each representing a code file in the project. Project also exposes the MetadataReferences property, which represents a collection of assembly references, including CLR libraries.

Each reference is represented by an object of type MetadataReference, whose Display property contains the assembly name. This example just shows the surface of the power of the Workspaces APIs, but it gives you an idea of how you can work with solutions, projects, and documents in a .NET way. You might also want to consider the AdHocWorkspace class, which also offers members to create projects, documents, and code files, and add them to a solution.

Code Generation and Emit

Note: This section requires you to have knowledge of reflection in .NET. For the code blocks that use reflection, only the Roslyn-related snippets will be discussed in detail.

With the Roslyn APIs, you can take source code, compile it, and emit an assembly. Source code can be pure-text parsed to a syntax tree or an existing syntax tree. Then you can use the Compilation class to perform an invocation to the compiler.

In this chapter, you will see how to generate a syntax tree from pure source text, and then how to use the Compilation class to invoke the compiler for emitting an assembly. More specifically, you will write source code that checks if a text file exists, printing its content to the Console window. You will also see how to interact with diagnostics by causing intentional errors.

Before you start, create a new console application based on the Stand-alone Code Analysis Tool project template (see Figure 56) in the language of your choice. Actually, Compilation is an abstract class and the base type for the VisualBasicCompilation and CSharpCompilation classes, which are tailored for VB and C#. As you know, the Microsoft.CodeAnalysis namespace exposes the VisualBasic and CSharp classes, which offer members that are tailored to a language’s specifications. Both offer an option to represent a source document under the form of a syntax tree via the VisualBasicSyntaxTree and CSharpSyntaxTree objects. Both classes expose a method called ParseText that you invoke to generate a syntax tree from pure source text. Code Listing 21 shows how to parse source text and generate a syntax tree.

Code Listing 21 (C#)

        private static void GenerateCode()

        {

            SyntaxTree syntaxTree =

                CSharpSyntaxTree.ParseText(@"

    using System;

    using System.IO;

    namespace RoslynSuccinctly

    {

        public class Helper

        {

            public void PrintTextFromFile(string fileName)

            {

                if (File.Exists(fileName) == false)

                {

                    Console.WriteLine(""File does not exist"");

                    return;

            }

                using (StreamReader str = new StreamReader(fileName))

                {

                    Console.WriteLine(str.ReadToEnd());

                }

            }

        }

    }");

    // more code goes here...

} //end GenerateCode()

Code Listing 21 (VB)

    'Generate a syntax tree

    'from source text

    Private Sub GenerateCode()

        Dim tree = VisualBasicSyntaxTree.ParseText("

Imports System

Imports System.IO

Namespace RoslynSuccinctly

    Public Class Helper

        Public Sub PrintTextFromFile(fileName As String)

            If File.Exists(fileName) = False Then

                Console.WriteLine(""File does not exist"")

                Exit Sub

            End If

            Using str As New StreamReader(fileName)

                Console.WriteLine(str.ReadToEnd())

            End Using

        End Sub

    End Class

End Namespace")

The next step is preparing the assembly’s metadata. This requires getting an assembly name, which is accomplished via the System.IO.Path.GetRandomFileName method for demonstration purposes, and creating a list of references that the assembly will rely on. The list of references must be an array of MetadataReference objects. MetadataReference exposes a method called CreateFromFile. If you pass a type to this method, it will be able to determine the assembly files that contain the corresponding namespace definitions, and get a reference. In this case, the sample code uses objects from the System and System.IO namespaces, so to get a reference to the containing assemblies, you can pass Object and File to each MetadataReference in the array. This is demonstrated in Code Listing 22.

Code Listing 22 (C#)

            //Get a random file name for

            //the output assembly

            string outputAssemblyName =

                System.IO.Path.GetRandomFileName();

            //Add a list of references from assemblies

            //By a type name, get the assembly ref

            MetadataReference[] referenceList =

                new MetadataReference[]

                {

                    MetadataReference.

                    CreateFromFile(typeof(object).

                    Assembly.Location),

                    MetadataReference.

                    CreateFromFile(typeof(File).

                    Assembly.Location)

                };

Code Listing 22 (VB)

        'Get a random file name for

        'the output assembly

        Dim outputAssemblyName As String =

            Path.GetRandomFileName()

        'Add a list of references from assemblies

        'By a type name, get the assembly ref

        Dim referenceList As MetadataReference() =

            New MetadataReference() _

            {MetadataReference.

            CreateFromFile(GetType(Object).

            Assembly.Location),

            MetadataReference.

            CreateFromFile(GetType(File).

            Assembly.Location)}

The next step is creating a new compilation. You invoke the Create method to invoke the compiler, passing as arguments the new assembly name, an array of syntax trees, an array of references, and compilation options. This is demonstrated in Code Listing 23.

Code Listing 23 (C#)

            //Single invocation to the compiler

            //Create an assembly with the specified

            //syntax trees, references, and options

            CSharpCompilation compilation =

                CSharpCompilation.Create(

                outputAssemblyName,

                syntaxTrees: new[] { syntaxTree },

                references: referenceList,

                options: new CSharpCompilationOptions(

                    OutputKind.DynamicallyLinkedLibrary));

Code Listing 23 (VB)

        'Single invocation to the compiler

        'Create an assembly with the specified

        'syntax trees, references, and options

        Dim compilation As VisualBasicCompilation =

            VisualBasicCompilation.

            Create(outputAssemblyName,

                   syntaxTrees:=New SyntaxTree() {tree},

                   references:=referenceList,

                   options:=New VisualBasicCompilationOptions(

                   OutputKind.DynamicallyLinkedLibrary))

You can incrementally add new syntax trees or references via the AddSyntaxTrees and AddReferences methods. Notice how the CSharpCompilationOptions and VisualBasicCompilationOptions types allow specifying, among others, the output type for the assembly.

The next step is emitting the IL code, which is accomplished by invoking the Emit method over the Compilation instance. If the emit phase fails, the code will iterate a list of diagnostics and print their messages. The EmitResult type, which is the type returned by Emit, exposes an ImmutableArray<Diagnostic>, which contains a list of diagnostics that exist in the code. Code Listing 24 demonstrates this.

Code Listing 24 (C#)

            //Create a stream

            using (var ms = new MemoryStream())

            {

                //Emit the IL code into the stream

                EmitResult result = compilation.Emit(ms);

                //If emit fails,

                if (!result.Success)

                {

                    //Query the list of diagnostics

                    //in the source code

                    IEnumerable<Diagnostic> diagnostics =

                        result.Diagnostics.Where(diagnostic =>

                        diagnostic.IsWarningAsError ||

                        diagnostic.Severity ==

                        DiagnosticSeverity.Error);

                    //Write ID and message for each diagnostic

                    foreach (Diagnostic diagnostic in

                        diagnostics)

                    {

                        Console.Error.

                            WriteLine("{0}: {1}",

                            diagnostic.Id,

                            diagnostic.

                            GetMessage());

                    }

                }

                else

                {

                    //If emit succeeds, move to

                    //the beginning of the assembly

                    ms.Seek(0,

                        SeekOrigin.Begin);

                    //Load the generated assembly

                    //into memory

                    Assembly inputAssembly =

                        Assembly.Load(ms.ToArray());

                    //Get a reference to the type

                    //defined in the syntax tree

                    Type typeInstance =

                        inputAssembly.

                        GetType("RoslynSuccinctly.Helper");

                    //Create an instance of the type

                    object obj =

                        Activator.CreateInstance(typeInstance);

                    //Invoke the method. Replace MyFile.txt with an

                    //existing file name

                    typeInstance.

                        InvokeMember("PrintTextFromFile",

                        BindingFlags.Default |

                        BindingFlags.InvokeMethod,

                        null,

                        obj,

                        new object[]

                        { "C:\\MyFile.txt" });

                }

            }

        } //end GenerateCode()

Code Listing 24 (VB)

        'Create a stream

        Using ms As New MemoryStream()

            'Emit the IL code into the

            'stream

            Dim result As EmitResult =

                compilation.Emit(ms)

            'If emit fails,

            If Not result.Success Then

                'Query the list of diagnostics in the source code

                Dim diagnostics As _

                    IEnumerable(Of Diagnostic) =

                    result.Diagnostics.Where(Function(diagnostic) _

                    diagnostic.IsWarningAsError _

                    OrElse diagnostic.Severity =

                    DiagnosticSeverity.[Error])

                'Write ID and message for each diagnostic

                For Each diagnostic As _

                    Diagnostic In diagnostics

                    Console.Error.WriteLine("{0}: {1}",

                                              diagnostic.Id,

                                              diagnostic.GetMessage())

                Next

            Else

                'If emit succeeds, move to

                'the beginning of the assembly

                ms.Seek(0, SeekOrigin.Begin)

                'Load the generated assembly

                'into memory

                Dim inputAssembly As Assembly =

                    Assembly.Load(ms.ToArray())

                'Get a reference to the type

                'defined in the syntax tree

                Dim typeInstance As Type =

                    inputAssembly.

                    GetType("RoslynSuccinctly.Helper")

                'Create an instance of the type

                Dim obj As Object =

                    Activator.

                    CreateInstance(typeInstance)

                'Invoke the method. Replace MyFile.txt with an existing

                'file name

                typeInstance.

                    InvokeMember("PrintTextFromFile",

                                 BindingFlags.Default Or

                                 BindingFlags.InvokeMethod,

                                 Nothing, obj,

                                 New Object() _

                                 {"C:\MyFile.txt"})

            End If

        End Using

    End Sub

To test the code, invoke the GenerateCode method in the Main method. If you supply the name of an existing text file, you will see how its content will be printed to the Console window. This is possible because the source text you wrote is parsed into a syntax tree and compiled into a .dll assembly. Other than testing how the code properly works against an existing text file, you can try causing intentional errors in the source text and then run the code. Because the compiler’s job is not only generating IL code but also performing code analysis, at that point it will report the proper diagnostics; these can be analyzed and utilized as required. For example, you can change Writeline to Witeline and StreamReader to SreamReader. Figure 58 shows the result of the code analysis.

The result of code analysis over Roslyn-generated source code

Figure 58: The result of code analysis over Roslyn-generated source code

In summary, the Roslyn APIs are very powerful and useful, not only to generate and compile code, but also for performing code analysis over the source code.

Chapter Summary

The .NET Compiler Platform is not just analyzers and code refactorings. In this chapter, you were given an overview of the Workspaces APIs and the Emit APIs. You first saw how to interact with MSBuild solutions by using workspaces and the Solution, Project, and Document objects. In the second part, you saw how you can leverage the Compilation class to generate an assembly based on a parsed syntax tree, how you can invoke types from the resulting library, and how you can investigate diagnostics in the source code.

Scroll To Top
Disclaimer
DISCLAIMER: Web reader is currently in beta. Please report any issues through our support system. PDF and Kindle format files are also available for download.

Previous

Next



You are one step away from downloading ebooks from the Succinctly® series premier collection!
A confirmation has been sent to your email address. Please check and confirm your email subscription to complete the download.