A Windows PC that has seen a few years of .NET work carries several SDKs and more runtimes, and two questions decide whether a build works: which SDK builds this project, and which runtime runs the result. This part answers both with the commands, on a PC with four SDKs and six runtimes, pins and unpins an SDK with global.json, breaks a build on purpose with NETSDK1045, runs an app where its runtime is missing, and reads the event log of the morning Visual Studio replaced the whole .NET 10 toolset underneath a running build.

Steps in this part
  1. dotnet --list-sdks and dotnet --list-runtimes, then dotnet --version in an empty folder: the highest SDK wins when nothing pins it
  2. dotnet new globaljson --sdk-version 10.0.402 in a project, then edit the version to one you do not have and read the error
  3. Add "rollForward": "latestFeature", then "latestMajor", and watch which SDK dotnet --version picks
  4. Pin 9.0.317 and build a net10.0 project: NETSDK1045
  5. Build a net8.0 app and run its dll in the sdk:10.0 container, with and without DOTNET_ROLL_FORWARD=Major
  6. dotnet nuget locals all --list and the size of your global packages folder
  7. dotnet new tool-manifest and a local dotnet-ef next to the global one

What is installed, and which one answers

This PC lists four SDKs, 8.0.426, 9.0.317, a leftover 10.0.100 release candidate and 10.0.402, and six runtime versions from 6.0.36 to 10.0.13. In an empty folder dotnet --version printed 10.0.402: with no global.json anywhere above the folder, the highest installed SDK is used. The documentation says the same, "the highest installed SDK version is used (equivalent to setting rollForward to latestMajor)", and adds the detail people miss: the search "starts from the current working directory, which isn't necessarily the same as the project directory". Pin an SDK in a parent folder and every project under it changes.

Pinning an SDK, and the error when it is not there

dotnet new globaljson --sdk-version 10.0.402 writes a file with only that version. Edit it to 10.0.300, a version this PC does not have, and nothing works any more, not even dotnet --version:

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: 10.0.300
global.json file: C:\...\SdkLab\global.json

Installed SDKs:
8.0.426 [C:\Program Files\dotnet\sdk]
9.0.317 [C:\Program Files\dotnet\sdk]
10.0.100-rc.1.25451.107 [C:\Program Files\dotnet\sdk]
10.0.402 [C:\Program Files\dotnet\sdk]

Install the [10.0.300] .NET SDK or update [C:\...\SdkLab\global.json] to match an installed SDK.

Why does 10.0.402 not satisfy a request for 10.0.300? Because a version with no rollForward means the patch policy, which the documentation defines as "Uses the specified version. If not found, rolls forward to the latest patch level. If not found, fails." The feature band is the hundreds digit: 10.0.402 is band 4, the request was band 3, and patch roll-forward stays inside the band. Pinning 10.0.400 with no policy picked 10.0.402, which is the patch roll-forward working as described. The paths array that lets a global.json point at local SDK folders is new in the .NET 10 SDK and was not tested here.

global.jsondotnet --version picked
10.0.300, no rollForwardfails, as above
10.0.300 with latestFeature10.0.402
10.0.300 with latestMajor10.0.402
8.0.100 with latestFeature8.0.426
8.0.100 with latestMajor10.0.402
10.0.400, no rollForward10.0.402
9.0.3179.0.317 in the folder, 10.0.402 one level up

The 8.0.100 pair shows the difference between the two policies: latestFeature stays on the requested major and minor, latestMajor takes the newest SDK on the machine. The last row is the habit that keeps a team honest: a global.json in the repository root pins the build for everyone, and only inside that tree.

The SDK is too old for the project: NETSDK1045

With 9.0.317 pinned and a project that targets net10.0, dotnet build stops during restore:

error NETSDK1045: The current .NET SDK does not support targeting .NET 10.0.  Either target .NET 9.0 or lower, or use a version of the .NET SDK that supports .NET 10.0. Download the .NET SDK from https://aka.ms/dotnet/download

Removing the pin made the same build succeed on 10.0.402. The error has a Fixes post, and the message is a clue in itself: an SDK can build any older target framework, so when it refuses a newer one, the SDK is the thing to update or unpin, not the project.

Which runtime runs the app

The SDK is for building; the runtime is what runs the result, and an app asks for a specific one. A net8.0 console app built on this PC carries "version": "8.0.0" for Microsoft.NETCore.App in its runtimeconfig. On the host it ran on 8.0.32, the highest 8.0 patch installed, although 8.0.30 is also present; the documentation's rule is "The host chooses the latest patch version installed on the machine." Copied into the sdk:10.0 container, where only a 10.0 runtime exists, it did not run:

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

App: /app/RtLab.dll
Architecture: x64
Framework: 'Microsoft.NETCore.App', version '8.0.0' (x64)
.NET location: /usr/share/dotnet/

The following frameworks were found:
  10.0.10 at [/usr/share/dotnet/shared/Microsoft.NETCore.App]

The default policy is Minor: roll forward within the same major, never across it. With DOTNET_ROLL_FORWARD=Major in the container's environment the same dll ran and printed Runtime: 10.0.10; on the host, the same variable changed nothing and the app still reported 8.0.32, because the Major policy only moves "if requested major version is missing". So the honest production fix is still the one in the Fixes post: install the runtime the app asks for, and use roll-forward as a deliberate choice, not an accident.

Where the packages go

dotnet nuget locals all --list prints four folders. On this PC the global packages folder is not the default under the profile but C:\Nuget, because the machine-level variable NUGET_PACKAGES says so, and it holds 932 package folders, 146,962 files and 35.58 GB. Every project on the machine restores into it, which is why a restore of a package you used last year is instant and why that folder is the first place to look when the disk fills up. dotnet nuget list source showed nuget.org and the Visual Studio offline packages folder, the two sources a Visual Studio install leaves behind.

Global tools and local tools

dotnet tool list --global showed one tool, dotnet-ef 10.0.0. In a project folder, dotnet new tool-manifest followed by dotnet tool install dotnet-ef installed 10.0.13 into a manifest, and inside that folder tree dotnet ef --version answered 10.0.13 while one folder up it still answered 10.0.0. The manifest travels with the repository, so every developer and the build server get the same tool version; the global tool stays whatever each PC happened to install. One detail of SDK 10.0.402: the manifest was written as dotnet-tools.json in the folder itself, not under a .config folder as older documentation shows.

The morning Visual Studio replaced .NET 10

On October 14, 2026 the Visual Studio 18.11 update ran on this PC, and the Application event log records what it did to .NET, product by product. It removed the 10.0.12 runtimes and the 10.0.401 SDK first, starting at 09:11, and installed 10.0.13 and 10.0.402 afterwards, from 09:18. The x64 .NET runtime was absent for five and a half minutes; the SDK toolset for seven and a half; from the first removal to the last core install was eleven minutes forty seconds, and every build started in that window failed with messages that had nothing to do with the code. The documentation warns about exactly this: "Visual Studio only ever installs a single copy of the .NET SDK. So if you upgrade your Visual Studio version, it removes the previous version of the .NET SDK it had used". The log also shows the update finding dotnet.exe held by a running dotnet process at 09:22 and deferring a restart. Two lessons: a global.json pinned to the exact SDK version that Visual Studio installed will break on the next Visual Studio update, so pin with latestFeature or install the SDK from the standalone installer; and stop your dev servers before you let the installer run. Earlier the same morning Windows Update had moved 8.0.31 to 8.0.32 the other way round, install first and remove second, with no gap at all.

Frequently asked

How does dotnet choose which SDK to use?
It looks for a global.json from the current folder upwards. With none, the highest installed SDK is used. With a version and no rollForward, the patch policy applies: the exact version or a later patch in the same feature band, otherwise the error A compatible .NET SDK was not found. rollForward latestFeature allows a newer feature band of the same major and minor; latestMajor allows any newer SDK.
What does NETSDK1045 mean?
The SDK that is building the project is older than the TargetFramework the project asks for, for example SDK 9.0.317 building net10.0. An SDK can target older frameworks but never newer ones. Remove or change the global.json pin, or install the newer SDK. The error is raised during restore, before any code is compiled.
Why does Visual Studio remove my .NET SDK?
Visual Studio installs a single copy of the .NET SDK and, when it updates, removes the previous version before installing the new one, even across major versions. On this PC the October 2026 update left no .NET 10 runtime for about five minutes and no toolset for seven. Pin SDKs with rollForward latestFeature rather than an exact version, or install the SDK you depend on from the standalone installer.

Next: Part 2, WSL for .NET work: a throwaway distribution, the SDK inside it, the speed difference between /mnt/c and the Linux file system, and reaching a server in WSL from Windows.