MSBuild Traversal project best practices
[!NOTE] This post assumes you're already familiar with the Microsoft.Build.Traversal SDK, a.k.a
dirs.proj
files.
Whether you're new to traversal projects or an experienced user, you may encounter some common pitfalls. This post covers frequently asked questions and best practices to help you avoid them.
Problem: Multiple SDK versions
The easiest way to use a traversal project is like this, where the version of
the project SDK is listed as part of the Sdk
property:
<Project Sdk="Microsoft.Build.Traversal/4.1.82">
If you have a large project, over time it's likely that someone will introduce a new version of the Traversal SDK without updating the others. MSBuild however only allows a single SDK version during a build. Trying to mix versions results in this warning:
MSB4240: Multiple versions of the same SDK 'Microsoft.Build.Traversal' cannot be
specified. The previously resolved SDK version 'value' from location 'value'
will be used and the version 'value' will be ignored.
To complicate matters, this warning may appear or disappear depending on which projects are included in the build.
To sidestep this entire issue and force a consistent SDK version across your
codebase, instead specify the SDK version in your global.json
file like this:
{
"msbuild-sdks": {
"Microsoft.Build.Traversal": "4.1.82"
}
}
Problem: Visual Studio / .sln files
Traversal projects offer a few advantages over .sln
files (including the new
.slnx
format).
First, traversal projects can reference other traversal projects. In large codebases, this is incredibly useful because it means each component can structure their code as they like while also making it easy to build multiple components by maintaining a single meta-project.
Second, with traversal files there's no duplication between projects and solutions. With .sln and .slnx files, adding or removing a project reference requires also updating the sln file. Traversal projects, by their nature, use project references as the source of truth and thus cannot get out-of-sync.
If you adopt traversal projects, you should stop tracking sln files in your
version control system by adding an entry to your .gitignore
like this:
+# Ignore sln files in favor of Traversal projects
+*.sln
and delete them from source control with a script like this:
git ls-files "*.sln" "**/*.sln" | xargs git rm
If your workflow relies on .sln files, such as for Visual Studio users, consider using a tool like SlnGen to generate .sln files on-the-fly.
Problem: MSB1008: Only one project can be specified
If you follow the approach above (using traversal projects to organize your
build while generating .sln files on the fly for editing) you may end up with
many untracked and stale .sln files littered around your codebase. This can lead
to errors when you run commands like dotnet build
such as this:
MSB1008: Only one project can be specified
because the dotnet command finds both a .proj
and .sln
file in the same
directory.
To avoid the need to constantly disambiguate between projects, you can instruct
MSBuild to ignore certain extensions using the -ignoreProjectExtensions
command-line switch
(docs).
Better yet, create and check in a Directory.Build.rsp
file that includes this
switch:
# Since we use traversal projects (dirs.proj), ignore any stale .sln files.
# These are generated, .gitignored, and may cause confusion.
-ignoreProjectExtensions:.sln,.slnx
to ensure that all command line builds inherit this option by default (docs).
Note that previously, *.rsp
files were excluded by the dotnet .gitignore
template, so be sure that you aren't ignoring this file. This has been
fixed for .NET 10.
Problem: Every Visual Studio window is now named "dirs"
Visual Studio uses the file name as the window name. When using traversal projects, this means every VS instance ends up named "dirs", which isn't helpful.
To set a meaningful solution name, define SlnGenProjectName
in your dirs.proj
file:
<PropertyGroup>
<SlnGenProjectName>MyCoolProject</SlnGenProjectName>
</PropertyGroup>
You can also customize other options, such as mirroring the folder structure in the solution file. See Configuring SlnGen for the full list of options.
If you have multiple dirs.proj files, managing solution names manually can be tedious. Instead, set default properties globally in a Directory.Build.props file:
<PropertyGroup>
<SlnGenFolders>true</SlnGenFolders>
<SlnGenProjectName>$([System.IO.Path]::GetFileName($([System.IO.Path]::GetDirectoryName($(MSBuildProjectFullPath)))))
</SlnGenProjectName>
</PropertyGroup>
This ensures:
- Folder structure is reflected in .sln files by default
- The solution name defaults to the parent folder name, avoiding the "dirs" issue automatically.
This approach keeps things consistent without needing to set SlnGenProjectName manually in every project file.
Wrapping up
MSBuild traversal projects can improve maintainability, reduce duplication, and
streamline large codebases. However, they come with unique challenges,
especially in larger teams accustomed to .sln
files. While moving away from
traditional solution files requires some adjustment, the long-term benefits in
automation and flexibility make it worthwhile. Have additional tips or insights?
Reach out to me and I'll update this post to include the best practices from the
community!