Setup Local Preview .NET

In this blog post I will detail how to setup early preview versions of .NET for development. My findings apply for .NET 9, but the methods should be applicable in general to most versions of .NET (considering version specific details).

While a preview version of .NET can be downloaded at any time from the dotnet website, installing the forthcoming preview version requires somewhat more investment.

The findings of this post are based on ASP.NET Core, which uses a similar, but more complex approach in its restore.cmd command. This blog post will focus on the key components to have the latest SDK or a custom runtime available.

The preview versions of .NET can heavily change. Features may get added, removed, changed, or their performance might significantly improve. It is always suggested to choose and use an LTS or STS release for production applications. However, in certain cases a developer might want to test the latest preview features, in which case the methods described in this blog post will help.

Most of the scripts below are written in PowerShell, which is cross-platform, when using the current version (v7.4.4). The key sections:

  • installing the SDK and runtime

  • override the default .NET SDK and runtime in a given project

  • setup Visual Studio to use the latest SDK or a custom runtime

  • setup nuget sources

Install the SDK and Runtime

The install script of .NET is available at https://dotnet.microsoft.com/download/dotnet/scripts/v1/dotnet-install.ps1. It can install both the SDK as well as the runtime. We can select to install different runtimes or SDKs.

The script below downloads the above installation script, then invokes it passing the versions: $sdkVersion = '9.0.100-preview.7.24371.4' and $runtimeVersion = '9.0.0-rc.1.24412.13' to install a custom SDK and runtime. To have .NET as a local installation for a given project, the script places all artifacts under the .dotnet folder relative to the location of this script.

function GetDotNetInstallScript([string] $dotnetRoot) {
  $installScript = Join-Path $dotnetRoot 'dotnet-install.ps1'
  if (!(Test-Path $installScript)) {
    New-Item -Path $dotnetRoot -Force -ItemType 'Directory' | Out-Null 
    $ProgressPreference = 'SilentlyContinue'
    $uri = "https://dotnet.microsoft.com/download/dotnet/scripts/v1/dotnet-install.ps1"
    Write-Host "GET $uri"
    Invoke-WebRequest $uri -OutFile $installScript
  }

  return $installScript
}

function InstallDotNet(
  [string] $dotnetRoot,
  [string] $version,
  [string] $runtime = '') {

  $dotnetVersionLabel = "'$version'"

  $installScript = GetDotNetInstallScript $dotnetRoot
  $installParameters = @{
    Version               = $version
    InstallDir            = $dotnetRoot
    SkipNonVersionedFiles = $False
    NoPath                = $True # PATH is not modified
  }  
  if ($runtime -ne '' -and $runtime -ne 'sdk') {
    $installParameters.Runtime = $runtime
  }

  Write-Host "Attempting to install $dotnetVersionLabel"
  & $installScript @installParameters
  Write-Host "Successfully to installed $dotnetVersionLabel."
}

function UpdateGlobalJson(
  [string]$SdkVersion
) {
  $jsonContent = @{
    sdk   = @{
      version = $SdkVersion
    }
  } | ConvertTo-Json -Depth 4

  # Specify the output file path
  $outputFilePath = ".\global.json"

  # Write the JSON content to the file
  $jsonContent | Out-File -FilePath $outputFilePath -Encoding UTF8

  Write-Host "global.json updated at $outputFilePath"
}

$sdkVersion = '9.0.100-preview.7.24371.4'
$runtimeVersion = '9.0.0-rc.1.24412.13'

$dotnetRoot = Join-Path $PSScriptRoot '.dotnet'
InstallDotNet $dotnetRoot $sdkVersion 'sdk'
InstallDotNet $dotnetRoot $runtimeVersion 'dotnet'
UpdateGlobalJson $sdkVersion

To avoid all other projects using installation, the script is instructed not to change the PATH by setting the NoPath to true. Even after running this script all applications will still use the version of .NET that is installed on the machine by the regular installers.

Confirm this by running dotnet --info, which will list the SDKs and runtimes installed on the machine, but does not list the downloaded ones:

.NET SDKs installed:
  8.0.108 [C:\Program Files\dotnet\sdk]
  9.0.100-preview.6.24328.19 [C:\Program Files\dotnet\sdk]
  9.0.100-preview.7.24407.12 [C:\Program Files\dotnet\sdk]

Add an exclusion for the .dotnet folder in the .gitignore file, so that this folder is not added to the git repo:

.dotnet/

Enable the SDK

To enable using the downloaded runtime and SDK, a few environment variables must be set.

activate.ps1

This PowerShell script tests if it has been invoked dot sourced, so that the environment variables are set scope of the invoking session.

# This file must be invoked as ". .\activate.ps1" from the command line.
if ($MyInvocation.CommandOrigin -eq 'runspace') {
    Write-Host -f Red "This script file must be 'dot sourced' by calling `". $PSCommandPath`"."
    exit 1
}
# Tell dotnet where to find itself
$env:DOTNET_ROOT = "$PSScriptRoot\.dotnet"
$env:DOTNET_MULTILEVEL_LOOKUP = 0
$env:PATH = "${env:DOTNET_ROOT};${env:PATH}"

Invoke the above script dot sourced as: . .\activate.ps1.

The script sets the .dotnet folder - where the SDK and the runtime is installed - on the PATH for the current user session. It also disables multilevel lookup, so that the .NET tooling will not search for other SDK and runtime installations.

Executing dotnet --info in the same session shows now the downloaded SDK and runtime:

.NET SDKs installed:
  9.0.100-preview.7.24371.4 [D:\repos\<path>\.dotnet\sdk]
...
.NET runtimes installed:
  Microsoft.NETCore.App 9.0.0-preview.7.24366.18 [D:\repos\<path>\.dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 9.0.0-rc.1.24412.13 [D:\repos\<path>\.dotnet\shared\Microsoft.NETCore.App]

Executing the dotnet command will now use this local .NET SDK.

startvs.cmd

To achieve the same with Visual Studio a cmd can be created that also open the corresponding solution file:

@ECHO OFF
SETLOCAL
SET DOTNET_ROOT=%~dp0.dotnet
SET DOTNET_MULTILEVEL_LOOKUP=0
SET PATH=%DOTNET_ROOT%;%PATH%
start "" "ConsoleApp.sln"

It executes the same commands as the above powershell script, and then open the sln file.

Use the SDK and Runtime

It may happen that multiple SDKs or runtimes are installed in the .dotnet folder. Notice that the install script does not remove previous installations. In such cases the .NET CLI will list all SDKs and runtimes installed in the .dotnet folder. A project may control which one to be used by placing a global.json file next to the project or solution file. The global.json file tells which SDK to use for the given project. Notice that the installation script above create/overwrites this file with the currently installed version.

{
  "sdk": {
    "version": "9.0.100-preview.7.24371.4"
  }
}

A specific runtime (independent from the SDK) can be further set by the runtimeconfig.json file or (<RollForward>latestminor</RollForward> proeprty info member) or with the --fx-version / --roll-forward LatestMinor when using the CLI.

Setup NuGet

Finally, when building and running the application in Visual Studio, additional nuget packages are required, for example: microsoft.net.sdk.compilers.toolset. Certain versions of this package is available on nuget.org, but most version are only accessible from a private nuget source. To access these version, add a nuget.config file to the repo enabling dotnet9 nuget sources:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <add key="dotnet9" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9/nuget/v3/index.json" />
  </packageSources>
</configuration>