Should I use global.json?
- 9 minutes read - 1791 wordsUpdated on
I often get asked if it is better to have a global.json in a .NET project (not necessarily .NET Core) to define a specific .NET Core SDK version and, unfortunately, if you need a short answer you will get from me the typical engineer answer: it depends! Here is the full answer so you can decide what suits best for your needs.
What is global.json
Let me first introduce what this file is.
The global.json
file in the root of your project (or even in the root of your file system as the SDK will traverse the file system until it finds one) allows defining which version of the SDK to use.
At the beginning of the .NET Core development, this file was defining more aspects of a project, similar to a Visual Studio Solution (sln
) file, but all these others aspects have been dropped after the move from project.json
to csproj
.
How to pin the SDK version
To specify the version of the .NET Core SDK to use, simply define it in the global.json
as:
{
"sdk": {
"version": "2.2.300"
}
}
when you then build in Visual Studio or use any command of the dotnet
CLI, this specific version will be used.
What happens if it is not present
If this file is missing (or doesn’t specify a version) the latest of the installed version of the SDK will be used. You can check which version will be used by running the following command from the folder of your project (so that the global.json is taken into account if present):
❯ dotnet --version
3.0.100-preview5-011568
You can also list all the installed versions with:
❯ dotnet --list-sdks
2.1.602 [C:\Program Files\dotnet\sdk]
2.1.700-preview-009597 [C:\Program Files\dotnet\sdk]
2.1.700-preview-009601 [C:\Program Files\dotnet\sdk]
2.1.700-preview-009618 [C:\Program Files\dotnet\sdk]
2.2.103 [C:\Program Files\dotnet\sdk]
2.2.202 [C:\Program Files\dotnet\sdk]
2.2.204 [C:\Program Files\dotnet\sdk]
2.2.300 [C:\Program Files\dotnet\sdk]
3.0.100-preview5-011568 [C:\Program Files\dotnet\sdk]
This means that if you install a new version of the SDK on your machine (or a build agent) all the projects that don’t have a global.json
that pin a specific version, will start to use it.
It also means that if you install a preview version of the next version of the SDK you will start to build and test all your projects with that preview version.
Note this will apply to both the dotnet
command-line tool and Visual Studio 2017 but not Visual Studio 2019 which by default ignores preview versions.
What happens if that SDK version is not installed
If that version is not installed, since .NET Core 2.2
, it will try to find the latest SDK version greater then the one required, constrained to the .NET Core version as well to the SDK feature band, which is defined by the first of the last 3 digits of the SDK version.
So for example, if we specify 2.2.204
and that version is not installed, it will try to use the highest 2.2.2xx
version installed, but it must be higher than 2.2.204
.
The dotnet
cli as well as Visual Studio will give you a meaningful error if they can’t find any appropriate version to use:
❯ dotnet build
A compatible installed dotnet SDK for global.json version: [2.2.300] from [C:\Users\Ale\global.json] was not found
Please install the [2.2.300] SDK or update [C:\Users\Ale\global.json] with an installed dotnet SDK:
2.1.502 [C:\Program Files\dotnet\sdk]
2.2.204 [C:\Program Files\dotnet\sdk]
3.0.100-preview5-011568 [C:\Program Files\dotnet\sdk]
When it is a problem to automatically use the latest version
Breaking changes in the SDK
A new version of the SDK usually adds new functionality or support for newer runtimes, however, sometimes there could be breaking changes, especially around the default behaviour.
For example, the .NET Core SDK 2.1.200 changed the default verbosity of the VSTest
verbosity to quiet
, causing the dotnet test
command to stop reporting test results on CI tools (see microsoft/vstest#1580) unless you were explicitly adding a verbosity
parameter.
In general, this sort of changes should only happen when changing the feature band.
Reproducible builds
A good practice is to have reproducible build, that is a guarantee that building again the same commit on a different machine will produce the same output. Specifying the version of the SDK in the global.json in the repository gives you a guarantee that every time that build is executed it will always use the same sdk and the outcome will be the same (assuming your build process doesn’t do anything particular that can have each time a different outcome). The roll-forward doesn’t create an issue as it is guaranteed that every SDK in the same feature band will behave the same but, if you don’t trust this, you can make sure to always have the specific SDK installed where you run your builds.
Runtime included in self-contained
If you publish self-contained applications, that is using the dotnet publish --self-contained --runtime win10x64
, the .NET Core runtime will be included in your deployables and be used to run the application.
Unless you explicitly specify a runtime version with the RuntimeFrameworkVersion
attribute in your csproj
, the runtime included will be the default one shipped with the SDK.
However, when the SDK is rolling forward, this will mean a potential greater patch version will be used.
This is generally a good thing as this means you will get the latest fixes (mostly security fixes), however, this means that the outcome of the publish will be different based on which versions of the SDK are installed.
If you want a guarantee to always publish the same runtime for a given commit, you need to also specify the runtime with the csproj
attribute RuntimeFrameworkVersion
, but that will mean you will have to manually update this when a new version is released (of course after installing it on all the machines you use to build).
Problems introduced
Every contributor and build agent needs to install a similar version
For the build to work, you need to have a matching or similar version following the rules described above.
This means that all contributors, as well as who maintains the build agents, need to install an appropriate version. Installing new versions every time the project SDK requirement in global.json
is updated can soon become burdensome, but generally, it’s a good practice to install and use new versions once they get released.
If the goal is to get reproducible build, it also requires to install the exact version, however, there is no way to enforce only the exact version to work, but, as described above, the matching rules should be enough to achieve this.
When publishing self-contained apps, it must be updated to use the latest runtime
If publishing self-contained apps it is not sufficient to rely on the roll-forward to use the latest runtime version, as if an exact match is detected, the roll-forward will not happen. For this reason, you are left with two options:
- When updating the build agents, remove the previous versions in the same feature band.
This, however, has a few drawbacks:
- Requires discipline or proper automation.
- The runtime used will depend on the SDK installed at the time of building, so it is not reproducible.
- Doesn’t allow contributors to easily know which version should be used.
- Contributors must uninstall old versions (or modify the global.json every time on their machine) to have the same behaviour. However they may want to have old versions to work on other projects, so this is quite a big ask.
Please note that this matter also when using the new --self-contained false
feature because, even if a runtime is not included, the generated executable depends on a specific runtime version.
What will happen with .NET Core 3.0
With .NET Core 3.0, the behaviour introduced in 2.2 of picking, when an exact match is not present, the highest version in the same feature band, will go one step further. In fact, in .NET Core 3.0 the SDK installer will take care to always have only one version installed per feature band. This means that as soon as you install a new version in the same feature band, there will be no more an exact match and the latest version will be used.
This will mean that defining 3.0.100
in the global.json
is equivalent as saying: please use the latest 3.0.1xx
you have installed (which will be the only one given that will be always at most one version installed per feature band).
As of .NET Core 2.2 it is not possible to have this behaviour as specifying 2.2.200
will only use the latest 2.2.2xx
if 2.2.200
is not installed.
It will also solve the problem of the proliferation of SDKs on your machine, including preview versions.
However, there are still mixed feeling if this is the correct behaviour so it could still change before .NET Core 3.0 reach RTM in July 2019.
Summary
Here a quick summary of the pros and cons in table format.
Feature | SDK version in global.json | No constraint |
---|---|---|
Reproducible builds | ✅ | ❌ |
Easy of contributing | ❌ | ✅ |
Self-contained: use specific runtime (without RuntimeFrameworkVersion ) | Only when matching SDK installed | ❌ |
Self-contained: use latest runtime | Only when matching SDK is NOT installed | ✅ |
Avoid installed preview versions | ✅ | ❌ (✅ in VS2019) |
Use latest version in same feature band | ❌ (✅ in .NET Core 3.0) | ❌ |
There is no right choice and you should evaluate what will work better based on your requirements.
With .NET Core 3.0 and the new roll-forward behaviour I believe it would be a lot more convenient to use global.json
defining only the feature band and not the specific version (e.g. always using 00
for the last two digits). However, you also need to make sure to always keep your build agents up to date, especially when building self-contained apps (as you want to get the latest runtime patches).
With the current version, I still believe the pros of using a global.json
are usually worth degrading the experience of contributors, but there could be a situation when you prefer to lower the bar of entry both in open-source and internal company projects (e.g. you want to encourage collaboration across teams).
A compromise can be achieved if you can specify the version of the SDK to use for your CI pipeline in a file committed in the repository, as you will have reproducible builds on your CI without constraining developers to install the specific version as for most of the development work it will not make a difference, but it will require to remember to temporary add a global.json
when in need to reproduce specific issues which requires a matching SDK or runtime.
Also, as previously highlighted, if you want a guarantee to use a specific runtime for self-contained applications, use the RuntimeFrameworkVersion
attribute in the csproj
.
I hope this helps! If you have any question, please leave a comment or send me a tweet.