Fixing HTTP 500 on ASP.NET Core under IIS – the missing site-scoped environment variables

This article describes how a routine 500 error on an ASP.NET Core site under IIS was traced to missing site-scoped environment variables. In the past a published web.config supplied those values automatically. Modern deployments place the variables in applicationHost.config instead, and a single PowerShell script now consolidates viewing, setting, and error-log inspection in one place.

What the 500 actually meant

  • The browser displayed HTTP 500.30 – ASP.NET Core app failed to start.
  • The root cause was that the ASP.NET Core Module could not locate the database connection string and other per-site settings because they were not defined at the IIS site level.
  • Without those variables the app silently fell back to a SQLite database that had no tables – every query produced another 500.

Why the variables disappeared on publish

  • Site-scoped environment variables live in applicationHost.config (the IIS server configuration).
  • When the publish profile used Web Deploy (MSDeploy), the web.config file in the site’s physical folder was overwritten each deployment.
  • If the variables had been placed only in that web.config, they were indeed wiped on every publish.
  • Variables set at the site level via the aspNetCore/environmentVariables section of applicationHost.config survive every publish – but they are not visible in the published web.config.

Moving from ASP.NET Framework to ASP.NET Core on IIS

When a site graduates from classic ASP.NET (.aspx, web.config with <system.web>) to ASP.NET Core, the hosting model changes entirely:

  • The old <%@ Page %> / webforms pipeline disappears.
  • IIS no longer hosts the app inside the .NET CLR; instead the ASP.NET Core Module launches the dotnet host process.
  • Because the launch mechanism is different, the Application Pool .NET CLR Version must be switched from v4.0 (or v2.0) to No Managed Code, and the Managed Pipeline Mode should be Integrated.
  • All per-site configuration that previously lived in web.config under <system.web> now lives in the site-scoped aspNetCore/environmentVariables section of applicationHost.config.
  • If those variables are not moved, the app silently falls back to defaults (often SQLite) and produces the “500.30 – app failed to start” error you saw.

This context helps explain why the variables disappeared on publish and why a single script to manage them is handy for anyone making that migration.

One script to view, set, and diagnose

A PowerShell script (IIS-EnvVarManager.ps1) now brings the formerly fragmented steps into a single console:

Menu optionWhat it does
1. List IIS sitesEnumerates all IIS sites on the server.
2. Select siteChooses a site (by number or name) for subsequent actions.
3. Show env varsLists every aspNetCore/environmentVariables entry for the selected site.
4. Set / update an env varAdds or changes a variable (idempotent – removes any existing entry first).
5. Remove an env varDeletes a variable from the site configuration.
6. Show latest 10 .NET/IIS errorsPulls the most recent entries from the Windows Application event log for providers IIS AspNetCore Module V2, .NET Runtime, and Application Error.
9. ExitExits the console.

The script runs in two modes:

  • Demo mode – on a machine without full IIS it uses an in-memory sandbox so the menu can be tested locally.
  • Live mode – on a server with IIS it uses %SystemRoot%\system32\inetsrv\appcmd.exe to read and modify the real site configuration.

Key features baked into the script:

  • Single-quote guard – values or names containing a literal single quote are rejected (the appcmd limitation redirects to IIS Manager).
  • Admin prompt – set/remove operations warn if the console is not run as Administrator.
  • Pool-recycle reminder – after changing variables the ASP.NET Core Module only reads them at application start, so the associated app pool should be recycled.
  • Error-log filter – the latest 10 errors menu uses a provider-agnostic Get-WinEvent plus Where-Object pattern that works on any Windows machine, even when the specific .NET providers are not installed.

Quick-fix checklist for the 500

  1. Open an elevated PowerShell prompt on the IIS server.
  2. Run the script (pwsh -File .\IIS-EnvVarManager.ps1).
  3. Option 2 – select the affected site (for example CDISV2SCUATWU).
  4. Option 4 – set the two connection-string variables that the app needs: ConnectionStrings__DefaultConnection and MonitoringConnection.
  5. Option 6 – verify that the Application event log now shows real application errors instead of “SQLite Error 1: no such table: ActiveUserSessions”.
  6. Recycle the app pool (IIS Manager – App Pools – right-click the site’s pool – Recycle) so the ASP.NET Core Module re-reads the new variables.
  7. Browse the site – the 500 should no longer appear; the app starts with the real SQL Server database and all tables are available.

How things have changed

  • Old way – publish a web.config that contained <environmentVariables>; the values travelled with the deployment and just worked on any server that ran the same web.config.
  • Current way – the web.config is regenerated on every publish; site-scoped variables must be defined once in applicationHost.config and then they persist across publishes.
  • The PowerShell script above bridges the gap: it lets administrators view, modify, and troubleshoot those site-scoped variables without having to memorize appcmd syntax or hunt through the IIS Manager GUI.

Summary

  • The 500 on this ASP.NET Core site was caused by missing site-scoped environment variables.
  • Those variables live in applicationHost.config, not in the published web.config.
  • A single PowerShell script (IIS-EnvVarManager.ps1) now consolidates viewing, setting, and error-log inspection.
  • The script works both locally (demo mode) and on a real IIS server (live mode).
  • Setting the two connection-string variables ConnectionStrings__DefaultConnection and MonitoringConnection fixes the 500.
  • After setting the variables, recycle the associated app pool and re-test the site.

Full script (copy and paste)

Save the text below as IIS-EnvVarManager.ps1. Then run it from an elevated PowerShell prompt on the IIS server.

# IIS-EnvVarManager.ps1
# Manage IIS site-scoped environment variables for ASP.NET Core apps.
# Also shows the latest .NET/IIS Application event log errors.
#
# Usage:  powershell -ExecutionPolicy Bypass -File .\IIS-EnvVarManager.ps1
# Requires: Windows Server with IIS + ASP.NET Core Module. Run as Administrator
#           to SET or REMOVE variables. Listing/errors work read-only.
# Works in Windows PowerShell 5.1 and PowerShell 7.
#
# Site-scoped env vars live in applicationHost.config (aspNetCore/environmentVariables).
# Web Deploy publishes DO NOT touch them. Setting them here survives every publish.

[CmdletBinding()]
param()

$AppCmd = Join-Path $env:SystemRoot 'system32\inetsrv\appcmd.exe'
$script:CurrentSite = $null
$script:DemoMode = $false
$script:FakeSites = @()
$script:FakeVars = @{}

function Test-IsAdmin {
    $identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
    $principal = New-Object System.Security.Principal.WindowsPrincipal($identity)
    return $principal.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)
}

function Get-IisSites {
    if ($script:DemoMode) { return ,$script:FakeSites }
    $raw = & $AppCmd list sites 2>$null
    $sites = foreach ($line in $raw) {
        if ($line -match 'SITE\s+"([^"]+)"') { $Matches[1] }
    }
    return ,$sites
}

function Get-SiteEnvVars {
    param([string]$Site)
    if ($script:DemoMode) {
        $list = @($script:FakeVars[$Site] | Where-Object { $_ })
        return ,$list
    }
    $raw = & $AppCmd list config $Site /section:system.webServer/aspNetCore 2>$null
    $vars = @()
    foreach ($line in $raw) {
        if ($line -match '<environmentVariable\s+name="([^"]*)"\s+value="([^"]*)"\s*/>') {
            $vars += [pscustomobject]@{ Name = $Matches[1]; Value = $Matches[2] }
        }
    }
    return ,$vars
}

function Show-SiteEnvVars {
    param([string]$Site)
    $vars = Get-SiteEnvVars -Site $Site
    if ($vars.Count -eq 0) {
        Write-Host "  No site-scoped env vars set for '$Site'." -ForegroundColor Yellow
        Write-Host "  Note: appsettings.json / web.config values still apply. Also check the site's web.config for a stale environmentVariables block." -ForegroundColor DarkYellow
        return
    }
    Write-Host "  Environment variables for '$Site':" -ForegroundColor Cyan
    $width = ($vars | ForEach-Object { $_.Name.Length } | Measure-Object -Maximum).Maximum
    foreach ($v in $vars) {
        Write-Host ("  {0,-$width} = {1}" -f $v.Name, $v.Value)
    }
}

function Set-SiteEnvVar {
    param([string]$Site, [string]$Name, [string]$Value)
    if ($Name -match "'") { Write-Host "ERROR: env var name contains a single quote - not supported by appcmd." -ForegroundColor Red; return }
    if ($Value -match "'") { Write-Host "ERROR: env var value contains a single quote - not supported by appcmd. Use IIS Manager instead." -ForegroundColor Red; return }

    if ($script:DemoMode) {
        $list = $script:FakeVars[$Site]
        $list = @($list | Where-Object { $_.Name -ne $Name })
        $list += [pscustomobject]@{ Name = $Name; Value = $Value }
        $script:FakeVars[$Site] = $list
        Write-Host "  OK (demo): $Name = $Value" -ForegroundColor Green
        return
    }

    # Idempotent: remove any existing entry first, then add.
    $removeArg = "/-environmentVariables.[name='$Name']"
    & $AppCmd set config $Site /section:system.webServer/aspNetCore $removeArg /commit:site 2>$null | Out-Null

    $addArg = "/+environmentVariables.[name='$Name',value='$Value']"
    & $AppCmd set config $Site /section:system.webServer/aspNetCore $addArg /commit:site
    if ($LASTEXITCODE -ne 0) {
        Write-Host "ERROR: appcmd failed to set $Name. Check the value and try again." -ForegroundColor Red
    } else {
        Write-Host "  OK: $Name = $Value" -ForegroundColor Green
        Write-Host "  Recycle the app pool for the change to take effect (the ASP.NET Core Module reads vars only at app start)." -ForegroundColor Yellow
    }
}

function Remove-SiteEnvVar {
    param([string]$Site, [string]$Name)
    if ($Name -match "'") { Write-Host "ERROR: env var name contains a single quote." -ForegroundColor Red; return }
    if ($script:DemoMode) {
        $script:FakeVars[$Site] = @($script:FakeVars[$Site] | Where-Object { $_.Name -ne $Name })
        Write-Host "  OK (demo): removed $Name" -ForegroundColor Green
        return
    }
    $removeArg = "/-environmentVariables.[name='$Name']"
    & $AppCmd set config $Site /section:system.webServer/aspNetCore $removeArg /commit:site
    if ($LASTEXITCODE -eq 0) {
        Write-Host "  OK: removed $Name" -ForegroundColor Green
    } else {
        Write-Host "  $Name was not set (nothing to remove)." -ForegroundColor Yellow
    }
}

function Show-LatestErrors {
    param([int]$Count = 10)
    if ($script:DemoMode) {
        Write-Host "  Latest demo .NET/IIS errors (canned):" -ForegroundColor Cyan
        $fake = @(
            [pscustomobject]@{ Time = (Get-Date).AddMinutes(-3); Provider = 'IIS AspNetCore Module V2'; Id = 1000; Level = 'Error'; Message = 'Application ''/LM/W3SVC/17/ROOT'' with physical root ''C:\inetpub\wwwroot\CDISV2SCUATWU'' failed to load clr and managed application. CLR worker thread exited prematurely.' },
            [pscustomobject]@{ Time = (Get-Date).AddMinutes(-7); Provider = '.NET Runtime'; Id = 1026; Level = 'Error'; Message = 'Application: CDISV2Core.exe. Exception Info: System.Data.SqlClient.SqlException: Cannot open database "CDISSCUAT" requested by the login. The login failed for user NT AUTHORITY\NETWORK SERVICE.' },
            [pscustomobject]@{ Time = (Get-Date).AddMinutes(-15); Provider = 'Application Error'; Id = 1000; Level = 'Error'; Message = 'Faulting application name: CDISV2Core.exe. Faulting module name: KERNELBASE.dll. Exception code: 0xe0434352. Faulting process id: 0x1234.' }
        )
        $i = 0
        foreach ($e in $fake) {
            $i++
            Write-Host ""
            Write-Host "  [$i] $($e.Time.ToString('yyyy-MM-dd HH:mm:ss'))  $($e.Provider)  #$($e.Id)  Level=$($e.Level)" -ForegroundColor White
            Write-Host "      $($e.Message)"
        }
        return
    }
    $providers = @('IIS AspNetCore Module V2', '.NET Runtime', 'Application Error')
    $events = Get-WinEvent -LogName Application -MaxEvents 500 -ErrorAction SilentlyContinue |
        Where-Object { $providers -contains $_.ProviderName } |
        Sort-Object TimeCreated -Descending | Select-Object -First $Count

    if (-not $events) {
        Write-Host "  No matching .NET/IIS Application event log entries found." -ForegroundColor Yellow
        return
    }

    Write-Host "  Latest $($events.Count) .NET/IIS errors from Application event log:" -ForegroundColor Cyan
    $i = 0
    foreach ($e in $events) {
        $i++
        Write-Host ""
        Write-Host "  [$i] $($e.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss'))  $($e.ProviderName)  #$($e.Id)  Level=$($e.LevelDisplayName)" -ForegroundColor White
        $msg = ($e.Message -split "`r?`n" | Where-Object { $_.Trim() -ne '' }) -join ' '
        if ($msg.Length -gt 300) { $msg = $msg.Substring(0, 300) + '...' }
        Write-Host "      $msg"
    }
}

function Show-Menu {
    Clear-Host
    Write-Host "======================================================" -ForegroundColor Cyan
    Write-Host "  IIS ASP.NET Core Env Var Manager" -ForegroundColor Cyan
    Write-Host "======================================================" -ForegroundColor Cyan
    if ($script:DemoMode) { Write-Host "  DEMO MODE - in-memory only, no IIS" -ForegroundColor Magenta }
    if ($script:CurrentSite) {
        Write-Host "  Selected site: $($script:CurrentSite)" -ForegroundColor Green
    } else {
        Write-Host "  Selected site: (none - choose one first)" -ForegroundColor Yellow
    }
    Write-Host ""
    Write-Host "  1. List IIS sites" -ForegroundColor White
    Write-Host "  2. Select site" -ForegroundColor White
    Write-Host "  3. Show env vars for selected site" -ForegroundColor White
    Write-Host "  4. Set / update an env var" -ForegroundColor White
    Write-Host "  5. Remove an env var" -ForegroundColor White
    Write-Host "  6. Show latest 10 .NET/IIS errors" -ForegroundColor White
    Write-Host "  9. Exit" -ForegroundColor White
    Write-Host ""
}

# Check appcmd exists
if (-not (Test-Path -LiteralPath $AppCmd)) {
    Write-Host "WARNING: Full IIS appcmd.exe not found at $AppCmd." -ForegroundColor Yellow
    Write-Host "         Running in DEMO mode - all changes are in-memory only (no IIS on this machine)." -ForegroundColor Yellow
    Write-Host ""
    $script:DemoMode = $true
    $script:FakeSites = @('CDISV2SCUATWU', 'CDISV2SCPREPRODCOREWP', 'CDISV2MSUATWU')
    $scuat = @(
        [pscustomobject]@{ Name = 'ASPNETCORE_ENVIRONMENT'; Value = 'Test' },
        [pscustomobject]@{ Name = 'CDIS_PROFILE'; Value = 'SUNSHINE-COAST' },
        [pscustomobject]@{ Name = 'SiteName'; Value = 'Sunshine Coast' },
        [pscustomobject]@{ Name = 'PHUCode'; Value = 'SC' },
        [pscustomobject]@{ Name = 'ClientCode'; Value = 'sc' },
        [pscustomobject]@{ Name = 'FileUpload__StoragePath'; Value = 'E:/Images/SunshineCoast' },
        [pscustomobject]@{ Name = 'FileUpload__PHUPrefix'; Value = 'CDISSC' },
        [pscustomobject]@{ Name = 'ConnectionStrings__DefaultConnection'; Value = 'data source=CDISSunCoast-sch-uat.db.sth.health.qld.gov.au,21433;initial catalog=CDISSCUAT;Integrated Security=True;MultipleActiveResultSets=true;TrustServerCertificate=True' }
    )
    $scpre = @(
        [pscustomobject]@{ Name = 'ASPNETCORE_ENVIRONMENT'; Value = 'Staging' },
        [pscustomobject]@{ Name = 'SiteName'; Value = 'Sunshine Coast PreProd' },
        [pscustomobject]@{ Name = 'PHUCode'; Value = 'SC' }
    )
    $msuat = @(
        [pscustomobject]@{ Name = 'ASPNETCORE_ENVIRONMENT'; Value = 'Test' },
        [pscustomobject]@{ Name = 'PHUCode'; Value = 'MS' },
        [pscustomobject]@{ Name = 'ClientCode'; Value = 'ms' }
    )
    $script:FakeVars['CDISV2SCUATWU'] = $scuat
    $script:FakeVars['CDISV2SCPREPRODCOREWP'] = $scpre
    $script:FakeVars['CDISV2MSUATWU'] = $msuat
}

$isAdmin = Test-IsAdmin
if (-not $isAdmin) {
    Write-Host "WARNING: Not running as Administrator. Viewing works; SET/REMOVE will fail." -ForegroundColor Yellow
    Write-Host "         Re-run from an elevated prompt to change variables." -ForegroundColor Yellow
    Write-Host ""
}

while ($true) {
    Show-Menu
    $choice = Read-Host "Select option"
    switch ($choice) {
        '1' {
            $sites = Get-IisSites
            if (-not $sites -or $sites.Count -eq 0) {
                Write-Host "  No IIS sites found." -ForegroundColor Yellow
            } else {
                Write-Host "  IIS sites:" -ForegroundColor Cyan
                foreach ($s in $sites) { Write-Host "    $s" }
            }
            Read-Host "  Press Enter to continue"
        }
        '2' {
            $sites = Get-IisSites
            Write-Host "  Available sites:" -ForegroundColor Cyan
            for ($i = 0; $i -lt $sites.Count; $i++) {
                Write-Host ("    {0,2}. {1}" -f ($i + 1), $sites[$i])
            }
            $idx = Read-Host "  Enter number (or site name)"
            if ($idx -match '^\d+$') {
                $n = [int]$idx - 1
                if ($n -ge 0 -and $n -lt $sites.Count) { $script:CurrentSite = $sites[$n] }
                else { Write-Host "  Invalid number." -ForegroundColor Red }
            } else {
                $script:CurrentSite = $idx.Trim()
            }
            if ($script:CurrentSite) { Write-Host "  Selected: $($script:CurrentSite)" -ForegroundColor Green }
        }
        '3' {
            if (-not $script:CurrentSite) { Write-Host "  Select a site first (option 2)." -ForegroundColor Yellow }
            else { Show-SiteEnvVars -Site $script:CurrentSite }
            Read-Host "  Press Enter to continue"
        }
        '4' {
            if (-not $script:CurrentSite) { Write-Host "  Select a site first (option 2)." -ForegroundColor Yellow; Read-Host "  Press Enter to continue"; break }
            if (-not $isAdmin -and -not $script:DemoMode) { Write-Host "  SET requires Administrator." -ForegroundColor Red; Read-Host "  Press Enter to continue"; break }
            Show-SiteEnvVars -Site $script:CurrentSite
            $name = Read-Host "  Env var name (e.g. ASPNETCORE_ENVIRONMENT, ConnectionStrings__DefaultConnection)"
            if ([string]::IsNullOrWhiteSpace($name)) { break }
            $value = Read-Host "  Value"
            Set-SiteEnvVar -Site $script:CurrentSite -Name $name.Trim() -Value $value
            Read-Host "  Press Enter to continue"
        }
        '5' {
            if (-not $script:CurrentSite) { Write-Host "  Select a site first (option 2)." -ForegroundColor Yellow; Read-Host "  Press Enter to continue"; break }
            if (-not $isAdmin -and -not $script:DemoMode) { Write-Host "  REMOVE requires Administrator." -ForegroundColor Red; Read-Host "  Press Enter to continue"; break }
            Show-SiteEnvVars -Site $script:CurrentSite
            $name = Read-Host "  Env var name to remove"
            if ([string]::IsNullOrWhiteSpace($name)) { break }
            Remove-SiteEnvVar -Site $script:CurrentSite -Name $name.Trim()
            Read-Host "  Press Enter to continue"
        }
        '6' {
            Show-LatestErrors -Count 10
            Read-Host "  Press Enter to continue"
        }
        '9' { exit 0 }
        default { Write-Host "  Invalid option." -ForegroundColor Red }
    }
}

Where to get the script

  • On a server with full IIS the script automatically switches to live mode and uses %SystemRoot%\system32\inetsrv\appcmd.exe.
  • On a machine without IIS it runs in demo mode with in-memory sample data.

Posted

in

by

Tags:

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *