Skip to content

Instantly share code, notes, and snippets.

@DustinCampbell
Created April 10, 2018 21:36
Show Gist options
  • Save DustinCampbell/32cd69d04ea1c08a16ae5c4cd21dd3a3 to your computer and use it in GitHub Desktop.
Save DustinCampbell/32cd69d04ea1c08a16ae5c4cd21dd3a3 to your computer and use it in GitHub Desktop.
Using MSBuildWorkspace

Introduction

Roslyn provides a rich set of APIs for analyzing C# and Visual Basic source code, but constructing a context in which to perform analysis can be challenging. For simple tasks, creating a Compilation populated with SyntaxTrees, MetadataReferences and a handful of options may suffice. However, if there are multiple projects involved in the analysis, it is more complicated because multiple Compilations need to be created with references between them.

To simplify the construction process. Roslyn provides the Workspace API, which can be used to model solutions, projects and documents. The Workspace API performs all of the heavy lifting needed to parse SyntaxTrees from source code, load MetadataReferences, and construct Compilations and add references between them.

This works very well when the source files, references and compiler options are known up front, but what if you want to analyze a MSBuild project or the projects in a Visual Studio solution file? For this purpose, Roslyn provides the MSBuildWorkspace API.

MSBuildWorkspace

MSBuildWorkspace is a special Workspace implementation that can load MSBuild projects and solutions. Once created, loading projects into an MSBuildWorkspace instance is easy: it's just a matter of calling OpenProjectAsync(...) or OpenSolutionAsync(...).

using Microsoft.CodeAnalysis.MSBuild;

...

using (var workspace = MSBuildWorkspace.Create())
{
    var project = await workspace.OpenProjectAsync("MyProject.csproj");
    var compilation = await project.GetCompilationAsync();

    // Perform analysis...
}

When a project is opened, MSBuildWorkspace loads it by using MSBuild to perform a design-time build of the project. This is a special build that retrieves all the necessary information to analyze the project (such as source files, references, compilation options, etc.), but stops short of actually compiling the project and emiting a binary on disk.

In addition, by default MSBuildWorkspace will attempt to load any project references that it finds when loading a project. This can be controlled with the MSBuildWorkspace.LoadMetadataForReferencedProjects property. If set to true, MSBuildWorkspace will avoid loading a project reference if that project's output binary already exists on disk. If it does, MSBuildWorkspace will reference the binary instead of the project.

As mentioned earlier, Visual Studio solutions can be loaded into an MSBuildWorkspace instance by calling the OpenSolutionAsync(...) API. This loads all of the projects in the solution.

using Microsoft.CodeAnalysis.MSBuild;

...

using (var workspace = MSBuildWorkspace.Create())
{
    var solution = await workspace.OpenSolutionAsync("MySolution.sln");

    foreach (var project in solution.Projects)
    {
        var compilation = await project.GetCompilationAsync();

        // Perform analysis...
    }
}

In older versions of Roslyn (2.8 and earlier), MSBuildWorkspace can be found in the Microsoft.CodeAnalysis.Workspaces.Common NuGet package. In Roslyn 2.9 and later, it can be found in the Microsoft.CodeAnalysis.Workspaces.MSBuild NuGet package.

Setting up MSBuild

In order to successfully load an MSBuild project, MSBuildWorkspace must have access to a properly configured MSBuild. There are a few options available for setting up MSBuild.

Using MSBuild from Visual Studio 2017 (or newer)

The recommended way to set up MSBuild is to install Visual Studio 2017 (or newer) or the Build Tools for Visual Studio 2017 (or newer) on the machine that MSBuildWorkspace will run on. The important detail is to ensure that the installation has the necessary workloads and components to support the projects you'll be loading. For example, if C# is not installed, you will not be able to load C# projects. Similarly, if a project targets .NET Framework 4.7.3, that targeting pack must be installed. A good rule of thumb is, if you can build the project with MSBuild at the command-line, MSBuildWorkspace should be able to load it.

Once a version of MSBuild is installed, the application using MSBuildWorkspace needs to be able to find it. To do this, reference the Microsoft.Build.Locator NuGet package from the application. This package provides faciltiies for locating and registering a valid installation of MSBuild on the current machine. In the simplest case, you can simply instruct the MSBuildLocator to RegisterDefaults, that is, register the first instance that it finds.

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

...

MSBuildLocator.RegisterDefaults();

using (var workspace = MSBuildWorkspace.Create())
{
    // Use MSBuildWorkspace...
}

When you register an instance with MSBuildLocator (or register the defaults), an assembly resolution handler is added to the current AppDomain via AppDomain.AssemblyResolve. This handler resolves various Microsoft.Build.* assemblies needed by MSBuildWorkspace from the location of the registered MSBuild. This results in MSBuildWorkspace using versions of the Microsoft.Build.* assemblies that are compatible with the MSBuild tasks that run when it loads projects.

The Microsoft.Build.Locator source code and issue tracking are available at https://github.com/Microsoft/MSBuildLocator.

Gotchas

  • Do not include any Microsoft.Build.* assemblies in your application output except for Microsoft.Build.Locator. If your application includes Microsoft.Build.dll, Microsoft.Build.Framework.dll, Microsoft.Build.Tasks.Core or Microsoft.Build.Utiltiies.Core, they can interfere with the assembly resolution handler that MSBuildLocator has installed. If this happens, you'll often see strange failures with MSBuildWorkspace, such as "Unable to cast object of type 'Microsoft.CodeAnalysis.BuildTasks.Csc' to type 'Microsoft.Build.Framework.ITask'".
  • Be sure use the MSBuildLocator to register an instance of MSBuild before creating an instance of MSBuildWorkspace. Otherwise, MSBuildWorkspace will fail with a MEF composition error.
  • In older versions of MSBuild, there is a bug that causes the current application directory to be considered an instance of MSBuild if your executable's name contains the text "MSBuild" within it. This bug is fixed, but it can still be a problem if the machine still has an older version of MSBuild installed See dotnet/msbuild#2194 for more details.

Troubleshooting

If there any warnings or errors are encountered when MSBuildWorkspace loads a project or solution, you can access them with MSBuildWorkspace.Diagnostics.

@daveaglick
Copy link

Just read through this and have a couple questions:

  • I've been playing around with MSBuildLocator and kept running into the problem of the caller loading the MSBuild assemblies before I could hook the assembly resolver whenever I exposed a leaky abstraction over MSBuild. I'm assuming the new MSBuildWorkspace API won't expose any MSBuild objects to avoid this problem?
  • Given that MSBuildLocator only targets .NET Framework, will that continue to be the only target for MSBuildWorkspace as well? Or are there longer-term plans to expand MSBuildWorkspace to .NET Standard with support for the .NET Core SDK?

@DustinCampbell
Copy link
Author

DustinCampbell commented Apr 10, 2018

@daveaglick:

I've been playing around with MSBuildLocator and kept running into the problem of the caller loading the MSBuild assemblies before I could hook the assembly resolver whenever I exposed a leaky abstraction over MSBuild. I'm assuming the new MSBuildWorkspace API won't expose any MSBuild objects to avoid this problem?

The MSBuildWorkspace API doesn't actually expose any MSBuild objects. However, the old NuGet package pulled Microsoft.Build and friends into your project. The new Microsoft.CodeAnalysis.Workspaces.MSBuild will *not do this. It defines those Microsoft.Build dependencies as ExcludeAssets="Runtime" and PrivateAssets="All" (code)

You can use the old MSBuildWorkspace with the Microsoft.Build.Locator package by deleting the Microsoft.Build.* assemblies from your application's output directory (except for Microsoft.Build.Locator.dll of course). I showed another customer how to do this here: FizzerWL/ErrorRepro#1.

Given that MSBuildLocator only targets .NET Framework, will that continue to be the only target for MSBuildWorkspace as well? Or are there longer-term plans to expand MSBuildWorkspace to .NET Standard with support for the .NET Core SDK?

I'm still considering whether MSBuildWorkspace will move to netstandard2.0 or not. My chief concern is around putting consumers into a pit of success. After all, if MSBuildWorkspace is used in a netcoreapp2.0 is it expected behavior that it can only load a subset of projects? What is their recourse when it doesn't work? By targeting .NET Framework, it means that more MSBuild tasks will just work.

@Pilchie
Copy link

Pilchie commented Apr 11, 2018

Looks great!

A few small suggestions to consider:

  1. Linking to https://github.com/dotnet/project-system/blob/master/docs/design-time-builds.md for more info about design time builds.
  2. Linking to your WorkspaceTester once you have a permanent home for it.
  3. Under TroubleShooting also suggest subscribing to WorkspaceFailed events.

@DustinCampbell
Copy link
Author

Good suggestions @Pilchie -- thanks!

@pkrukp
Copy link

pkrukp commented May 16, 2018

Hi,

As I understand the build locator works only if you have VS 2017 (or Build Tools 2017) installed on machine.

Is there a way to configure MSBuildWorkspace to work if VS 2017 is not installed on machine?

From what I see in MSBuildLocator.cs, it simply detects location of VS2017 and when AssemblyResolve is used, it provides msbuild assemblies from this location.

  1. would it work if I provided msbuild assemblies from location of older VS or msbuild?
  2. would it work if I shipped with latest msbuild assemblies and used them?

Or does MSBuildWorkspace work only with VS2017 ?

@aggieben
Copy link

Thanks so much for writing this. I was pretty lost in terms of how to get starting opening a solution until I found this.

One thing I did run into however: it appears that it's impossible to use this method in fsi.exe:

#r "./packages/Microsoft.Build.Locator/lib/net46/Microsoft.Build.Locator.dll"

open Microsoft.Build.Locator
MSBuildLocator.RegisterDefaults |> ignore

This script fails with the following output:

--> Referenced 'd:\Users\ben\proj\aggieben\CodeQuery\./packages/Microsoft.Build.Locator/lib/net46/Microsoft.Build.Locator.dll' (file may be locked by F# Interactive process)

System.InvalidOperationException: Microsoft.Build.Locator.MSBuildLocator.RegisterInstance was called, but MSBuild assemblies were already loaded.
Ensure that RegisterInstance is called before any method that directly references types in the Microsoft.Build namespace has been called.
Loaded MSBuild assemblies: Microsoft.Build.Utilities.Core, Version=15.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
Microsoft.Build.Tasks.Core, Version=15.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
Microsoft.Build.Framework, Version=15.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
   at Microsoft.Build.Locator.MSBuildLocator.RegisterMSBuildPath(String msbuildPath)
   at Microsoft.Build.Locator.MSBuildLocator.RegisterDefaults()
   at <StartupCode$FSI_0015>.$FSI_0015.main@()
Stopped due to error
>

This smells to me like fsi.exe is eager-loading the referenced assemblies, which is different than the usual runtime behavior of a .NET application. Is there an alternative way to create an MSBuildWorkspace?

@JoshuaKGoldberg
Copy link

Hey thanks for writing this! This is exactly what I'm looking for... except I can't get it to work with .NET Core and the following dependency versions:

    <PackageReference Include="Microsoft.CodeAnalysis" Version="2.8.2" />
    <PackageReference Include="Microsoft.CodeAnalysis.Csharp" Version="2.8.2" />
    <PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="2.8.2" />

@DustinCampbell any chance you could help out?

@thauk-copperleaf
Copy link

When opening the solution with

var solution = workspace.OpenSolutionAsync(solutionPath).Result;

I get an exception

System.AggregateException: 'One or more errors occurred.'

With inner exception

Object reference not set to an instance of an object

With stacktrace

   at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions)
   at System.Threading.Tasks.Task`1.GetResultCore(Boolean waitCompletionNotification)
   at System.Threading.Tasks.Task`1.get_Result()

@DustinCampbell
Copy link
Author

DustinCampbell commented Sep 14, 2018

@pkruk2, @aggieben, @JoshuaKGoldberg, @thauk-copperleaf: This gist isn't a great medium for asking general questions. This is just a draft of a document that's in progress and I haven't looked at it for a few months. If you haven't already (though you probably have), please file issues in the appropriate repro.

@pkruk2: MSBuildWorkspace only supports MSBuild 15. There are public NuGet packages that you can use to cobble together your own copy of MSBuild and some small toolset that will support some projects. This is not a simple process however. You might consider filing an issue at https://github.com/microsoft/msbuild to request help with deploying MSBuild with your application.

@aggieben: I have no idea how the internals of FSI work. I recommend filing an issue over at https://github.com/microsoft/visualfsharp.

@JoshuaKGoldberg: Unfortunately, MSBuildWorkspace is not currently supported in a .NET Core application. Both Microsoft.CodeAnalysis.Workspaces.MSBuild and Microsoft.Build.Locator require net46. There's been some work to try and address this, but it's a difficult problem (microsoft/MSBuildLocator#33).

@thauk-copperleaf: If you could file an issue over at https://github.com/dotnet/roslyn along with a solution and project that reproduces the issue, I'll take a look.

@rfabro
Copy link

rfabro commented Sep 2, 2019

Any news on when MSBuildWorkspace will be supported for .NET core?

@kerryjiang
Copy link

Some question about the .NET Core support of MSBuildWorkspace.

@mjallaq88
Copy link

Some question about the .NET Core support of MSBuildWorkspace.

@smetlov
Copy link

smetlov commented Dec 2, 2020

Same question about the .NET Core support of MSBuildWorkspace.

@pflajszer
Copy link

Same question about the .NET Core support of MSBuildWorkspace.

@panayot-cankov
Copy link

Same question about the .NET Core support of MSBuildWorkspace.

@hovik-aghajanyan
Copy link

Same question about the .NET Core support of MSBuildWorkspace.

@MatthewSteeples
Copy link

microsoft/MSBuildLocator#51 - this seems to indicate that support is already in

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment