# 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](https://dotnet.microsoft.com/en-us/download/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.

```ps1
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.

```ps1
# 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:

```cmd
@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.

```json
{
  "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
<?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>
```
