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:

HTML / XML
<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:

Bash
dotnet --list-sdks

Example output:

Code
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:

Bash
dotnet --list-runtimes

To inspect the complete .NET environment, execute:

Bash
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:

Bash
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
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:

Bash
nano ~/.zshrc

Add the following configuration:

Bash
export DOTNET_ROOT="$HOME/.dotnet"
export PATH="$DOTNET_ROOT:$PATH"

Save the file and reload the configuration:

Bash
source ~/.zshrc

Verify the installation:

Bash
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:

Bash
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:

Bash
nano ~/.bashrc

Add:

Bash
export DOTNET_ROOT="$HOME/.dotnet"
export PATH="$DOTNET_ROOT:$PATH"

Reload the configuration:

Bash
source ~/.bashrc

Now check the installed SDKs:

Bash
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:

Code
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:

JSON
{
  "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:

Code
Projects/
│
├── ApplicationA/
│   ├── global.json
│   └── ApplicationA.csproj
│
└── ApplicationB/
    ├── global.json
    └── ApplicationB.csproj

Inside ApplicationA/global.json:

JSON
{
  "sdk": {
    "version": "8.0.100",
    "rollForward": "latestFeature"
  }
}

Inside ApplicationB/global.json:

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:

Bash
cd Projects/ApplicationA

dotnet --version

Then navigate to the second project:

Bash
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:

Bash
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:

Bash
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:

Bash
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:

Bash
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:

Bash
which dotnet

Then inspect the environment variables:

Bash
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:

Bash
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.