Customizing the Build
|
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. |
MSBuild offers more than ten places to put build logic, from a file next to one project to a file that affects every build on a machine. They differ in scope (one project, a folder tree, a solution, a user, a machine), in timing (before or after the project body) and in who sees them (command line only, or Visual Studio too). Choosing the narrowest one that works keeps builds predictable. This page shows each on Ledgerline with MSBuild 18.9 (.NET SDK 10.0.402); the repository-root files as a whole are described on Repository-Wide Configuration.
The extension points
| File or property | Scope | Imported | IDE |
|---|---|---|---|
|
projects in the folder and below (nearest file only) |
early, from |
yes |
|
same |
late, from |
yes |
|
one project, one developer (not committed) |
by the common targets, in inner and outer builds |
yes |
|
command-line solution builds |
into the solution metaproject |
no |
|
command-line builds under the folder / every |
as switches, before evaluation |
no |
|
whatever sets the property (a build server, a |
before/after |
if set |
|
every build by that MSBuild installation / user |
at those points |
yes |
Directory.Build.props and .targets in depth
The search starts in the project’s folder and walks up to the root, stopping at the first file found (case-sensitive on Linux and macOS). Three consequences:
-
A nested file replaces the outer one for its subtree unless it imports it with
$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))(multi-level merging, shown on Repository-Wide Configuration). -
An empty file stops the search — the way to keep a subtree (vendored code, a tool with its own conventions) out of the repository defaults:
<Project> <!-- Empty: stops the upward search, so this subtree ignores the repository defaults. --> </Project>cd tools/legacy dotnet msbuild -getProperty:Nullable -getProperty:TreatWarningsAsErrors -getProperty:DirectoryBuildPropsPath{ "Properties": { "Nullable": "", "TreatWarningsAsErrors": "false", "DirectoryBuildPropsPath": "~/ledgerline/tools/legacy/Directory.Build.props" } } -
The search also leaves the repository: a
Directory.Build.propsin a parent folder of a CI agent’s work directory takes part in every build below it. A root-level file (even an empty one) protects against that, which the MSBuild security guidance recommends.
ImportDirectoryBuildProps/ImportDirectoryBuildTargets=false turn the import off; DirectoryBuildPropsPath and
DirectoryBuildTargetsPath show which file was used, or point to another one. For troubleshooting, -pp shows where
the file was imported and -getProperty:DirectoryBuildPropsPath which one.
Source: MSBuild: Directory.Build.props and Directory.Build.targets, Search scope, Import order, Use case: multi-level merging, Troubleshooting.
Why TargetFramework is unreliable in Directory.Build.props
Directory.Build.props is read before the project body. Whether it sees TargetFramework depends on where the value
comes from. To show it, Ledgerline's Directory.Build.props was given
<TfmSeenInProps>[$(TargetFramework)]</TfmSeenInProps>, its Directory.Build.targets the same line as
TfmSeenInTargets plus a target that prints both before CoreCompile (and the .user file of the next section).
Building the CLI tool, which targets net10.0 and references the three-TFM library:
dotnet build src/Ledgerline.Cli -tl:off -v:m | grep "props saw"
cd src/Ledgerline
dotnet msbuild -getProperty:TfmSeenInProps -getProperty:TfmSeenInTargets
props saw [net10.0], targets saw [net10.0], user file says hello from Ledgerline.csproj.user
props saw [], targets saw [net10.0], user file says
{
"Properties": {
"TfmSeenInProps": "[]",
"TfmSeenInTargets": "[]"
}
}
-
The first line is the referenced library’s inner build: the outer build passes
TargetFrameworkas a global property, which exists before any file is read, soDirectory.Build.propssees it. -
The second line is the single-target CLI project:
TargetFrameworkis set in its project body, afterDirectory.Build.props— so the props file sees nothing, the targets file seesnet10.0. -
The JSON is the library’s outer build, where
TargetFrameworkis empty everywhere.
A condition on $(TargetFramework) in Directory.Build.props therefore works in some projects and silently fails in
others. Put it in Directory.Build.targets or in the project file.
The .user file
Ledgerline.csproj.user, next to the project, is imported automatically (in inner and outer builds) and is meant for
one developer’s machine — the default .gitignore excludes it. The demonstration above set a property in it:
<Project>
<PropertyGroup>
<LedgerlineLocalNote>hello from Ledgerline.csproj.user</LedgerlineLocalNote>
</PropertyGroup>
</Project>
Its value appears only in the library’s lines; the CLI project has no .user file, hence the empty user file says.
Visual Studio stores per-user settings (start-up arguments, the selected debug profile) in the same file, so keep
manual additions small.
Source: MSBuild: Use the user file.
Custom before/after files
For logic that must apply to every project on a build machine without touching repositories, the common targets
import the files named by CustomBeforeMicrosoftCommonProps, CustomBeforeMicrosoftCommonTargets,
CustomAfterMicrosoftCommonProps and CustomAfterMicrosoftCommonTargets (plus language-specific
CustomBefore/AfterMicrosoftCSharpTargets). The property can be set on the command line:
<Project>
<Target Name="MachineWideHook" AfterTargets="Build" Condition="'$(TargetFramework)' != ''">
<Message Importance="high" Text="CustomAfterMicrosoftCommonTargets: built $(MSBuildProjectName) for $(TargetFramework)" />
</Target>
</Project>
dotnet build src/Ledgerline -tl:off -v:m -p:CustomAfterMicrosoftCommonTargets=$PWD/../machine/Custom.After.Microsoft.Common.targets
props saw [net8.0], targets saw [net8.0], user file says hello from Ledgerline.csproj.user
props saw [netstandard2.0], targets saw [netstandard2.0], user file says hello from Ledgerline.csproj.user
CustomAfterMicrosoftCommonTargets: built Ledgerline for net8.0
CustomAfterMicrosoftCommonTargets: built Ledgerline for netstandard2.0
props saw [net10.0], targets saw [net10.0], user file says hello from Ledgerline.csproj.user
CustomAfterMicrosoftCommonTargets: built Ledgerline for net10.0
Without the property, files with the default names in the MSBuild folder (Custom.After.Microsoft.Common.targets
and so on) are used, and per-user files can be dropped into $(MSBuildUserExtensionsPath) (%LOCALAPPDATA%\Microsoft\MSBuild
on Windows) under …/ImportBefore/ or …/ImportAfter/. The documentation warns that a broken machine-wide file can
stop Visual Studio from building anything; ImportUserLocationsByWildcardBefore<File> properties switch the user
locations off. AlternateCommonProps replaces Microsoft.Common.props entirely.
Response files
MSBuild.rsp next to MSBuild.exe (none ships in the .NET SDK) and Directory.Build.rsp in the project folder or a
parent (MSBuild 15.6+) add switches to command-line builds; %MSBuildThisFileDirectory% (with percent signs on every
OS) refers to the .rsp file’s folder. Visual Studio ignores both. -noAutoResponse disables them; explicit @file
response files still apply. Example and output are on
Repository-Wide Configuration.
Solution-level files
Directory.Solution.props/.targets and the before/after.<solution>.sln.targets files extend the in-memory
solution metaproject of command-line builds only; they do not reach the projects and are ignored by Visual Studio. See
Solutions and .slnx.
Source: MSBuild: Customize solution builds.
Props or targets: the rule
-
Directory.Build.props— defaults that projects and the SDK read: language version,Nullable,TreatWarningsAsErrors, package metadata,ArtifactsPath. A project can override them. -
Directory.Build.targets— targets, item changes, and properties that depend on project values (TargetFramework,IsPackable,OutputType, anything set by a package). They win over the project.
Source: MSBuild: Props or targets.
Common mistakes
-
A nested
Directory.Build.propsthat forgets the parent — the repository defaults disappear for that subtree. -
TFM-dependent logic in
Directory.Build.props— see above. -
Build-changing switches in
Directory.Build.rsp— the IDE builds differently. -
Machine-wide
Custom.After…files on developer machines — invisible, surprising, hard to debug; use them only on build servers, and prefer the property form. -
Committing
.userfiles — they are per developer.