Using the Roslyn APIs to Analyse a .NET Solution

In a previous post “Getting Started with the Roslyn APIs: Writing Code with Code“, I demonstrated a relatively simple way to generate code using the Roslyn APIs. In this post, I want to revisit the topic from a fresh angle and demonstrate the foundations for achieving a slightly different goal.

What is Roslyn?

As a reminder, Roslyn is the name for the .NET compiler platform, which includes compilers for both C# and VB.NET and various APIs and tools. The APIs are extremely powerful and can be used to understand existing code and generate additional code. You may have recently heard about Source Generators in C#, which allow compile-time code analysis and code generation. These optimise areas that traditionally relied on runtime reflection and much boilerplate code such as JSON serialisation and Regex. These offer both performance and code maintenance improvements.

Analysing an Existing Solution

For this post, I wanted to focus on how we can begin to leverage the Roslyn APIs to analyse existing code. I’m exploring this currently as I plan future improvements to my code generator for the Elasticsearch .NET v8 client. Today, I generate a large portion of the types needed to model the requests and responses for endpoints in the client. The current process works but is quite brutal in its approach. Before regenerating everything from the schema, each run deletes the existing target folder for the generated C# files. After generating code, I can use the git diff to review changes before merging them.

In the future, the code generator can be much more intelligent. What if, instead of starting fresh each time, it could analyse the existing code, determine what (if any) changes to make to each type, and update accordingly. A significant advantage of this is that the generator could be aware of any breaking changes it may introduce and report these for quick review.

To support this concept, I’ve begun to experiment in my own time with how to leverage the APIs to begin to analyse code. In this post, we’ll start by attempting to analyse the projects contained in a target solution. I’ll demonstrate two gotchas you may encounter and how I overcame them in my initial prototype. The plan is to continue this series with more valuable tasks in the future.

Getting Started with a MsBuildWorkspace

To keep things simple, I began by creating a small .NET solution to act as a target for the analysis. This solution begins with a single class library project containing a single class. The structure is as follows:

Next, I created a new .NET 6 console application using the top-level statements template.

Before adding any code, we need to reference the Roslyn API library that supports analysing an existing solution. Roslyn includes the concept of workspaces that provide a logical container for a collection of solution, project and code related information and documents. IDEs such as Visual Studio load a workspace for the currently open solution and layer on other Roslyn APIs for code analysis, code completion, automated fixers etc. We can use this same API surface programmatically outside of an IDE, offering the same powerful constructs and abilities.

Various types of Workspace exist which serve different needs. I use an AdhocWorkspace as a starting point for my current code generator to generate source code files for various types. In this new scenario, we want to analyse an existing .NET solution. Solutions are used to logically group and work on a set of (zero or more) .NET projects. To support analysing and working with existing solutions, we can use a specific MsBuildWorkspace API.

To reference this, we can add a package reference to the console application serving as our “host” for analysing our target solution.

<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="4.1.0" />

We’ll need some other packages soon, but for now, we’ll begin with this so I can demonstrate a few gotchas that it’s pretty easy to run into.

Back in our program.cs file, we can write some initial code that would open the solution and enumerate the projects that it contains.

using Microsoft.CodeAnalysis.MSBuild;

const string targetPath = @"e:\Projects\roslyn-playground\target\Sample.sln";

var workspace = MSBuildWorkspace.Create();

var sln = await workspace.OpenSolutionAsync(targetPath);

foreach (var project in sln.Projects)
{
    Console.WriteLine(project.AssemblyName);
}

This code includes a using directive for the Microsoft.CodeAnalysis.MSBuild namespace. It defines a string constant for the full path to the target solution file.

It then creates an instance of an MsBuildWorkspace and calls its OpenSolutionAsync method, passing the solution path as the only argument. This returns a Solution instance that contains some top-level properties for the solution. Inside the foreach loop, it enumerates a collection of projects inside the solution, writing the project assembly name to the console.

We can attempt to execute this code, but it will likely fail with an exception when trying to create the workspace.

Unhandled exception. System.Reflection.ReflectionTypeLoadException: Unable to load one or more of the requested types.
Could not load file or assembly 'Microsoft.Build.Framework, Version=15.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'. The system cannot find the file specified.
...

This ReflectionTypeLoadException is fairly terminal, and the cause and fix may not be immediately apparent. The problem is that the library needs to locate an instance of MsBuild on your system. We’ve provided no hints as to where this is located as it may be installed in various ways.

Fortunately, this problem can be overcome by referencing an additional helper package:

<PackageReference Include="Microsoft.Build.Locator" Version="1.4.1" />

As the name suggests, this package includes a library that knows how to locate a suitable MsBuild version, assuming one is installed somewhere on your developer machine. Not only that, but it configures things so that we can use the MsBuildWorkspace APIs correctly.

We need to add one line to our program before creating the workspace, requiring one additional using directive.

using Microsoft.Build.Locator;
using Microsoft.CodeAnalysis.MSBuild;

const string targetPath = @"e:\Projects\roslyn-playground\target\Sample.sln";

MSBuildLocator.RegisterDefaults();

var workspace = MSBuildWorkspace.Create();

var solution = await workspace.OpenSolutionAsync(targetPath);

foreach (var project in solution.Projects)
{
    Console.WriteLine(project.AssemblyName);
}

MSBuildLocator.RegisterDefaults() does all of the heavy lifting here and sets things up as we need them.

At this point, we can attempt to rerun the application. We should no longer see the ReflectionTypeLoadException, and the application should run to completion. However, you may notice that the console output is empty, and we don’t see the assembly name of the Sample.Library project in the output. Why is this?

Well, the MsBuild workspace is designed to work with either C# or Visual Basic projects. However, we require an extra package reference for the types of project we expect to work with. In my example, it’s a C# project in the solution, so we need to import the corresponding library that knows how to work with C# projects.

<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.1.0" />

We can attempt to rerun the application with no additional code changes now that this package is referenced. This time, we do see the assembly name for our project written to the console.

Sample.Library

This isn’t particularly thrilling at this stage, but we’ve laid the groundwork to start a further analysis of the projects, documents, and types defined within the project. That’s a subject for another post (or two, or many)!


Have you enjoyed this post and found it useful? If so, please consider supporting me:

Buy me a coffeeBuy me a coffee Donate with PayPal

Steve Gordon

Steve Gordon is a Pluralsight author, 7x Microsoft MVP, and a .NET engineer at Elastic where he maintains the .NET APM agent and related libraries. Steve is passionate about community and all things .NET related, having worked with ASP.NET for over 21 years. Steve enjoys sharing his knowledge through his blog, in videos and by presenting talks at user groups and conferences. Steve is excited to participate in the active .NET community and founded .NET South East, a .NET Meetup group based in Brighton. He enjoys contributing to and maintaining OSS projects. You can find Steve on most social media platforms as @stevejgordon