Sessions with Redis

Use StackExchange.Redis as the distributed cache backing for ASP.NET Core session state.

Full source

File: pwsh/tutorial/examples/19.2-Sessions-Redis.ps1

<#
    Sample Kestrun Server Configuration – Sessions Demo
    This script shows how to enable Session middleware and use it in routes.
    FileName: 19.2-Sessions-Redis.ps1
#>

param(
    [int]$Port = 5000,
    [IPAddress]$IPAddress = [IPAddress]::Loopback,
    [string]$UpstashUrl = $env:UPSTASH_REDIS_URL
)
# Configure default logging
New-KrLogger |
    Set-KrLoggerLevel -Value Debug |
    Add-KrSinkConsole |
    Register-KrLogger -Name 'myLogger' -SetAsDefault

# Create a new Kestrun server
New-KrServer -Name 'Sessions Demo Server'


# Listener
Add-KrEndpoint -Port ($Port) -IPAddress $IPAddress -SelfSignedCert

# --- Session setup -----------------------------------------------------------
# Cookie is optional, but nice to be explicit.
$cookie = New-KrCookieBuilder `
    -Name 'Kr.Session' `
    -HttpOnly `
    -SameSite Lax `
    -SecurePolicy Always  # set to None if you want to test over plain http

if ([string]::IsNullOrWhiteSpace($UpstashUrl)) {
    Write-Error "Redis URL not provided. Set the UPSTASH_REDIS_URL environment variable (recommended) or pass -UpstashUrl."
    return
}
# Parse the URL to extract host/port/password safely
$uri = [System.Uri]::new($UpstashUrl)

# uri.UserInfo is 'default:YOUR_TOKEN_HERE' -> split to get password
$userInfo = $uri.UserInfo.Split(':')
$lpwd = if ($userInfo.Length -ge 2) { $userInfo[1] } else { $null }

# Build a StackExchange.Redis ConfigurationOptions
$cfg = [StackExchange.Redis.ConfigurationOptions]::new()
$cfg.EndPoints.Add("$($uri.Host):$($uri.Port)")
$cfg.Ssl = $true                         # Upstash uses TLS
$cfg.Password = $lpwd
$cfg.AbortOnConnectFail = $false         # important for DI/startup resilience
$cfg.ConnectTimeout = 5000               # optional tuning
$cfg.SyncTimeout = 5000                  # optional tuning
# $cfg.DefaultDatabase = 0               # optional if you want a different DB

# (Optional) set a client name for observability
$cfg.ClientName = "kestrun-$(hostname)"

# Inspect (for debug only) - don't print secrets in prod logs
# $cfg.ToString()


# Redis configuration (if using Redis)
Add-KrDistributedRedisCache -ConfigurationOptions $cfg -InstanceName 'KestrunSession_'

# Add session services + middleware. By default, Add-KrSession will ensure a distributed memory cache exists.
Add-KrSession -Cookie $cookie -IdleTimeout 20 -IOTimeout 10

# Enable configuration
Enable-KrConfiguration

# Tip:
# If you're wiring Redis/SQL yourself, use: Add-KrSession -NoDistributedMemoryCache
# and add your cache provider first.

# --- Routes -----------------------------------------------------------------

# 1) Simple counter: increments an integer stored in session.
Add-KrMapRoute -Verbs Get -Path '/session/counter' -ScriptBlock {
    # Get current value (or 0 if not set)
    $current = Get-KrSessionInt32 -Key 'counter'
    # Increment and store back
    Set-KrSessionInt32 -Key 'counter' -Value ($current + 1)

    Write-KrJsonResponse -StatusCode 200 -InputObject @{
        message = 'Counter incremented'
        counter = $current + 1
    }
}

# 2) Login: set a 'user' value in session (?user=max)
Add-KrMapRoute -Verbs Get -Path '/session/login' -ScriptBlock {
    # Get user from query
    $user = Get-KrRequestQuery -Name 'user'
    # Validate
    if ([string]::IsNullOrWhiteSpace($user)) {
        Write-KrJsonResponse -StatusCode 400 -InputObject @{
            error = "Missing 'user' query parameter"
            example = '/session/login?user=max'
        }
        return
    }
    # Store in session
    Set-KrSessionString -Key 'user' -Value $user
    # respond to login event
    Write-KrJsonResponse -StatusCode 200 -InputObject @{
        message = 'Logged in'
        user = $user
    }
}

# 3) WhoAmI: read 'user' from session
Add-KrMapRoute -Verbs Get -Path '/session/whoami' -ScriptBlock {
    # Get user from session
    $user = Get-KrSessionString -Key 'user'
    # If not set, return 401
    if ([string]::IsNullOrEmpty($user)) {
        Write-KrJsonResponse -StatusCode 401 -InputObject @{
            error = 'No active session or user not set'
        }
        return
    }
    # respond with user info
    Write-KrJsonResponse -StatusCode 200 -InputObject @{
        user = $user
    }
}

# 4) Logout: clear the session
Add-KrMapRoute -Verbs Get -Path '/session/logout' -ScriptBlock {
    # Clear the session
    Clear-KrSession
    # respond to logout event
    Write-KrJsonResponse -StatusCode 200 -InputObject @{
        message = 'Session cleared'
    }
}

# 5) Generic set: /session/set?key=foo&value=bar
Add-KrMapRoute -Verbs Get -Path '/session/set' -ScriptBlock {
    # Get key and value from query
    $key = Get-KrRequestQuery -Name 'key'
    $value = Get-KrRequestQuery -Name 'value'

    # Validate
    if ([string]::IsNullOrWhiteSpace($key) -or [string]::IsNullOrWhiteSpace($value)) {
        Write-KrJsonResponse -StatusCode 400 -InputObject @{
            error = "Missing 'key' and/or 'value' query parameter(s)"
            example = '/session/set?key=color&value=purple'
        }
        return
    }
    # Store in session
    Set-KrSessionString -Key $key -Value $value
    # respond to set event
    Write-KrJsonResponse -StatusCode 200 -InputObject @{
        message = 'Stored in session'
        key = $key
        value = $value
    }
}

# 6) Generic get: /session/get?key=foo
Add-KrMapRoute -Verbs Get -Path '/session/get' -ScriptBlock {
    # Get key from query
    $key = Get-KrRequestQuery -Name 'key'
    # Validate
    if ([string]::IsNullOrWhiteSpace($key)) {
        Write-KrJsonResponse -StatusCode 400 -InputObject @{
            error = "Missing 'key' query parameter"
            example = '/session/get?key=color'
        }
        return
    }
    # Get from session
    $val = Get-KrSessionString -Key $key
    # If not found, return 404
    if ($null -eq $val) {
        Write-KrJsonResponse -StatusCode 404 -InputObject @{
            error = "Key '$key' not found in session"
        }
        return
    }
    # respond with value
    Write-KrJsonResponse -StatusCode 200 -InputObject @{
        key = $key
        value = $val
    }
}

# Convenience: hello (non-session)
Add-KrMapRoute -Verbs Get -Path '/hello' -ScriptBlock {
    Write-KrTextResponse -StatusCode 200 -InputObject 'Hello, Session World!'
}

# Start the server asynchronously
Start-KrServer -CloseLogsOnExit

Step-by-step

  1. Logging: Register a console logger and set level to Debug.
  2. Server: Create a Kestrun server named “Sessions Demo Server”.
  3. Listener: Listen on loopback using a self-signed certificate on the provided port (HTTPS).
  4. Cookie: Build a secure session cookie (Kr.Session) with HttpOnly, SameSite=Lax, and SecurePolicy=Always.
  5. Redis cache: Configure Redis using Add-KrDistributedRedisCache -Configuration 'localhost:6379' -InstanceName 'KestrunSession_'.
  6. Session: Enable session services and middleware with idle (20s) and I/O (10s) timeouts.
  7. Configure: Call Enable-KrConfiguration to build the app.
  8. Routes: Map endpoints for counter, login, whoami, logout, set, and get using session cmdlets.
  9. Start: Run the server asynchronously with Start-KrServer.

Try it

Because this sample uses a self-signed certificate and a Secure cookie, use HTTPS and skip certificate verification during local testing.

$base = 'https://127.0.0.1:5000'

# Counter increments with the same cookie jar
curl -k -s -c jar.txt -b jar.txt "$base/session/counter" | jq
curl -k -s -c jar.txt -b jar.txt "$base/session/counter" | jq

# WhoAmI requires login
curl -k -s -c jar.txt -b jar.txt -i "$base/session/whoami"

# Login and read identity
curl -k -s -c jar.txt -b jar.txt "$base/session/login?user=max" | jq
curl -k -s -c jar.txt -b jar.txt "$base/session/whoami" | jq

# Generic set/get within the same session
curl -k -s -c jar.txt -b jar.txt "$base/session/set?key=color&value=purple" | jq
curl -k -s -c jar.txt -b jar.txt "$base/session/get?key=color" | jq

# Logout clears the session
curl -k -s -c jar.txt -b jar.txt "$base/session/logout" | jq
curl -k -s -c jar.txt -b jar.txt -i "$base/session/whoami"

PowerShell equivalents (Invoke-WebRequest):

$base = 'https://127.0.0.1:5000'
$sess = New-Object Microsoft.PowerShell.Commands.WebRequestSession

# Counter increments with the same WebRequestSession
((Invoke-WebRequest -Uri "$base/session/counter" -WebSession $sess -SkipCertificateCheck).Content | ConvertFrom-Json).counter
((Invoke-WebRequest -Uri "$base/session/counter" -WebSession $sess -SkipCertificateCheck).Content | ConvertFrom-Json).counter

# WhoAmI requires login (expected 401 before login)
try {
    Invoke-WebRequest -Uri "$base/session/whoami" -WebSession $sess -SkipCertificateCheck -ErrorAction Stop | Out-Null
} catch {
    'status: 401 (unauthorized)'
}

# Login and read identity
Invoke-WebRequest -Uri "$base/session/login?user=max" -WebSession $sess -SkipCertificateCheck | Out-Null
(Invoke-WebRequest -Uri "$base/session/whoami" -WebSession $sess -SkipCertificateCheck).Content | ConvertFrom-Json

# Generic set/get within the same session
Invoke-WebRequest -Uri "$base/session/set?key=color&value=purple" -WebSession $sess -SkipCertificateCheck | Out-Null
(Invoke-WebRequest -Uri "$base/session/get?key=color" -WebSession $sess -SkipCertificateCheck).Content | ConvertFrom-Json

# Logout clears the session
Invoke-WebRequest -Uri "$base/session/logout" -WebSession $sess -SkipCertificateCheck | Out-Null
try {
    Invoke-WebRequest -Uri "$base/session/whoami" -WebSession $sess -SkipCertificateCheck -ErrorAction Stop | Out-Null
} catch {
    'status: 401 (unauthorized)'
}

Tip: Keep Redis running locally (e.g., Docker) and ensure connectivity with your configuration string.

Key Points

  • Use Redis for scale-out session storage across multiple instances.
  • The sample demonstrates Add-KrDistributedRedisCache plus Add-KrSession.
  • Cookie settings matter in production: Secure, HttpOnly, SameSite.
  • Session must be enabled before routes that depend on it.

Troubleshooting

  • Connection refused: Verify Redis is running and your connection string is correct.
  • Session not persisting: Confirm the cookie is preserved and the server process can reach Redis.
  • Timeouts: Tune Redis and session timeouts based on workload.

References


Previous / Next

Previous: Sessions Next: Sessions with SQL Server