Managing .NET SDK Versions with global.json

If you've ever had a build work on your machine but fail on a colleague's — or in CI — the cause is often a mismatched .NET SDK version. global.json solves this by pinning the SDK version for a repository, ensuring every developer and build agent uses the same toolchain.

The basics

Create a global.json file in your repository root:

global.json
{
  "sdk": {
    "version": "8.0.404"
  }
}

When you run dotnet build (or any dotnet CLI command) in a directory that contains a global.json — or any ancestor directory — the CLI uses the specified SDK version. If that version isn't installed, the command fails with a clear error message rather than silently using whatever is available.

Generate one interactively:

dotnet new globaljson --sdk-version 8.0.404

Roll-forward policies

Strict version pinning is sometimes too rigid. The rollForward property controls how the CLI selects an SDK when the exact version isn't available:

data.json
{
  "sdk": {
    "version": "8.0.400",
    "rollForward": "latestPatch"
  }
}

The available policies, from most restrictive to most permissive:

Policy Behaviour
disable Exact match only. Fails if the version isn't installed.
patch Uses the requested version or the latest patch in the same major.minor.patch-band. Default behaviour.
feature Uses the latest patch in the same major.minor with a feature band >= the requested one.
latestPatch Uses the latest patch in the same major.minor.feature-band.
latestFeature Uses the latest feature band and patch in the same major.minor.
latestMajor Uses any installed SDK >= the requested version.
latestMinor Uses the latest minor, feature band, and patch in the same major.

For most teams, latestPatch strikes the right balance — you get security patches without unexpected feature changes.

Understanding SDK versioning

The .NET SDK version format is major.minor.feature-band-patch:

8.0.404
│ │ │││
│ │ │││
│ │ ││└─ patch (04)
│ │ │└── patch (04) -- combined as feature band patch
│ │ └─── feature band (4)
│ └───── minor (0)
└─────── major (8)

The feature band represents a quarterly release cadence. SDK 8.0.100 shipped with .NET 8 GA; 8.0.200 came with Visual Studio 17.9; 8.0.300 with Visual Studio 17.10; and so on.

This distinction matters because rollForward: "patch" won't jump from 8.0.100 to 8.0.200 — those are different feature bands.

Multiple SDKs on one machine

You can install multiple SDK versions side by side. List installed versions:

dotnet --list-sdks

Output:

6.0.428 [C:\Program Files\dotnet\sdk]
8.0.404 [C:\Program Files\dotnet\sdk]
9.0.200 [C:\Program Files\dotnet\sdk]

Without a global.json, the CLI uses the latest installed SDK. This is why a global.json matters — without it, a developer who installs .NET 9 Preview will use preview tooling for a .NET 8 project, which may produce different build artifacts or exhibit different compiler behaviour.

CI/CD integration

In CI pipelines, install the exact SDK version from your global.json. For GitHub Actions:

config.yaml
- uses: actions/setup-dotnet@v4
  with:
    global-json-file: global.json

This reads the version from your global.json and installs it, ensuring CI matches local development exactly.

For Azure DevOps:

config.yaml
- task: UseDotNet@2
  inputs:
    useGlobalJson: true

Keeping the SDK current

A stale global.json can block important updates. Establish a routine:

  1. When a new patch release drops (e.g., 8.0.404 to 8.0.405), update global.json and test.
  2. When a new feature band ships (e.g., 8.0.400 to 8.0.500), update with more care — feature bands occasionally include SDK-level changes.
  3. When a new major version ships, plan a proper upgrade.

Automate this with Dependabot or Renovate — both support global.json as a tracked file.

Additional global.json options

Beyond SDK pinning, global.json can configure MSBuild SDK resolvers:

data.json
{
  "sdk": {
    "version": "8.0.404",
    "rollForward": "latestPatch"
  },
  "msbuild-sdks": {
    "Microsoft.Build.Traversal": "4.1.0"
  }
}

This is useful for custom MSBuild SDKs used in large repositories for build orchestration.

Wrapping up

A global.json is a small file with an outsized impact on build reliability. Add one to every repository, choose a sensible roll-forward policy, and integrate it into your CI pipeline. It takes two minutes to set up and prevents hours of "works on my machine" debugging.