The MSBuild API and MSBuild Locator
|
This section documents MSBuild 18.9, the .NET SDK 10.0.402 (with its This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The C# Reference bibliography lists the reference material consulted while preparing these pages. |
Everything dotnet build does is available as a .NET API. Tools that inspect solutions, generate code from project
metadata, run custom build orchestration or analyse a repository’s dependency graph load projects through the
Microsoft.Build assemblies instead of parsing XML themselves, and get exactly the values MSBuild would compute — imports, conditions and SDK defaults included. This page builds a small Ledgerline tool with MSBuild 18.9 (.NET
SDK 10.0.402), Microsoft.Build 18.9.6 and Microsoft.Build.Locator 1.11.2.
Packages and namespaces
| Package | Contents |
|---|---|
|
|
|
Shared interfaces: |
|
Base classes for tasks and loggers ( |
|
The built-in tasks |
|
A complete MSBuild to ship with an application — rarely the right choice, because evaluating real projects also needs the SDKs and compilers |
|
Finds an installed MSBuild (a .NET SDK, Visual Studio or Build Tools) and loads it |
An application should not ship its own copy of MSBuild. It compiles against the Microsoft.Build packages and, at
run time, loads the MSBuild of an installed .NET SDK or Visual Studio, so that projects resolve the same SDKs, imports
and tasks as a command-line build. Microsoft.Build.Locator does the loading.
Source: MSBuild API.
A project-inspection tool
The tool references both packages. ExcludeAssets="runtime" keeps Microsoft.Build.dll out of the output folder;
PrivateAssets="all" keeps the reference from flowing to anything that references the tool. The Locator package itself
is referenced normally.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Build.Locator" Version="1.11.2" />
<!-- Compile against MSBuild, but load the SDK's own copy at run time. -->
<PackageReference Include="Microsoft.Build" Version="18.9.6" ExcludeAssets="runtime" PrivateAssets="all" />
</ItemGroup>
</Project>
The compile-time Microsoft.Build version must not be newer than the MSBuild the tool will load: 18.9.6 is the
newest 18.9 package, matching MSBuild 18.9.15 in SDK 10.0.402.
using Microsoft.Build.Locator;
// Must run before any Microsoft.Build type is loaded.
VisualStudioInstance instance = MSBuildLocator.RegisterDefaults();
Console.WriteLine($"MSBuild {instance.Version} from {instance.MSBuildPath}");
if (args.Length > 1 && args[0] == "build")
{
return Builder.Run(args[1]);
}
Inspector.Run(args.Length > 0 ? args[0] : Directory.GetCurrentDirectory());
return 0;
static class Inspector
{
// Kept in a separate method so the JIT loads Microsoft.Build only after registration.
public static void Run(string root)
{
using var collection = new Microsoft.Build.Evaluation.ProjectCollection();
foreach (string path in Directory.EnumerateFiles(root, "*.csproj", SearchOption.AllDirectories).Order())
{
var project = collection.LoadProject(path);
string tfms = project.GetPropertyValue("TargetFrameworks");
if (tfms.Length == 0) tfms = project.GetPropertyValue("TargetFramework");
Console.WriteLine($"{Path.GetRelativePath(root, path),-55} {tfms}");
}
}
}
dotnet run --project LedgerInspect -- ~/ledgerline
MSBuild 10.0.402 from ~/.dotnet/sdk/10.0.402
samples/Ledgerline.Report/Ledgerline.Report.csproj net10.0
src/Ledgerline.Build/Ledgerline.Build.csproj netstandard2.0
src/Ledgerline.Cli/Ledgerline.Cli.csproj net10.0
src/Ledgerline.Templates/Ledgerline.Templates.csproj net10.0
src/Ledgerline.Templates/templates/ledgerline-app/LedgerlineApp.csproj net10.0
src/Ledgerline/Ledgerline.csproj net10.0;net8.0;netstandard2.0
tests/Ledgerline.Tests/Ledgerline.Tests.csproj net10.0
Points worth noticing:
-
instance.Versionis the SDK version (10.0.402) for a .NET SDK instance, not the MSBuild version (18.9.15). -
Evaluation is real evaluation.
TargetFrameworksforLedgerline.csprojcame throughDirectory.Build.propsand the SDK imports exactly as in a build, and the template project (whichLedgerline.slnxdoes not list) was evaluated too, because the tool walks the file system rather than the solution. -
ProjectCollectioncaches. A project loaded twice with the same global properties is returned from the cache; pass global properties (new ProjectCollection(new Dictionary<string,string> { ["Configuration"] = "Release" })) to evaluate a different configuration. -
No targets ran. Evaluation computes properties and items only (How a .NET Build Works).
Source: Use Microsoft.Build.Locator.
Building with BuildManager
BuildManager runs targets with loggers and global properties, like the command line. The restore must run as its own
request, as -restore does: a restore generates the nuget.g.props/nuget.g.targets imports that the build’s
evaluation needs, and a fresh MSBuildRestoreSessionId global property makes it a separate evaluation.
static class Builder
{
public static int Run(string projectPath)
{
var parameters = new Microsoft.Build.Execution.BuildParameters
{
Loggers = [new Microsoft.Build.Logging.ConsoleLogger(Microsoft.Build.Framework.LoggerVerbosity.Minimal)],
};
var manager = Microsoft.Build.Execution.BuildManager.DefaultBuildManager;
var globals = new Dictionary<string, string?> { ["Configuration"] = "Release" };
// Restore in its own request (what -restore does), so Build evaluates the restored imports.
var restoreGlobals = new Dictionary<string, string?>(globals) { ["MSBuildRestoreSessionId"] = Guid.NewGuid().ToString() };
var restore = manager.Build(parameters, new Microsoft.Build.Execution.BuildRequestData(
projectPath, restoreGlobals, null, ["Restore"], null,
Microsoft.Build.Execution.BuildRequestDataFlags.ClearCachesAfterBuild));
if (restore.OverallResult != Microsoft.Build.Execution.BuildResultCode.Success) return 1;
var result = manager.Build(parameters, new Microsoft.Build.Execution.BuildRequestData(
projectPath, globals, null, ["Build"], null));
Console.WriteLine($"OverallResult: {result.OverallResult}");
return result.OverallResult == Microsoft.Build.Execution.BuildResultCode.Success ? 0 : 1;
}
}
dotnet run --project LedgerInspect -- build ~/ledgerline/src/Ledgerline.Cli/Ledgerline.Cli.csproj
MSBuild 10.0.402 from ~/.dotnet/sdk/10.0.402
Determining projects to restore...
All projects are up-to-date for restore.
Ledgerline -> ~/ledgerline/src/Ledgerline/bin/Release/net10.0/Ledgerline.dll
Ledgerline.Cli -> ~/ledgerline/src/Ledgerline.Cli/bin/Release/net10.0/Ledgerline.Cli.dll
OverallResult: Success
BuildResult.ResultsByTarget holds each target’s outputs (for example the item returned by GetTargetPath), which is
how tools get data out of a build without parsing logs.
Source: MSBuild API.
Two ways to get it wrong
Shipping MSBuild in the output. Without ExcludeAssets="runtime", Microsoft.Build.Locator 1.11.2 stops the build:
~/.nuget/packages/microsoft.build.locator/1.11.2/buildTransitive/Microsoft.Build.Locator.targets(19,5): error MSBL001: A PackageReference to the package 'Microsoft.Build' at version '18.9.6' is present in this project without ExcludeAssets="runtime" and PrivateAssets="all" set. This can cause errors at run-time due to MSBuild assembly-loading.
Touching an MSBuild type before registering. The JIT loads the assemblies a method needs when it compiles that
method. Adding using var early = new Microsoft.Build.Evaluation.ProjectCollection(); to the top-level statements,
before RegisterDefaults(), makes Main itself need Microsoft.Build, which is not in the output folder:
Unhandled exception. System.IO.FileNotFoundException: Could not load file or assembly 'Microsoft.Build, Version=15.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'. The system cannot find the file specified.
File name: 'Microsoft.Build, Version=15.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'
at Program.<Main>$(String[] args)
The assembly version is 15.1.0.0 because every MSBuild from 15 to 18 keeps that assembly version. Keep all MSBuild usage in methods (or classes) that the registering method only calls.
Choosing an instance
RegisterDefaults() picks one instance: on .NET, the SDK that dotnet --version would choose for the current folder
(so global.json applies); on .NET Framework, the newest Visual Studio. To choose explicitly, enumerate and register:
var instance = MSBuildLocator.QueryVisualStudioInstances()
.OrderByDescending(i => i.Version)
.First();
MSBuildLocator.RegisterInstance(instance);
The .NET build of the Locator finds only .NET SDK instances, and the .NET Framework build only Visual Studio and Build
Tools instances, so a tool that must load msbuild.exe (for example, to evaluate classic .NET Framework projects)
targets .NET Framework.
Source: Use Microsoft.Build.Locator.
Common mistakes
-
A
Microsoft.Buildpackage newer than the installed MSBuild —MissingMethodExceptionat run time; compile against the oldest MSBuild you support. -
Microsoft.Build.dllin the output folder — MSBL001, or, if forced, a build that cannot find SDK resolvers. -
MSBuild types in the method that calls
RegisterDefaults()—FileNotFoundExceptionfor 15.1.0.0. -
Restore and Build in one request — the build evaluates without the restored imports.
-
Expecting
instance.Versionto be the MSBuild version — it is the SDK version for .NET SDK instances.