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), theweb.configfile 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/environmentVariablessection ofapplicationHost.configsurvive every publish – but they are not visible in the publishedweb.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
dotnethost process. - Because the launch mechanism is different, the Application Pool
.NET CLR Versionmust be switched fromv4.0(orv2.0) toNo Managed Code, and the Managed Pipeline Mode should beIntegrated. - All per-site configuration that previously lived in
web.configunder<system.web>now lives in the site-scopedaspNetCore/environmentVariablessection ofapplicationHost.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 option | What it does |
|---|---|
| 1. List IIS sites | Enumerates all IIS sites on the server. |
| 2. Select site | Chooses a site (by number or name) for subsequent actions. |
| 3. Show env vars | Lists every aspNetCore/environmentVariables entry for the selected site. |
| 4. Set / update an env var | Adds or changes a variable (idempotent – removes any existing entry first). |
| 5. Remove an env var | Deletes a variable from the site configuration. |
| 6. Show latest 10 .NET/IIS errors | Pulls the most recent entries from the Windows Application event log for providers IIS AspNetCore Module V2, .NET Runtime, and Application Error. |
| 9. Exit | Exits 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.exeto 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-WinEventplusWhere-Objectpattern that works on any Windows machine, even when the specific .NET providers are not installed.
Quick-fix checklist for the 500
- Open an elevated PowerShell prompt on the IIS server.
- Run the script (
pwsh -File .\IIS-EnvVarManager.ps1). - Option 2 – select the affected site (for example
CDISV2SCUATWU). - Option 4 – set the two connection-string variables that the app needs:
ConnectionStrings__DefaultConnectionandMonitoringConnection. - Option 6 – verify that the Application event log now shows real application errors instead of “SQLite Error 1: no such table: ActiveUserSessions”.
- 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.
- 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.configthat contained<environmentVariables>; the values travelled with the deployment and just worked on any server that ran the sameweb.config. - Current way – the
web.configis regenerated on every publish; site-scoped variables must be defined once inapplicationHost.configand 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
appcmdsyntax 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 publishedweb.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__DefaultConnectionandMonitoringConnectionfixes 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.
Leave a Reply