This is the story of Tom, a .NET developer who has just finished implementing a new feature in an ASP.NET Core application. Everything works perfectly on his machine, and he submits his code via a pull request. However, the CI pipeline fails to compile due to an IDE0100 error. Not understanding the cause of the problem, Tom asks for help from his colleague Brian, who is able to reproduce it.
Under pressure due to tight deadlines, Tom and Brian do not take the time to investigate why the error does not appear on Tom’s machine. They decide to disable the error with a #pragma
directive, and the pull request is quickly completed. The application is then successfully deployed to production a few minutes later.
Everything seems to end well, but that’s not the case. If Tom and Brian had taken the time to examine the situation more closely, they would have discovered several irregularities in their application’s compilation process. Here’s what really happened.
Tom uses version 8.0.200
of the .NET SDK, while Brian has version 8.0.204
. Curious about the new features of .NET 9, Brian has also installed the preview version 9.0.100-preview.4
. The CI pipeline, hosted on Azure DevOps, uses the UseDotNet@2
task to install the 8.x
version of the SDK, including preview versions.
The day the IDE0100
error appeared was the same day Microsoft released SDK version 8.0.300
. This new version introduces a fix that changes the behavior of the Roslyn analyzer IDE0100
. Tom, using an earlier and vulnerable version of the .NET 8 SDK, was not affected. However, Brian compiled the application with the .NET 9 SDK preview 4, which included this new behavior.
The irony is that the application is deployed as a container, and the base image used is mcr.microsoft.com/dotnet/sdk:8.0.100
, the first version of the .NET 8 SDK, released in November 2023. This version contains several known vulnerabilities and is no longer visible on Docker Hub.
Here are the conclusions that can be drawn from this story inspired by real events:
- Tom and Brian’s team did not implement a mechanism to ensure that the .NET SDK version used to compile the application is the same on all machines.
- Their CI behavior can change without notice as Microsoft releases new preview versions of the SDK.
- SDK versions containing vulnerabilities that have already been fixed are still being used on both developer machines and in production.
This situation can be avoided by explicitly specifying the required .NET SDK version to compile the application. In this article, we will see how to do this using a global.json
file, as well as other techniques to ensure regular SDK updates controlled by automatic dependency management.
Understanding .NET SDK versioning
Before explaining what a global.json
file is, it is important to understand the .NET SDK versioning and its release cycle.
Every year, a major version of .NET is released: .NET 5.0, 6.0, 7.0, 8.0, and soon 9.0 in November 2024. This is generally the version that developers reference in projects with <TargetFramework>netX.0</TargetFramework>
.
The .NET SDK, however, usually receives monthly updates that may include security patches, new features, or component updates such as NuGet or MSBuild. Therefore, the SDK versioning differs slightly from the runtime versioning. For example, the first SDK for .NET 8 was 8.0.100
. This version corresponds to the feature band 8.0.1nn
. An increment of the hundredths digit in the third section of the version number may indicate the addition of new features and possibly a breaking change.
Thus, 8.0.101
and 8.0.201
belong to different feature bands, while 8.0.101
and 8.0.199
are in the same feature band. The change in the nn
digits in 8.0.1nn
indicates that there are no new features, and most often this corresponds to vulnerability or bug fixes.
In summary, the .NET SDK version format is as follows: x.y.znn
, where:
x
is the major version, usually incremented with each annual major release.y
is the minor version, which has remained at0
since .NET 5.z
is the feature band, which can change monthly, indicating the addition of new features.nn
is the patch version, which can change monthly, indicating bug or vulnerability fixes.
Setting up a global.json
file
A global.json file allows you to define which version of the .NET SDK is used to run .NET CLI commands such as dotnet build
, dotnet run
, or dotnet test
. In the absence of this file, the latest SDK installed on the machine is used. Most of the time, it is created at the root of the solution directory.
{
"sdk": {
"version": "8.0.300",
"rollForward": "latestPatch",
"allowPrerelease": false
}
}
sdk.version
specifies the minimum SDK version required according to the roll-forward strategy defined below.
sdk.rollForward
determines whether a version higher than sdk.version
can be used. Some possible values are:
latestPatch
is the default value and allows any higher patch version of the same major, minor, and feature band version. In the above example,8.0.300
allows8.0.301
,8.0.399
, etc.latestFeature
allows any higher feature band and patch version of the same major and minor version. In the above example,8.0.300
allows8.0.400
,8.0.500
, etc.latestMinor
same principle applied to minor versions. In our example above,8.0.300
allows8.1.100
,8.2.199
, etc.latestMajor
allows any higher major version, regardless of the minor, feature band, or patch version.disable
indicates that an exact version is required.
My recommendations are as follows:
- Use the latest available SDK version that you are using. You can get it on the .NET SDK download page.
- Allow only patches to avoid breaking changes.
- Disallow preview versions.
The goal is to ensure reproducible builds and identical behavior from the developer’s machine to production.
Configure CI tasks to consume the global.json
file
Azure DevOps and GitHub Actions allow you to install .NET SDK in a CI pipeline. It is possible to configure these tasks to use the global.json
file and ensure that the SDK version is the same as the developer’s machine.
Here is the Azure DevOps task to install the .NET SDK from the global.json
file:
- task: UseDotNet@2
displayName: "Install .NET SDK from global.json"
inputs:
packageType: "sdk"
useGlobalJson: true
And here is the corresponding action for GitHub. When no version is specified, the action tries to find a global.json
file in the current directory:
# Detect the global.json file in the current directory
- uses: actions/setup-dotnet@v4
# It is also possible to specify the path of the global.json file
- uses: actions/setup-dotnet@v4
with:
global-json-file: "./something/global.json"
Updating Docker .NET images versions
Let’s look at these few Docker image tags for the .NET SDK, .NET runtime, and ASP.NET Core:
mcr.microsoft.com/dotnet/sdk:8.0
mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim
mcr.microsoft.com/dotnet/sdk:8.0-alpine
mcr.microsoft.com/dotnet/runtime:8.0
mcr.microsoft.com/dotnet/runtime:8.0-jammy-chiseled
mcr.microsoft.com/dotnet/aspnet:8.0-jammy
What they have in common is that they do not specify a specific version, but rather “the latest version of the 8.0 branch”. Using these tags can have several consequences:
- Forgetting to pull the latest image can result in using an old version.
- A new feature band may be released, possibly with breaking changes.
It is preferable, for clarity and consistency with the global.json
file, to specify a precise version of the .NET SDK. For example, mcr.microsoft.com/dotnet/sdk:8.0.301
:
# Build the app
FROM mcr.microsoft.com/dotnet/sdk:8.0.301 AS build
# [...] Omitted for brevity
# Build runtime image
FROM mcr.microsoft.com/dotnet/aspnet:8.0.3
# [...] Omitted for brevity
In the next section, we will also see how to automate version updates in your Dockerfiles.
Automating .NET SDK version updates with Renovate
At this point, you are assured that your developers, your CI, and your containers are using the same version of the .NET SDK. The build result you will get will be the same everywhere.
However, it is important to keep this version up to date to benefit from the latest features and security fixes.
To do this, you can use dependency management tools such as Renovate or Dependabot, so that each new version of the .NET SDK automatically creates a pull request to update the global.json
file and Dockerfiles.
In the event that a breaking change is introduced, you will only need to fix the open pull request by these tools so that everyone benefits from the new version.
Here is an example of a Renovate configuration file renovate.json
to update the .NET SDK:
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:best-practices"
],
"enabledManagers": [
"nuget",
"dockerfile"
],
"packageRules": [
{
"groupName": "dotnet-sdk",
"description": "Enable non-major updates for .NET SDK and runtime (global.json and Docker images)",
"matchPackageNames": [
"dotnet-sdk",
"mcr.microsoft.com/dotnet/sdk",
"mcr.microsoft.com/dotnet/aspnet",
"mcr.microsoft.com/dotnet/runtime",
"mcr.microsoft.com/dotnet/runtime-deps"
],
"extends": [
":disableMajorUpdates",
":pinDigestsDisabled"
]
}
]
}
Read my article Locally test and validate your Renovate configuration files to test your configuration against your repository.
This example produced this result on a demo project:
Want to go a step further? Remove the
:pinDigestsDisabled
rule to pin the digest of the Docker images. The image tag will look like this:mcr.microsoft.com/dotnet/sdk:8.0.301@sha256:1e0c55b0ae732f333818f13c284a01c0e3a2ec431491e23c0a525f6803895c50
.
It has been observed that new .NET Docker images are not immediately available following the release of a new SDK version, which usually occurs during “Microsoft Patch Tuesday”, the second Tuesday of each month. You can use the Renovate schedule option to delay the update by a few days to ensure that the Docker images are available.
Conclusion
Don’t be like Tom and Brian. Take the time to properly configure your development environment and CI pipelines to ensure consistency and security of your builds. By specifying the .NET SDK version with a global.json
file and using tools like Renovate to manage updates, you will avoid unpleasant surprises due to unexpected or vulnerable SDK versions. Also, fix the versions of Docker images in your Dockerfiles to ensure the stability of your deployments. Adopt these best practices for robust and reliable development and deployment.