Overview
Microsoft has announced that Cloud Services (classic) will be retired on 31 August 2024. Their recommendation is to move to a new deployment model based on ARM called Cloud Services (extended support).
Before you get started following this guide, I recommend looking into moving to App Services or Kubernetes instead. I have doubts about Microsoft’s support of Cloud Services (extended support) -they do not market it, and in Visual Studio, it is limited to working with .NET Framework, not Core/NET5+. It may be worth your time to move completely away from this deployment model.
However, if you need to continue using Cloud Services, and also want to take advantage of the latest updates in .NET development, this guide will explain how to go about installing.NET 6+ on Cloud Services as well as hosting an ASP.NET Core site that runs on .NET 6+. I will be using .NET 6 in this guide, but you can use any version of .NET where you can download the .NET Hosting Bundle.
Here is a link to the solution used in this guide which you can use to base your own implementation on (or just browse to copy the script files): MorneZaayman/Supporting-.NET-6-in-Cloud-Services-Extended-Support (github.com)
Solution Setup
Before you can begin following this guide, you should have a Cloud Service solution in Visual Studio ready, as well as the associated resources configured in Azure. If you have not done this, you can follow this post of mine to see how to do so.
There is no way (at least at the time of writing), to set the Cloud Service to support anything newer than .NET Framework 4.8. We have to deploy the Cloud Service with a .NET Framework 4.8 ASP.NET application that will be responsible for installing .NET 6+ and then copying the ASP.NET Core application into the correct location for IIS to serve it. A simple, default ASP.NET (Framework) application is good enough for this.
Inside the solution, add a new ASP.NET Core application that targets .NET 6. I went with the defaults and named the application WebApplication1 but thinking about it now, Bootstrap would probably be a better name. Next, right-click on the web role project and select Properties. Under the Build Events tab, add the following under Pre-build event command line:
dotnet publish $(ProjectDir)..\WebApplication1\WebApplication1.csproj -c Release
This will publish the ASP.NET Core project before the ASP.NET Framework project is built. Next, unload the ASP.NET Framework project and then edit the .csproj file. Add the following under one of the <ItemGroup> elements that also have <Content Include… /> elements.
<Content Include="..\WebApplication1\bin\Release\net6.0\publish\**\*.*">
<Link>WebApplication1\%(RecursiveDir)%(Filename)%(Extension)</Link>
</Content>
Now when you package your Cloud project, and the ASP.NET Framework project is built, it will also publish and include the ASP.NET Core project as a folder within it as well.
Create setup scripts
We need to create two files inside the ASP.NET (Framework) project, which is also the WebRole project – Net6Setup.cmd and Net6Setup.ps1. Net6Setup.cmd is used to call a PowerShell script called Net6Setup.ps1 which does the work of downloading and installing .NET 6+ and setting up the ASP.NET Core application in IIS. This .cmd file can be set to automatically run when the Cloud Service instance is started (We’ll get to that soon).
Right-click on both files in the Solution Explorer, select Properties and then set their Build Action to Content and Copy to Output Directory to Copy always.
The contents of the .cmd file is:
Net6Setup.cmd
@echo off
if "%EMULATED%"=="true" (
echo Azure environment is emulated - .NET 6 will not be installed
EXIT 0
)
echo Running Net6Setup.ps1...
powershell -command "Set-ExecutionPolicy Unrestricted"
powershell -File .\Net6Setup.ps1
The .ps1 file contains:
# This script must be run with Administrator privileges in order for .NET 6 to be able to be installed properly
$nl = "`r`n"
# Load the Cloud Service assembly
[Reflection.Assembly]::LoadWithPartialName("Microsoft.WindowsAzure.ServiceRuntime") | Out-Null
# Using the Net6Setup path that has a higher limit than the 100 MB default.
$tempPath = [Microsoft.WindowsAzure.ServiceRuntime.RoleEnvironment]::GetLocalResource("Net6Setup").RootPath.TrimEnd('\\')
[Environment]::SetEnvironmentVariable("TEMP", $tempPath, "Machine")
[Environment]::SetEnvironmentVariable("TEMP", $tempPath, "User")
Write-Output "============ .NET 6 Windows Hosting Installation ============$nl"
Function TestIf-DotNet6Exists
{
$ErrorActionPreference = 'stop'
try
{
if (Get-Command dotnet)
{
$dotnetRuntimes = dotnet --list-runtimes
$net6AppInstalled = if ($dotnetRuntimes -Like "*Microsoft.NETCore.App 6*") { $true } else { $false }
$aspnet6Installed = if ($dotnetRuntimes -Like "*Microsoft.AspNetCore.App 6*") { $true } else { $false }
if ($net6AppInstalled -And $aspnet6Installed)
{
return $true
}
else
{
return $false
}
}
}
Catch
{
return $false
}
}
if (TestIf-DotNet6Exists)
{
Write-Output ".NET 6 is already installed. $nl"
}
else
{
Write-Output ".NET 6 not installed.$nl"
$tempPath = [Microsoft.WindowsAzure.ServiceRuntime.RoleEnvironment]::GetLocalResource("AppTemp").RootPath.TrimEnd('\\')
Write-Output "Downloading the Microsoft Visual C++ 2017 Redistributable.$nl"
$tempFile = New-Item ($tempPath + "\vcredist.exe")
Invoke-WebRequest -Uri https://aka.ms/vs/16/release/vc_redist.x64.exe -OutFile $tempFile -Verbose:$false
Write-Output "Installing the Microsoft Visual C++ 2017 Redistributable.$nl"
$proc = (Start-Process $tempFile -PassThru "/quiet /install /log C:\Logs\vcredist.x64.log")
$proc | Wait-Process
Write-Output "Deleting the Microsoft Visual C++ 2017 Redistributable installer file.$nl"
Remove-Item -Path ($tempPath + "\vcredist.exe") -Force
Write-Output "Downloading the .NET 6 Hosting Bundle.$nl"
$tempFile = New-Item ($tempPath + "\netcore-bundle.exe")
Invoke-WebRequest -Uri https://download.visualstudio.microsoft.com/download/pr/b69fc347-c3c8-49bc-b452-dc89a1efdf7b/ebac64c8271dab3b9b1e87c72ef47374/dotnet-hosting-6.0.1-win.exe -OutFile $tempFile -Verbose:$false
Write-Output "Installing the .NET 6 Hosting Bundle.$nl"
$proc = (Start-Process $tempFile -PassThru "/quiet /install /log C:\Logs\dotnet_install.log")
$proc | Wait-Process
Write-Output "Deleting the .NET 6 Hosting Bundle installer file.$nl"
Remove-Item -Path ($tempPath + "\netcore-bundle.exe") -Force
}
This takes care of installing .NET 6+. Now we need to set up IIS to serve the ASP.NET Core website. We need to copy the ASP.NET Core website out of the ASP.NET Framework folder structure into the ASP.NET Framework folder and delete the ASP.NET Framework application files. This will effectively replace the ASP.NET Framework application with the ASP.NET Core application in IIS. Finally, we need to restart IIS.
Add the following to the bottom of the .ps1 file.
Write-Output "Stop w3svc and IIS"
net stop w3svc
iisreset /stop
Write-Output "Remove ASP.NET Framework application"
Get-ChildItem -Path "E:\sitesroot\0\" -Exclude @('Net6Setup.ps1','WebApplication1*') |
Remove-Item -Recurse -Force
Write-Output "Move ASP.NET Core application"
Get-ChildItem "E:\sitesroot\0\WebApplication1\" | Move-Item -Destination "E:\sitesroot\0\"
Remove-Item "E:\sitesroot\0\WebApplication1\"
Write-Output "Start w3svc and IIS"
net start w3svc
iisreset /start
Configure the .csdef file
Inside the ServiceDefinition.csdef file of the Cloud Service project, add a new Startup task to run the scripts. You also need to add a new LocalStorage element for the installer with a sizeInMB of 5000. The default storage is not large enough.
<?xml version="1.0" encoding="utf-8"?>
<ServiceDefinition name="MzansiBytes" xmlns="http://schemas.microsoft.com/ServiceHosting/2008/10/ServiceDefinition" schemaVersion="2015-04.2.6">
<WebRole name="WebRole1" vmsize="Standard_D1_v2">
<Startup>
<!-- Setup the .NET 6 environment -->
<Task commandLine="Net6Setup.cmd" executionContext="elevated" taskType="simple" />
</Startup>
<LocalStorage name="Net6Setup" sizeInMB="5000" cleanOnRoleRecycle="true" />
<Sites>
<Site name="Web">
<Bindings>
<Binding name="Endpoint1" endpointName="Endpoint1" />
</Bindings>
</Site>
</Sites>
<ConfigurationSettings>
<Setting name="Microsoft.WindowsAzure.Plugins.Diagnostics.ConnectionString" />
</ConfigurationSettings>
<Endpoints>
<InputEndpoint name="Endpoint1" protocol="http" port="80" />
</Endpoints>
</WebRole>
</ServiceDefinition>
Finally, let’s package and deploy the Cloud Service and see if everything works. If so, you’ll be greeted by the ASP.NET Core application after deployment and having navigated to the Cloud Service’s IP address or domain name.