Why Not Document What Works?

I recently started trying to use the MSBuild API in a project where I need to be able to parse csproj files and then do syntactical analyses on them. It’s cool that you can access the build API behind Visual Studio from within a C# project!

What’s not cool, though, is some critical documentation about setting things up provided by Microsoft.

It turns out you need to initialize the MSBuild environment before you can use it. That makes sense since there are different frameworks (e.g., Net and Net Core) that it applies to. There’s even a provided helper class you can use to do the initialization, MSBuildLocator. All you need to do is initialize it with an instance of VisualStudioInstance, which is in turn dependent on which version of Visual Studio your program will have available to use (or which you want it to use, if there are multiple version available).

But MSBuildLocator is weird. From the documentation:

You can’t reference any MSBuild types (from the Microsoft.Build namespace) in the method that calls MSBuildLocator.

The documentation then goes on to state:

For example, you can’t use code like the following:

void ThisWillFail()
{
    // Register the most recent version of MSBuild
    RegisterInstance(MSBuildLocator.QueryVisualStudioInstances().OrderByDescending(
       instance => instance.Version).First());
    Project p = new Project(SomePath); // Could be any MSBuild type
    // Code that uses the MSBuild type
}

Instead, write code like this:

void MethodThatDoesNotDirectlyCallMSBuild()
{
    var instance = ... // select which of the installed instances to use
    
    // Register a specific instance of MSBuild
    MSBuildLocator.RegisterInstance(instance);
    MethodThatCallsMSBuild();
}

void MethodThatCallsMSBuild()
{
    Project p = new Project(SomePath);
    // Code that uses the MSBuild type
}

Notice what’s missing? That’s right, an example of code that works and won’t fail!

It is really dumb to describe things with negatives. Particularly when, as in this case, your normal style of programming cannot work because of (apparently) the way the MSBuildLocator API is written internally.

I wasted almost an hour trying to figure out how to get things to work. I was doing that in an XUnit test environment, so adding to my difficulties was the fact that, while the project would build without reporting an error, it would immediately throw an exception that XUnit couldn’t capture in debug mode. So I had no idea what line was causing the problem.

Eventually I figured out this approach, which worked:

using Microsoft.Build.Locator;

public class TestBase
{
    protected TestBase()
    {
        MSBuildLocator.CanRegister.Should().BeTrue();
        MSBuildLocator.AllowQueryAllRuntimeVersions = true;

        VsInstance = GetLatestVsInstance();

        MSBuildLocator.RegisterInstance( VsInstance );
    }

    private static VisualStudioInstance GetLatestVsInstance()
    {
        var retVal = MSBuildLocator.QueryVisualStudioInstances().MaxBy(x => x.Version);
        retVal.Should().NotBeNull();

        return retVal!;
    }
}
using FluentAssertions;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Locator;

namespace SyntaxScanner;

public class BasicSyntax : TestBase
{
    [ Theory ]
    [InlineData("C:\\Programming\\ProgrammingUtilities\\FileUtilities\\FileUtilities.csproj")]
    public void ParseProjectFile( string projFile )
    {
        File.Exists( projFile ).Should().BeTrue();

        var projects = new ProjectCollection();
        var project = projects.LoadProject( projFile );
    }
}

By separating the method where the MSBuildLocator initialization takes place from where I’m trying to do something which, behind the scenes, depends on it being initialized (that’s the stuff in the ParseProjectFile method), I got things to work.

But intuitive this was not.

Leave a Comment

Your email address will not be published. Required fields are marked *

Categories
Archives