0

I am trying to run MSBuild programmatically from a C# DLL (which will ultimately be loaded from PowerShell), and as a first step from a command-line application. I have used Microsoft.Build.Locator as recommended (or so I reckon) by installing its NuGet package to my project, and adding the following references to my test project:

    <Reference Include="Microsoft.Build, Version=15.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
      <Private>False</Private>
    </Reference>
    <Reference Include="Microsoft.Build.Framework, Version=15.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
      <Private>False</Private>
    </Reference>
    <Reference Include="Microsoft.Build.Locator, Version=1.0.0.0, Culture=neutral, PublicKeyToken=9dff12846e04bfbd, processorArchitecture=MSIL">
      <HintPath>..\packages\Microsoft.Build.Locator.1.2.6\lib\net46\Microsoft.Build.Locator.dll</HintPath>
    </Reference>

The project targets .NET Framework 4.8, and the source code is as follows:

using Microsoft.Build.Evaluation;
using Microsoft.Build.Execution;
using Microsoft.Build.Locator;
using System.Collections.Generic;

namespace nrm_testing
{
    class Program
    {
        static void Main(string[] args)
        {
            MSBuildLocator.RegisterDefaults();
            DoStuff();
        }
        
        static void DoStuff()
        {
            using (var projectCollection = new ProjectCollection())
            {
                var buildParameters = new BuildParameters
                {
                    MaxNodeCount = 1 // https://stackoverflow.com/q/62658963/3233393
                };
                
                var buildRequestData = new BuildRequestData(
                    @"path\to\a\project.vcxproj",
                    new Dictionary<string, string>(),
                    null,
                    new string[0],
                    null
                );

                var result = BuildManager.DefaultBuildManager.Build(buildParameters, buildRequestData);
            }
        }
    }
}

Upon entering the using block, I receive the following exception:

System.IO.FileNotFoundException: 'Could not load file or assembly 'System.Runtime.CompilerServices.Unsafe, Version=4.0.4.1, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' or one of its dependencies. The system cannot find the file specified.'

The Modules window shows that MSBL did successfully locate my VS2019 installation:

Microsoft.Build.dll           16.07.0.37604 C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\Microsoft.Build.dll
Microsoft.Build.Framework.dll 16.07.0.37604 C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\Microsoft.Build.Framework.dll
Microsoft.Build.Locator.dll   1.02.6.49918  C:\dev\nrm3-tests\nrm\nrm-testing\.out\AnyCPU-Debug\Microsoft.Build.Locator.dll

System.Runtime.CompilerServices.Unsafe.dll is indeed present besides the located MSBuild assemblies, in version 4.0.6.0 (according to DotPeek).

What could be causing this error, and how could I fix it?


My attempts so far:

  • I have found this question, but the linked GitHub issue is still open and I'm unsure whether it's the same problem.

  • I have managed to get an binding redirects working, but I don't think I can use them from within a DLL, so that's a dead end.

  • Adding the System.Runtime.CompilerServices.Unsafe NuGet package to the project (and verifying that it is indeed copied alongside the project's executable) does nothing (thanks magicandre1981 for the suggestion).

  • Switching from packages.config to PackageReference (as suggested by Perry Qian), with no change in behaviour.

Quentin
  • 62,093
  • 7
  • 131
  • 191
  • add System.Runtime.CompilerServices.Unsafe package directly to project – magicandre1981 Sep 30 '20 at 13:33
  • @magicandre1981 I just tried -- alas, no change whatsoever. – Quentin Sep 30 '20 at 14:38
  • in [app.config add assemblyBinding](https://learn.microsoft.com/en-us/dotnet/framework/configure-apps/redirect-assembly-versions#manually-editing-the-app-config-file) entries for System.Runtime.CompilerServices.Unsafe – magicandre1981 Sep 30 '20 at 16:14
  • @magicandre1981 that works in my test application, however manually binding the dependencies of a dynamically-located assembly feels like a huge hack. The real problem, though, is that libraries cannot declare binding redirects... I will look into a workaround I found elsewhere based on overriding one of the assembly loader's callbacks. – Quentin Oct 01 '20 at 13:59
  • this is no hack, this is normal, VS does this on its own when adding packages – magicandre1981 Oct 01 '20 at 14:06
  • you can also add [AutoGenerateBindingRedirects and GenerateBindingRedirectsOutputType](https://stackoverflow.com/a/46120907/1466046) to csproj – magicandre1981 Oct 01 '20 at 14:17
  • @magicandre1981 usually yes, but the whole point of MSBuildLocator is to just use the existing MSBuild installation. Distributing a copy of the dependencies from every MSBuild version because they can't find their own pieces sounds pretty fishy to me. – Quentin Oct 01 '20 at 15:08
  • as you already noticed, there is an open bug on this. Wait until it is fixed. – magicandre1981 Oct 02 '20 at 17:14
  • @Quentin, any update about this issue? – Mr Qian Oct 05 '20 at 03:35

2 Answers2

1

After a lot of fiddling with different ideas, I ended up writing this workaround based on manual assembly resolution.

RegisterMSBuildAssemblyPath detects when Microsoft.Build.dll gets loaded, and memorizes its directory. Upon subsequent assembly load failures, RedirectMSBuildAssemblies checks if the missing assembly exists inside that path, and loads it if it does.

class Program
{
    private static string MSBuildAssemblyDir;

    static void Main(string[] args)
    {
        MSBuildLocator.RegisterDefaults();

        Thread.GetDomain().AssemblyLoad += RegisterMSBuildAssemblyPath;
        Thread.GetDomain().AssemblyResolve += RedirectMSBuildAssemblies;

        DoStuff();
    }

    private static void RegisterMSBuildAssemblyPath(object sender, AssemblyLoadEventArgs args)
    {
        var assemblyPath = args.LoadedAssembly.Location;

        if (Path.GetFileName(assemblyPath) == "Microsoft.Build.dll")
            MSBuildAssemblyDir = Path.GetDirectoryName(assemblyPath);
    }

    private static Assembly RedirectMSBuildAssemblies(object sender, ResolveEventArgs args)
    {
        if (MSBuildAssemblyDir == null)
            return null;

        try
        {
            var assemblyFilename = $"{args.Name.Split(',')[0]}.dll";
            var potentialAssemblyPath = Path.Combine(MSBuildAssemblyDir, assemblyFilename);

            return Assembly.LoadFrom(potentialAssemblyPath);
        }
        catch (Exception)
        {
            return null;
        }
    }

    static void DoStuff()
    {
        // Same as before
    }
}

I'm pretty sure there are (many) corner cases that will make this fail, but it will do for now.

Quentin
  • 62,093
  • 7
  • 131
  • 191
0

Actually, this is an real issue for a long time for packages.config nuget management format. And Microsoft's recommended solution for this problem is to add a bindingRedirect.

Usually, you can use this node in xxx.csproj file to automatically generate bindingredirect.

However, for some specific dlls, this node may not work due to serveral reasons. And it is still an issue on the current packages.config nuget management format as your said.

Suggestion

As a suggestion, you could use the new PackageReference nuget manage format instead since VS2017. This format is simple, convenient and efficient.

Also, when you use this format, first, you should make a backup of your project.

Just right-click on the packages.config file-->click Migrate packages.config to PackageReference.

Besides, I have also reported this issue on DC Forum and I hope the team will provide a better suggestion.

Mr Qian
  • 21,064
  • 1
  • 31
  • 41
  • No luck with `PackageReference` either... I have managed to make the binding redirect work on my test application, but that doesn't help with my end goal which has to be a library. I have since found a somewhat hacky workaround based on manually loading unresolved assemblies from MSBuild's directory, which I'll document in an answer as soon as I get the time. – Quentin Oct 05 '20 at 10:54