Q:

Building msbuild 15 project programmatically

I'm trying to build a simple C# 7 class library project created with VS2017. 
MSBuild from framework assemblies is outdated, so I'm referencing Microsoft.Build, Microsoft.Build.Engine and Microsoft.Build.Framework from MSBuild folder within visual studio (C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\MSBuild\15.0\Bin).
Still, when I do this:
using (var collection = new ProjectCollection())
{
    var proj = collection.LoadProject(@"c:\projects\Sample\Sample.csproj"); // <-- exception
    proj.Build(new[] {new ConsoleLogger()});
}

I'm getting InvalidProjectFileException: The tools version "15.0" is unrecognized. Available tools versions are "4.0", "2.0".
Is there programmatic way to invoke build using the latest build tools and C# 7 compiler?

A:

I had similar needs for my team and I wrote a Builder library for C# that supports several versions of Visual Studio.
I could not make the Project.Build function work properly, so I went for executing MsBuild.exe directly.
How I built it:
Use Microsoft.Build.Framework from NuGet
Create a new Project object with 1 target called Build
Define the right ToolsVersion
Of the Project according to the Visual Studio version: 

2010, 2012 => 4.0
2013 => 12.0
2015 => 14.0
2017 => 15.0

Add a new task of type MsBuild
With Projects property containing all the project I need to build
Define the ToolsVersion
of the MsBuild task with same value as for the Project
Serialize the Project into a temporary file
Find the right MsBuild.exe according to the ToolsVersion
4.0, 12.0, 14.0
Found in the registry:
Registry.LocalMachine.OpenSubKey($@"SOFTWARE\Microsoft\MSBuild\ToolsVersions\{msBuildVersion}")

15.0
Not anymore in the registry, you need to use the Nuget package Microsoft.VisualStudio.Setup.Configuration.Interop
        var query = new SetupConfiguration();

        var query2 = (ISetupConfiguration2)query;

        var e = query2.EnumAllInstances();

        var helper = (ISetupHelper)query;

        int fetched;

        var instances = new ISetupInstance[1];

        do
        {
            e.Next(1, instances, out fetched);
            if (fetched > 0)
            {
                var instance = instances[0];

                var instance2 = (ISetupInstance2)instance;

                var state = instance2.GetState();

                // Skip non-complete instance, I guess?
                // Skip non-local instance, I guess?
                // Skip unregistered products?
                if (state != InstanceState.Complete
                    || (state & InstanceState.Local) != InstanceState.Local
                    || (state & InstanceState.Registered) != InstanceState.Registered)
                {
                    continue;
                }

                var msBuildComponent =
                    instance2.GetPackages()
                        .FirstOrDefault(
                            p =>
                                p.GetId()
                                    .Equals("Microsoft.Component.MSBuild",
                                        StringComparison.InvariantCultureIgnoreCase));

                if (msBuildComponent == null)
                {
                    continue;
                }

                var instanceRootDirectory = instance2.GetInstallationPath();

                var msbuildPathInInstance = Path.Combine(instanceRootDirectory, "MSBuild", msBuildVersion, "Bin", "msbuild.exe");

                if (File.Exists(msbuildPathInInstance))
                {
                    return msbuildPathInInstance;
                }
            }
        } while (fetched > 0);

Execute MsBuild.exe
And build the serialized Project using a custom XML Logger - you can use the one provided by MsBuildExtensionPack
Read the result summary
Deserialize the result summary from Xml and use it to determine if build failed or not, which errors and warnings occurred etc.

A:

I had exactly the same issues in my project.
In my case I want to build an SSDT project programmatically but I tried other project types as well.
Interestingly, it worked perfectly fine in build 26228.04 of VS2017 (which was the Release build) and stopped working in build 26228.09.
Yesterday build 26228.10 was released, so I decided to give it another go.
Surprisingly the following worked for me:

Update to the latest VS2017 build 26228.10 using the Visual Studio Installer.
I was never able to get collection.LoadProject(...) to work without getting some weird error but you can use this code to do the build:
BuildResult result = null;

using (var pc = new ProjectCollection())
    result = BuildManager.DefaultBuildManager.Build(
        new BuildParameters(pc) { Loggers = new[] { new ConsoleLogger() } },
        // Change this path to your .sln file instead of the .csproj.
        // (It won't work with the .csproj.)
        new BuildRequestData(@"c:\projects\Sample.sln",
            // Change the parameters as you need them,
            // e.g. if you want to just Build the Debug (not Rebuild the Release).
            new Dictionary<string, string>
            {
                { "Configuration", "Release" },
                { "Platform", "Any CPU" }
            }, null, new[] { "Rebuild" }, null));

if (result.OverallResult == BuildResultCode.Failure)
    // Something bad happened...

Ensure that you have copied all the MSBuild assembly binding redirections to your .config file.
If you haven't already done this, open the file C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\Bin\MSBuild.exe.config and copy the whole <runtime> element to the <configuration> element of your own .config file in the project that uses MSBuild programmatically.
If you don't redirect the MSBuild assembly versions, you will get some pretty strange error messages from MSBuild which will vary based on your MSBuild version.
But it's sure to say: It will definitely not work without the assembly binding redirections.
One additional step if you want to build an SSDT project:
Change the following two lines in your .sqlproj file from
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">11.0</VisualStudioVersion>
<VisualStudioVersion Condition="'$(SSDTExists)' == ''">11.0</VisualStudioVersion>

to
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">15.0</VisualStudioVersion>
<VisualStudioVersion Condition="'$(SSDTExists)' == ''">15.0</VisualStudioVersion>

I'm not 100% sure why, but these steps worked perfectly for me!
But because MSBuild is a bit "difficult" to use programmatically (in my opinion it's sometimes as stable as a weather forecast), chances are that my approach won't work in your case.

A:

If you just need the path to the latest MSBuild.exe, use Microsoft.Build.Utilities.Core from Nuget and use the following code:
ToolLocationHelper.GetPathToBuildToolsFile("msbuild.exe", ToolLocationHelper.CurrentToolsVersion);

This works whether just Build Tools are installed or the full Visual Studio installation.

