Introduction
Working on multiple .NET projects often means dealing with different SDK versions. One application may still use .NET 8, while another project requires .NET 10 or a newer SDK. This becomes a problem when the development machine has only one SDK installed or when the default SDK changes after an update.
For example, imagine you maintain an existing ASP.NET Core application that uses .NET 8 and start a new project targeting .NET 10. Both projects need to build and run on the same machine. Installing a newer SDK should not force you to upgrade the older application.
.NET supports installing multiple SDK versions side by side. You can use the dotnet command to list installed SDKs, select a version for a particular project, and control how the SDK is resolved.
This article explains how to manage multiple .NET SDK versions on macOS and Linux, configure global.json, avoid common version-selection problems, and keep development environments consistent across a team.
Understanding .NET SDK and Runtime Versions
Before managing multiple versions, it is important to understand the difference between the .NET SDK and the .NET Runtime.
The .NET SDK contains the tools required to create, build, test, and publish .NET applications. The runtime is responsible for executing applications.
Installing an SDK also installs the associated runtime, but installing only a runtime does not provide the tools needed to build an application.
Consider the following project configuration:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
The TargetFramework property tells the project to target .NET 8. It does not force the machine to use the .NET 8 SDK.
For example, a project targeting .NET 8 can generally be built using a newer compatible SDK. However, the SDK version can affect build tools, analyzers, restore behavior, and generated output.
This is why controlling the SDK version separately from the target framework is important.
Checking Installed .NET SDK Versions
Before installing another SDK, check which versions are already available.
Open Terminal on macOS or Linux and execute:
dotnet --list-sdks
Example output:
8.0.408 [/usr/local/share/dotnet/sdk]
9.0.203 [/usr/local/share/dotnet/sdk]
10.0.100 [/usr/local/share/dotnet/sdk]
The output shows the installed SDK versions and their installation locations.
To check installed runtimes, use:
dotnet --list-runtimes
To inspect the complete .NET environment, execute:
dotnet --info
This command provides information about the SDK, runtime, operating system, architecture, and installation paths.
These commands are useful when troubleshooting a machine that has multiple SDK installations or when a project unexpectedly selects the wrong version.
Installing Multiple .NET SDK Versions on macOS
macOS supports side-by-side .NET SDK installations. You do not need to uninstall an existing SDK before installing another version.
One way to install an additional SDK is through the official dotnet-install.sh installation script.
For example, to install the .NET 8 SDK into a user-level directory:
mkdir -p "$HOME/.dotnet"
curl -sSL https://dot.net/v1/dotnet-install.sh -o dotnet-install.sh
bash dotnet-install.sh \
--channel 8.0 \
--install-dir "$HOME/.dotnet"
Now install another SDK into the same directory:
bash dotnet-install.sh \
--channel 10.0 \
--install-dir "$HOME/.dotnet"
Both SDK versions can exist in the same installation directory.
The important point is that the installer places SDK versions side by side rather than replacing the previously installed SDK.
Configuring the PATH on macOS
If you install .NET into $HOME/.dotnet, your terminal must know where to find the dotnet executable.
For the default Zsh shell, open the configuration file:
nano ~/.zshrc
Add the following configuration:
export DOTNET_ROOT="$HOME/.dotnet"
export PATH="$DOTNET_ROOT:$PATH"
Save the file and reload the configuration:
source ~/.zshrc
Verify the installation:
dotnet --list-sdks
If you use Bash instead of Zsh, add the same configuration to ~/.bashrc or the appropriate shell startup file.
When using a package manager or an existing system-wide installation, check its installation path before changing DOTNET_ROOT. The environment variable should point to the intended .NET installation.
Installing Multiple .NET SDK Versions on Linux
The installation process on Linux is similar to macOS. You can use the installation script to install multiple SDK versions into a common directory.
For example:
mkdir -p "$HOME/.dotnet"
curl -sSL https://dot.net/v1/dotnet-install.sh -o dotnet-install.sh
bash dotnet-install.sh \
--channel 8.0 \
--install-dir "$HOME/.dotnet"
bash dotnet-install.sh \
--channel 10.0 \
--install-dir "$HOME/.dotnet"
Configure the environment variables in your shell configuration file.
For Bash:
nano ~/.bashrc
Add:
export DOTNET_ROOT="$HOME/.dotnet"
export PATH="$DOTNET_ROOT:$PATH"
Reload the configuration:
source ~/.bashrc
Now check the installed SDKs:
dotnet --list-sdks
The installation script is useful when you want a user-level installation without depending on a Linux distribution's package manager.
For system-wide installations, you can also use Microsoft's package repository for supported Linux distributions. Avoid mixing package-manager installations and manual installations without checking their locations, because this can make SDK resolution confusing.
Selecting an SDK Version Using global.json
Installing multiple SDKs is only part of the solution. You also need a way to tell the .NET CLI which SDK a particular project should use.
This is where global.json becomes useful.
The .NET CLI searches for this file starting from its current working directory and moving up through parent directories.
Consider a workspace containing two applications:
Projects/
│
├── LegacyApplication/
│ ├── LegacyApplication.csproj
│ └── Program.cs
│
├── NewApplication/
│ ├── NewApplication.csproj
│ └── Program.cs
│
└── global.json
If both applications are expected to use the same SDK version, a global.json file in the workspace root can control SDK selection.
For example:
{
"sdk": {
"version": "8.0.100",
"rollForward": "latestFeature",
"allowPrerelease": false
}
}
This configuration asks the .NET CLI to use an installed .NET 8 SDK at or above the specified version, selecting the latest eligible feature band and patch within that major and minor version.
The version property specifies the minimum SDK version for the selection policy. It does not install the SDK.
The rollForward property determines which installed versions are acceptable when the requested version is unavailable or when the policy allows a newer version.
The allowPrerelease property controls whether prerelease SDK versions can be considered.
Understanding SDK Roll-Forward Options
Choosing the correct roll-forward policy matters because it determines how flexible SDK selection will be.
Policy | Behavior |
|---|
disable | Requires an exact SDK version |
patch | Allows a suitable patch-level fallback |
latestPatch | Selects the latest eligible patch in the same feature band |
feature | Allows movement to a higher feature band within the same major and minor version |
latestFeature | Selects the latest eligible feature band within the same major and minor version |
minor | Allows movement to a higher minor version within the same major version |
latestMinor | Selects the latest eligible minor version within the same major version |
major | Allows movement to a higher major version when necessary |
latestMajor | Allows selection of the latest eligible major version |
For most development teams, latestFeature is a practical choice when they want to remain within the same major and minor SDK version while receiving newer feature-band and patch updates.
For a strictly controlled build environment, disable can be used to require an exact SDK version.
Managing Different SDK Versions for Different Projects
A common requirement is to use different SDK versions for different applications.
The .NET CLI supports this through project-level global.json files.
For example, consider this structure:
Projects/
│
├── ApplicationA/
│ ├── global.json
│ └── ApplicationA.csproj
│
└── ApplicationB/
├── global.json
└── ApplicationB.csproj
Inside ApplicationA/global.json:
{
"sdk": {
"version": "8.0.100",
"rollForward": "latestFeature"
}
}
Inside ApplicationB/global.json:
{
"sdk": {
"version": "10.0.100",
"rollForward": "latestFeature"
}
}
Now each application can select its own compatible SDK.
Open Terminal and navigate to the first project:
cd Projects/ApplicationA
dotnet --version
Then navigate to the second project:
cd ../ApplicationB
dotnet --version
The selected version can change automatically because the CLI discovers the nearest applicable global.json file.
This is particularly useful for developers maintaining older applications while building new services with a different .NET version.
One detail is worth remembering: global.json controls SDK selection based on the current working directory and its parent directories. It does not automatically select an SDK based on the .csproj file you pass to a command.
Using the Same SDK Version Across a Development Team
Installing the same SDK version on every developer's machine is not always practical. However, a project should produce consistent build results across development machines and CI environments.
Commit the global.json file to the repository so every developer uses the same SDK selection rules.
You can also check the selected version during development:
dotnet --version
dotnet --info
For CI pipelines, install an SDK that satisfies the project's global.json configuration before running restore, build, and test commands.
For example, a build script can execute:
dotnet restore
dotnet build --configuration Release
dotnet test --configuration Release
If the required SDK is missing and the roll-forward policy does not allow another installed version, the command will fail.
This is preferable to silently building with an unintended SDK version.
Advantages and Disadvantages of Managing Multiple SDKs
Advantages | Disadvantages |
|---|
Supports multiple application versions on one machine | Requires additional disk space |
Makes maintaining legacy applications easier | SDK selection can become confusing without global.json |
Allows project-specific SDK selection | Developers must maintain compatible installations |
Helps keep local and CI builds consistent | Environment variables may need configuration |
Avoids unnecessary project upgrades | Older SDKs still require maintenance and security updates |
Common Problems and Troubleshooting
The Wrong SDK Version Is Selected
If dotnet --version displays an unexpected version, check whether a global.json file exists in the current directory or one of its parent directories.
Run:
find .. -name global.json
Also verify that you are running the command from the intended project directory.
The Required SDK Is Not Found
If the CLI reports that the required SDK cannot be found, list the installed versions:
dotnet --list-sdks
Compare the installed versions with the version specified in global.json.
Install the required SDK or adjust the roll-forward policy if the project allows a newer compatible version.
The Dotnet Command Is Not Recognized
Check the executable location:
which dotnet
Then inspect the environment variables:
echo "$DOTNET_ROOT"
echo "$PATH"
Make sure the installation directory is included in PATH and that DOTNET_ROOT points to the intended installation.
The Application Cannot Find the Required Runtime
An SDK installation normally includes its corresponding runtime, but an application may target a runtime that is not installed.
Check available runtimes:
dotnet --list-runtimes
Install the required runtime if necessary. Remember that installing an SDK and installing only a runtime are different operations.
Best Practices
Keep SDK management simple and predictable. Use global.json for projects that need a specific SDK selection instead of relying on whichever version happens to be installed on a developer's machine.
Use a roll-forward policy that matches the project's requirements. Avoid latestMajor when the application must remain on a particular major version.
Keep installation paths consistent across development environments where possible. This reduces problems caused by multiple dotnet executables or conflicting environment variables.
For team projects, commit global.json and configure CI to install a compatible SDK. Review the file whenever the project intentionally moves to another SDK version.
Finally, do not confuse SDK upgrades with application framework upgrades. Changing the SDK used to build a project does not automatically change its TargetFramework or migrate its application code.
Summary
Managing multiple .NET SDK versions on macOS and Linux is straightforward once you understand side-by-side installation and SDK selection.
You can install multiple versions using the .NET installation script or a supported package manager. The dotnet --list-sdks command helps identify installed versions, while global.json controls which SDK the CLI selects for a particular working directory.
For developers maintaining several applications, project-level SDK configuration is a practical way to keep older projects stable while working with newer .NET applications.
The most important part is to keep SDK selection explicit, configure the development environment correctly, and use the same SDK requirements in local development and CI. This makes working across multiple .NET projects easier without forcing unnecessary framework upgrades.
Join the conversation! Your thoughts help the community grow.