An MSBuild file does little by itself: a .csproj of ten lines builds a
program because it imports thousands of lines of properties, items, targets and
tasks. This page says which files those are and how MSBuild finds them. Evaluation reads
the files in this order: the SDK’s props, the project’s own body, then the SDK’s targets.
Naming an SDK
All four forms below have the same effect: an implicit
<Import Project="Sdk.props" Sdk="…"/> at the very top of the project and an
implicit <Import Project="Sdk.targets" Sdk="…"/> at the very bottom.
<Project Sdk="Microsoft.NET.Sdk"> <!-- the attribute -->
<Project Sdk="Microsoft.NET.Sdk;My.Extra.Sdk/2.1.0"> <!-- several, with a version -->
<Project>
<Sdk Name="Microsoft.NET.Sdk" Version="10.0.0" MinimumVersion="9.0.0" /> <!-- the element -->
</Project>
<Project>
<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" /> <!-- written out, to put things in between -->
<!-- … the project … -->
<Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk" />
</Project>
In the attribute, SDKs are separated by semicolons and a version follows a slash. An
empty name, or a slash with no version after it, is an error (MSB4238). The
written-out form is how a project runs its own properties before the SDK’s
targets but after its props, or imports an SDK in the middle of a file.
How an SDK is found
MSBuild asks its SDK resolvers in turn; the first that knows the name answers with a folder. The ones that come with the .NET SDK are:
- the .NET SDK resolver, for the SDKs bundled with the .NET SDK (
Microsoft.NET.Sdkand its relatives, which sit in theSdksfolder of the installed SDK) and for the .NET SDK versionglobal.jsonasks for; - the NuGet SDK resolver, for any other name: it restores a NuGet package of that name
and uses its
Sdkfolder. The version comes from the attribute (My.Sdk/2.1.0) or from themsbuild-sdkssection ofglobal.json.
An SDK is a folder with Sdk/Sdk.props and Sdk/Sdk.targets. A name
that no resolver finds is an error at load time; the explorer cannot know, so it lists a name
it does not know as found by the SDK resolvers and shows the two file names the
folder would have.
{
"sdk": { "version": "10.0.100", "rollForward": "latestFeature", "allowPrerelease": false },
"msbuild-sdks": { "My.Extra.Sdk": "2.1.0", "Microsoft.Build.Traversal": "4.1.0" }
}
global.json: sdk chooses the .NET SDK for dotnet commands in this folder and below, msbuild-sdks gives the versions of NuGet SDKs so projects can write the name alone.The SDKs of the .NET SDK
| Sdk | For | Imports besides its own files |
|---|---|---|
Microsoft.NET.Sdk | Class libraries, console programs, tests and most other C#, Visual Basic and F# projects | Microsoft.Common.props, the language targets and the common targets, NuGet |
Microsoft.NET.Sdk.Web | ASP.NET Core applications | Microsoft.NET.Sdk, Microsoft.NET.Sdk.Razor |
Microsoft.NET.Sdk.Razor | Razor class libraries, Razor views and components | Microsoft.NET.Sdk, Microsoft.NET.Sdk.StaticWebAssets |
Microsoft.NET.Sdk.StaticWebAssets | The files under wwwroot and what libraries bring | |
Microsoft.NET.Sdk.BlazorWebAssembly | Blazor WebAssembly applications | its own props and targets |
Microsoft.NET.Sdk.Worker | Worker services | Microsoft.NET.Sdk |
Microsoft.NET.Sdk.WindowsDesktop | Windows Forms and WPF (a base SDK import that UseWPF and UseWindowsForms turn on) | Microsoft.NET.Sdk |
Microsoft.NET.Sdk.Publish | Web publish profiles | its own props and targets |
Other SDKs come as NuGet packages: Microsoft.Build.NoTargets (a project that
builds nothing), Microsoft.Build.Traversal (a project that builds the projects it
references), Microsoft.Build.CentralPackageVersions, MSTest.Sdk,
Aspire.AppHost.Sdk, Uno.Sdk and others.
What Microsoft.NET.Sdk imports
The complete chain for a C# project in .NET SDK 10.0.404, in the order MSBuild imports
it (this is what dotnet msbuild -preprocess lists). Names are relative to the
SDK’s folder. An F# or Visual Basic project differs in the language-specific files; the
workload manifests an installation adds are not listed.
| Phase | Files |
|---|---|
| Sdk.props, before the project | Sdks/Microsoft.NET.Sdk/Sdk/Sdk.props, then Current/Microsoft.Common.props (which imports Directory.Build.props, below), UseArtifactsOutputPath.props, NuGet.props, then Sdks/Microsoft.NET.Sdk/targets/Microsoft.NET.Sdk.props: DefaultItems.props (the default globs), Microsoft.NETCoreSdk.BundledVersions.props, SupportedTargetFrameworks.props, SupportedPlatforms.props, SourceLink.props (and the SourceLink packages for GitHub, GitLab, Azure Repos, Bitbucket), Sdk.CSharp.props, PackTool.props, the Windows Desktop props, Microsoft.NET.Windows.props. |
| The project | Your properties, items, imports and targets. |
| Sdk.targets, after the project | Sdks/Microsoft.NET.Sdk/Sdk/Sdk.targets, then BeforeCommon.targets (default assembly information, publish profile, target framework inference, default output paths, runtime identifier inference), the language targets (Microsoft.CSharp.targets and the Roslyn Core targets, which lead to Microsoft.Common.targets and Microsoft.Common.CurrentVersion.targets; Microsoft.Common.targets imports Directory.Build.targets when one exists), NuGet.targets,Microsoft.NET.Sdk.targets (package dependency resolution, default items, framework reference resolution, SourceLink, generated assembly information and global usings, publish, conflict resolution, Sdk.CSharp.targets, analysers), the API compatibility targets, NuGet.Build.Tasks.Pack.targets and the container build targets. |
The explorer lists every one of these files for a project that names
Microsoft.NET.Sdk, with the place it is imported at, in the Overview. It
does not open them: they are a different file each, and a project’s own view stays its
own.
Files MSBuild looks for by name
| File | When it is read |
|---|---|
Directory.Build.props | Imported by Microsoft.Common.props, so before the project body: the first one found in the project’s folder or the nearest folder above it. Only one is imported; to chain to a higher one, import it with $([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../')). Switched off by ImportDirectoryBuildProps set to false; its path can be given in DirectoryBuildPropsPath. |
Directory.Build.targets | The same for Microsoft.Common.targets, so after the project body (ImportDirectoryBuildTargets, DirectoryBuildTargetsPath). |
Directory.Packages.props | NuGet central package management: the nearest one above the project, read through NuGet.props; it holds PackageVersion items and ManagePackageVersionsCentrally. |
Directory.Build.rsp, MSBuild.rsp | Response files of command-line switches: Directory.Build.rsp from the folder of the project or solution (and above), MSBuild.rsp beside the MSBuild program. |
Directory.Solution.props, Directory.Solution.targets | Imported by the project MSBuild makes from a solution file. |
obj/project.nuget.g.props, obj/project.nuget.g.targets | Written by restore: the build assets of the packages. Imported through MSBuildProjectExtensionsPath. |
global.json | Read by the dotnet command and the SDK resolvers, not imported: the .NET SDK version and the msbuild-sdks versions. |
nuget.config | Read by restore: the package sources and settings. |
Props and targets files
A .props file sets properties and default items and is imported before
the project body, so the project can override it. A .targets file defines
targets and anything that must see the project’s final values and is imported
after. Both are ordinary MSBuild files with a Project root; the
names are convention only, and the explorer treats both alike. In an imported file
$(MSBuildThisFileDirectory) is its own folder, and a Condition
on the Import such as Exists('…') makes it optional.
A property defined in a props file is overridden by the project’s later definition of
the same name; the usual idiom for a default that the project may change is
<Name Condition="'$(Name)' == ''">value</Name>.
Choose
Choose holds one or more When elements, each with a required
Condition, and at most one Otherwise at the end. MSBuild takes
the first When whose condition is true, else the Otherwise; a
When after the Otherwise is an error (MSB4084), a
When without a condition is an error (MSB4035). A branch holds
PropertyGroup, ItemGroup and further Choose
elements. Unlike a Condition on a group, a Choose decides once,
during evaluation; legacy Visual Studio projects use it to give each configuration its own
settings.
Legacy projects
Before SDKs, a project listed everything. It has the MSBuild 2003 namespace, a
ToolsVersion, no Sdk, an Import of
Microsoft.Common.props at the top and of Microsoft.CSharp.targets
at the bottom, an ItemGroup that names each Compile file and each
Reference, and a PropertyGroup for every configuration and platform.
The explorer tells the two styles apart in the Overview (SDK style or legacy
style with the 2003 namespace) and lists the explicit imports in the Imports tab, since
for a legacy project they are in the file.
In the explorer
The Overview of a project lists the SDK references (name, version, how it was written
and whether the explorer knows it), the implicit imports of each SDK in the order above,
and the files the common targets import by themselves. The Imports tab lists the
Import elements written in the file with their conditions.
Not described
Known, not yet covered
- Reference: the import chains of the other SDKs (web, Razor, Blazor, Worker) down to the last file, and of an F# or Visual Basic project; the
sdk-resolversof third parties; the contents of the SDK files themselves.
Sources
Microsoft Learn, use
MSBuild project SDKs, customize
your build and global.json.
The import chain, the Directory.Build lookup and the language targets were read
from the files of .NET SDK 10.0.404 itself (Sdks/Microsoft.NET.Sdk/Sdk/Sdk.targets,
Current/Microsoft.Common.props, Microsoft.Common.targets) and listed
by dotnet msbuild -preprocess.