Installing and Selecting SDKs

This section documents MSBuild 18.9, the .NET SDK 10.0.402 (with its dotnet CLI) and NuGet 7.9, as published in the MSBuild, .NET and NuGet documentation on learn.microsoft.com, which is the reference these pages are written and verified against. Material about .NET 11 / NuGet 8.0 is still in preview and is always flagged as such — never presented as baseline.

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.

Every dotnet command that builds, tests or packs runs inside one specific .NET SDK, and a machine usually has several. Which one runs decides the C# compiler, the MSBuild version, the NuGet client and the defaults of every property on this site, so a repository should state it rather than inherit whatever is newest on each machine. This page installs SDKs side by side and pins one with global.json, using .NET SDK 10.0.402 (the baseline) and SDK 9.0.319 installed next to it.

Installing SDKs

Method Use it for

Operating-system packages (Windows installer, winget, Homebrew casks, Linux distribution or Microsoft packages)

Developer machines. One SDK per feature band is typical; updates come through the OS.

The dotnet-install.sh / dotnet-install.ps1 scripts

CI and non-admin installs: any version (--version 9.0.319) or channel (--channel 10.0, --channel LTS) into any folder (--install-dir), without changing the system.

Container images (mcr.microsoft.com/dotnet/sdk:10.0)

Builds in containers; the tag pins the SDK.

dotnetup (public preview)

A user-level toolchain manager that installs SDKs and runtimes, follows channels, and can install the SDK requested by the nearest global.json.

SDKs install side by side under one dotnet root (sdk/<version>/), sharing the runtimes in shared/. This page added SDK 9.0.319 to an existing 10.0.402 installation with the install script:

./dotnet-install.sh --version 9.0.319 --install-dir ~/.dotnet --no-path
dotnet --list-sdks
dotnet-install: Installation finished successfully.
9.0.319 [~/.dotnet/sdk]
10.0.402 [~/.dotnet/sdk]

The script does not verify that the operating system’s native dependencies are present (it says so in its output), and it does not configure PATH with --no-path. In CI, prefer the setup action of the CI system (for example actions/setup-dotnet), which runs the same script and reads global.json (CI Builds and Caching).

Run the install scripts only from https://dot.net/v1/dotnet-install.sh (or .ps1), and in automated pipelines download a pinned copy and check its signature or hash before running it. A modified install script installs a modified compiler, which then builds every binary you ship.

How the SDK is selected

dotnet is a small native muxer. For an SDK command it searches for global.json from the current working directory upwards; without one, it runs the highest installed SDK, including previews. With one, it applies the file’s version and roll-forward policy.

flowchart TD start([dotnet build]) --> find{global.json found
from the current folder upwards?} find -- no --> latest[Use the highest installed SDK] find -- yes --> valid{File valid?} valid -- no --> ignored[Ignore it; use the highest installed SDK] valid -- yes --> ver{sdk.version set?} ver -- no --> latest ver -- yes --> paths[Search the folders in sdk.paths,
or the dotnet root by default] paths --> roll{A version matching
version + rollForward?} roll -- yes --> run([Run that SDK]) roll -- no --> fail([Fail: errorMessage, or
A compatible .NET SDK was not found])

A loop over the global.json variants, run in an empty folder on the machine above:

dotnet --version
for g in '{"sdk":{"version":"9.0.319"}}' \
         '{"sdk":{"version":"9.0.300","rollForward":"latestPatch"}}' \
         '{"sdk":{"version":"9.0.100","rollForward":"latestFeature"}}' \
         '{"sdk":{"version":"8.0.100","rollForward":"latestMajor"}}' \
         '{"sdk":{"version":"10.0.402","rollForward":"disable"}}'; do
  echo "$g" > global.json; echo "$g -> $(dotnet --version)"
done
10.0.402
{"sdk":{"version":"9.0.319"}} -> 9.0.319
{"sdk":{"version":"9.0.300","rollForward":"latestPatch"}} -> 9.0.319
{"sdk":{"version":"9.0.100","rollForward":"latestFeature"}} -> 9.0.319
{"sdk":{"version":"8.0.100","rollForward":"latestMajor"}} -> 10.0.402
{"sdk":{"version":"10.0.402","rollForward":"disable"}} -> 10.0.402

When nothing matches — 9.0.100 with the default policy (no 9.0.1xx SDK is installed), 8.0.100 without latestMajor, or 10.0.400 with disable — every command fails before MSBuild starts:

The command could not be loaded, possibly because:
  * You intended to execute a .NET application:
      The application '--version' does not exist or is not a managed .dll or .exe.
  * You intended to execute a .NET SDK command:
      A compatible .NET SDK was not found.

Requested SDK version: 9.0.100
global.json file: ~/sel/global.json

Installed SDKs:
9.0.319 [~/.dotnet/sdk]
10.0.402 [~/.dotnet/sdk]

Install the [9.0.100] .NET SDK or update [~/sel/global.json] to match an installed SDK.

Learn about SDK resolution:
https://aka.ms/dotnet/sdk-not-found

Every global.json key

Key Since Meaning

sdk.version

3.0

The full SDK version (10.0.402). No wildcards, ranges or partial versions (10, 10.0, 10.0.x).

sdk.rollForward

3.0

How far a later SDK may be used; values below. Default patch when version is set.

sdk.allowPrerelease

3.0

Whether previews are considered. Default true outside Visual Studio; inside Visual Studio, the IDE’s preview setting.

sdk.paths

10.0

Folders to search, in order, relative to global.json; $host$ is the root of the running dotnet. Default: only $host$.

sdk.errorMessage

10.0

Replaces the "A compatible .NET SDK was not found" text.

sdk.workloadVersion

8.0.400

Pins the workload set that dotnet workload install/update/restore use (Workloads).

msbuild-sdks

 — 

Versions for MSBuild project SDKs such as Microsoft.Build.Traversal (Imports and Project SDKs).

test.runner

10.0

Microsoft.Testing.Platform switches dotnet test to MTP mode (dotnet test).

The rollForward values, for a version written x.y.znn (major, minor, feature band z, patch nn):

Value Behaviour

patch (default)

The version, else the latest patch in the same feature band; else fail.

feature

The latest patch in the same band, else the next higher band of the same x.y; else fail.

minor

As feature, then the next higher minor of the same major; else fail.

major

As minor, then the next higher major; else fail.

latestPatch

The highest patch >= the version in the same band.

latestFeature

The highest band and patch >= the version in the same x.y.

latestMinor

The highest minor, band and patch >= the version in the same major.

latestMajor

The highest installed SDK >= the version.

disable

Exactly the version.

The latest* values always take the newest match; the others take the nearest match and move further only when nothing nearer is installed. Ledgerline uses 10.0.402 with latestPatch: every developer and CI agent runs a 10.0.4xx SDK at or above 402, and nobody moves to 10.0.5xx by accident. The documentation adds that repositories using NuGet lock files should consider disable, so that the SDK (whose pruning data affects the restore graph) and the lock file change together (Lock Files and Locked Mode).

global.json accepts // and /* */ comments. dotnet new globaljson --sdk-version 10.0.402 --roll-forward latestPatch writes one.

A custom error message

errorMessage tells a contributor what to do instead of printing a version mismatch:

{
  "sdk": {
    "version": "10.0.500",
    "rollForward": "latestFeature",
    "errorMessage": "Ledgerline needs the .NET SDK 10.0.500 or later. Run ./eng/install-sdk.sh."
  }
}
The command could not be loaded, possibly because:
  * You intended to execute a .NET application:
      The application '--version' does not exist or is not a managed .dll or .exe.
  * You intended to execute a .NET SDK command:
      Ledgerline needs the .NET SDK 10.0.500 or later. Run ./eng/install-sdk.sh.

Invalid files are ignored, not rejected

A global.json that cannot be used does not stop the build. "version": "10.0" and "version": "ten" both built with the latest SDK and no warning; only dotnet --info reports it:

global.json file:
  Invalid [~/sel3/global.json]
    Version '10.0' is not valid for the 'sdk/version' value
    Invalid global.json is ignored for SDK resolution.

A JSON syntax error is worse: a trailing comma ({"sdk":{"version":"10.0.402",}}) crashed dotnet build with System.TypeInitializationException: The type initializer for 'Microsoft.DotNet.Cli.Parser' threw an exception and a JsonException about the trailing comma. Check dotnet --version in CI right after the SDK is installed, and fail the job if it is not the expected version.

Testing a prerelease SDK locally

paths (SDK 10.0 and later muxers) lets a repository use an SDK that is not installed globally — for example a .NET 11 preview — by installing it into a repository folder:

{
  "sdk": {
    "version": "9.0.300",
    "rollForward": "latestFeature",
    "paths": [ ".dotnet", "$host$" ],
    "errorMessage": "Run ./eng/install-sdk.sh to install the SDK into .dotnet/."
  }
}

The folder must be a complete dotnet root, with the runtime the SDK runs on. With only .dotnet/sdk/9.0.319/ present, the muxer selected the SDK and then failed:

You must install or update .NET to run this application.

App: ~/.dotnet/sdk/9.0.319/dotnet.dll
Architecture: x64
Framework: 'Microsoft.NETCore.App', version '9.0.21' (x64)
.NET location: ~/paths/.dotnet/

No frameworks were found.

After installing the SDK into .dotnet with dotnet-install.sh --install-dir .dotnet, the repository runs it while the rest of the machine keeps 10.0.402:

dotnet --version
dotnet --info | sed -n '/^.NET SDK:/,/Base Path/p'
9.0.319
.NET SDK:
 Version:           9.0.319
 Commit:            9de5af5aa0
 Workload version:  9.0.300-manifests.f33e7c40
 MSBuild version:   17.14.60+43b635718

Runtime Environment:
 OS Name:     ubuntu
 OS Version:  24.04
 OS Platform: Linux
 RID:         linux-x64
 Base Path:   ~/paths/.dotnet/sdk/9.0.319/

paths affects SDK commands only (dotnet build, dotnet run, …). An app started with its apphost or with dotnet app.dll still resolves runtimes from the normal locations. Add .dotnet/ to .gitignore.

Checking for updates

dotnet sdk check
.NET SDKs:
Version       Status
----------------------------------------------------
9.0.319       .NET 9.0 is going out of support soon.
10.0.402      Up to date.

.NET Runtimes:
Name                          Version      Status
---------------------------------------------------------------------------------
Microsoft.NETCore.App         8.0.32       .NET 8.0 is going out of support soon.
Microsoft.AspNetCore.App      9.0.21       .NET 9.0 is going out of support soon.
Microsoft.NETCore.App         9.0.21       .NET 9.0 is going out of support soon.
Microsoft.AspNetCore.App      10.0.13      Up to date.
Microsoft.NETCore.App         10.0.13      Up to date.
NET 8 and .NET 9 both leave support on 2026-11-10; .NET 10 is supported until 2028-11-14

(Versions and Release History).

Source: dotnet sdk check.

NETSDK1141

The MSBuild SDK resolver — used when Visual Studio or msbuild.exe loads a project — reports SDK-selection failures as NETSDK1141: "Unable to resolve the .NET SDK version as specified in the global.json located at …". Its causes are the same as the muxer failure above: a malformed version, an SDK that is not installed, or a wrong path. It did not appear from the dotnet CLI in this environment. The muxer fails first when the current folder’s global.json cannot be satisfied, and a project in a subfolder with an unsatisfiable global.json built with the SDK the muxer had already chosen — even though the global.json page says the project SDK resolver starts its search from the solution or project folder. Run dotnet commands from the repository root, where the global.json is.

Common mistakes

  • No global.json — every machine builds with its newest SDK, including previews.

  • A partial version (10.0) — silently ignored.

  • rollForward left at the default patch with a version from another band — fails as soon as that band is not installed.

  • latestMajor "to be safe" — the next major SDK changes analyzers, defaults and warnings without a commit.

  • A .dotnet folder with only sdk/ — "No frameworks were found".

  • Running commands from a subfolder with its own global.json — a different SDK than CI.