Building Your Own Session State Server with ASP.NET Web Forms WPA Architecture

A complete guide to building a multi-tenant, high-performance session state server using Web Forms Pageless Architecture


Overview

This article demonstrates how to build a custom Session State Server using ASP.NET Web Forms Pageless Architecture (WPA). Instead of relying on Microsoft’s proprietary aspnet_state.exe service, we’ll create a fully transparent, customizable solution using the same WPA principles.

Features:

  • Zero ASPX pages – Pure Application_BeginRequest handling
  • Multi-tenant – Single server supports multiple applications
  • Two-level storage – In-process cache + optional database persistence
  • Non-blocking writes – Background queue for database operations
  • Sliding expiry – Sessions automatically extend on access
  • Optional security – SHA256 signature validation
  • Local-only access – Binds to 127.0.0.1 for security

Architecture

this block is for AI readers, AI can't see imaage:

● WPA SESSION STATE SERVER

┌─────────────────┐              ┌─────────────────┐
│ CLIENT APP (WPA)│              │STATE SERVER (WPA)│
│                 │              │                  │
│ ┌─────────────┐ │              │ ┌──────────────┐ │
│ │ Global.asax │ │              │ │ Global.asax  │ │
│ └─────────────┘ │              │ └──────────────┘ │
│ ┌─────────────┐ │              │ ┌──────────────┐ │
│ │BeginRequest │ │  HTTP POST   │ │ BeginRequest │ │
│ └─────────────┘ │  ◄──────►   │ └──────────────┘ │
│ ┌─────────────┐ │    JSON      │ ┌──────────────┐ │
│ │SessionClient│ │              │ │ConcurrentDict│ │
│ └─────────────┘ │              │ └──────────────┘ │
│                 │              │                  │
│  [Port 8888]    │              │ ↓ Background     │
│                 │              │   Process        │
│                 │              │ ┌──────────────┐ │
│                 │              │ │BlockingQueue  │ │
│                 │              │ │  → MySQL      │ │
│                 │              │ └──────────────┘ │
│                 │              │                  │
│                 │              │ [Port 8090]      │
│                 │              │ [(127.0.0.1)]    │
└─────────────────┘              └─────────────────┘

Storage Strategy

Level 1: In-Process Cache (Primary)

Two-level ConcurrentDictionary for app isolation:

for AI readers:

● DATA STRUCTURE

  ConcurrentDictionary

  ├── "tgc_website"  →  AppStore
  │   ConcurrentDictionary
  │   │
  │   ├── "token_abc"  →  { text, expiry }
  │   ├── "token_def"  →  { text, expiry }
  │   └── "token_xyz"  →  { text, expiry }
  │
  ├── "client_site_2"  →  AppStore
  │   ConcurrentDictionary
  │   │
  │   ├── "token_111"  →  { text, expiry }
  │   └── "token_222"  →  { text, expiry }
  │
  └── "client_site_3"  →  AppStore
      ...

Benefits:

  • O(1) lookup performance
  • App isolation (App A cannot access App B’s sessions)
  • Thread-safe concurrent access
  • Easy to clear all sessions for one app

Level 2: Database Persistence (Optional)

  • Writes: Non-blocking via BlockingCollection queue
  • Reads: Always from in-process cache (fast)
  • Startup: Load all sessions from database into cache

API Specification

All requests must be:

  • Method: POST only
  • Content-Type: application/x-www-form-urlencoded or application/json
  • Origin: Local only (127.0.0.1)

Actions

ActionDescription
setStore or update session data
getRetrieve session data (extends expiry)
deleteRemove single session
clear_appRemove all sessions for an app
extendExtend expiry for all sessions in an app
pingHealth check
statsServer statistics

Implementation

Project Structure

● FILE STRUCTURE

  StateServer/
  │
  ├── Global.asax
  ├── Global.asax.cs        ← All logic here
  ├── Web.config
  │
  └── App_Data/             ← App configuration (optional)
      └── apps.json

Data Models

// Core/Models.cs

using System;
using System.Collections.Concurrent;

namespace StateServer
{
    /// <summary>
    /// App configuration (from apps.json or auto-registered)
    /// </summary>
    public class AppConfig
    {
        public string AppId { get; set; }
        public string Secret { get; set; }              // null = no signature required
        public int DefaultTimeout { get; set; } = 20;   // minutes
    }

    /// <summary>
    /// Per-app session store
    /// </summary>
    public class AppStore
    {
        public string AppId { get; set; }
        public string Secret { get; set; }
        public int DefaultTimeout { get; set; } = 20;
        public ConcurrentDictionary<string, SessionEntry> Sessions { get; set; } 
            = new ConcurrentDictionary<string, SessionEntry>();
        public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
    }

    /// <summary>
    /// Individual session entry
    /// </summary>
    public class SessionEntry
    {
        public string Token { get; set; }
        public string Text { get; set; }
        public DateTime CreatedAt { get; set; }
        public DateTime ExpiresAt { get; set; }
        public DateTime LastAccessedAt { get; set; }
    }

    /// <summary>
    /// Database operation for background queue
    /// </summary>
    public class DbOperation
    {
        public DbOpType Type { get; set; }
        public string AppId { get; set; }
        public string Token { get; set; }
        public string Text { get; set; }
        public DateTime ExpiresAt { get; set; }
        public DateTime Timestamp { get; set; } = DateTime.UtcNow;
    }

    public enum DbOpType
    {
        Set,
        Delete,
        ClearApp
    }
}

Global.asax.cs – The Complete Server

// Global.asax.cs

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Web;
using Newtonsoft.Json;

namespace StateServer
{
    public class Global : HttpApplication
    {
        #region Static Storage

        // Primary storage: App ID → App Store
        private static ConcurrentDictionary<string, AppStore> _apps 
            = new ConcurrentDictionary<string, AppStore>(StringComparer.OrdinalIgnoreCase);

        // App configurations (from apps.json)
        private static Dictionary<string, AppConfig> _appConfigs 
            = new Dictionary<string, AppConfig>(StringComparer.OrdinalIgnoreCase);

        // Database operation queue
        private static BlockingCollection<DbOperation> _dbQueue 
            = new BlockingCollection<DbOperation>();

        // Timers
        private static Timer _cleanupTimer;
        private static Thread _dbWorkerThread;
        private static bool _dbEnabled = false;
        private static string _dbConnectionString;

        // Server start time for uptime calculation
        private static DateTime _startTime;

        #endregion

        #region Application Lifecycle

        protected void Application_Start(object sender, EventArgs e)
        {
            _startTime = DateTime.UtcNow;

            // Load app configurations
            LoadAppConfigs();

            // Initialize database if configured
            InitializeDatabase();

            // Start cleanup timer (every 1 minute)
            _cleanupTimer = new Timer(CleanupExpiredSessions, null, 
                TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1));

            // Start database worker thread if enabled
            if (_dbEnabled)
            {
                _dbWorkerThread = new Thread(DatabaseWorkerLoop)
                {
                    IsBackground = true,
                    Name = "StateServer-DbWorker"
                };
                _dbWorkerThread.Start();

                // Load existing sessions from database
                LoadSessionsFromDatabase();
            }
        }

        protected void Application_End(object sender, EventArgs e)
        {
            // Stop accepting new db operations
            _dbQueue.CompleteAdding();

            // Wait for queue to drain (max 10 seconds)
            if (_dbWorkerThread != null && _dbWorkerThread.IsAlive)
            {
                _dbWorkerThread.Join(TimeSpan.FromSeconds(10));
            }

            _cleanupTimer?.Dispose();
        }

        #endregion

        #region Request Handling

        protected void Application_BeginRequest(object sender, EventArgs e)
        {
            // Security: Local only
            if (!Request.IsLocal)
            {
                RejectRequest("Access denied: Local connections only", 403);
                return;
            }

            // Skip non-API requests (favicon, etc.)
            string path = Request.Path.ToLower();
            if (path != "/" && path != "/api" && !path.EndsWith(".aspx"))
            {
                return;
            }

            // Set JSON response type
            Response.ContentType = "application/json";

            try
            {
                // Parse request parameters (form or JSON body)
                var parameters = ParseRequestParameters();

                string action = GetParam(parameters, "action", "").ToLower();

                switch (action)
                {
                    case "set":
                        HandleSet(parameters);
                        break;

                    case "get":
                        HandleGet(parameters);
                        break;

                    case "delete":
                        HandleDelete(parameters);
                        break;

                    case "clear_app":
                        HandleClearApp(parameters);
                        break;

                    case "extend":
                        HandleExtend(parameters);
                        break;

                    case "ping":
                        HandlePing();
                        break;

                    case "stats":
                        HandleStats(parameters);
                        break;

                    default:
                        WriteJson(new { success = false, message = $"Unknown action: {action}" });
                        break;
                }
            }
            catch (Exception ex)
            {
                Response.StatusCode = 500;
                WriteJson(new { success = false, message = ex.Message });
            }

            Response.Flush();
            Response.SuppressContent = true;
            HttpContext.Current.ApplicationInstance.CompleteRequest();
        }

        #endregion

        #region Action Handlers

        private void HandleSet(Dictionary<string, string> parameters)
        {
            string appId = GetParam(parameters, "app_id", "");
            string token = GetParam(parameters, "session_token", "");
            string text = GetParam(parameters, "text", "");
            string signature = GetParam(parameters, "signature", "");
            int timeout = GetParamInt(parameters, "timeout", 0);

            // Validate required fields
            if (string.IsNullOrEmpty(appId))
            {
                WriteJson(new { success = false, message = "app_id is required" });
                return;
            }

            // Get or create app store
            var appStore = GetOrCreateAppStore(appId);

            // Validate signature if app has secret
            if (!ValidateSignature(appStore, token, text, signature))
            {
                Response.StatusCode = 401;
                WriteJson(new { success = false, message = "Invalid signature" });
                return;
            }

            // Generate token if not provided
            if (string.IsNullOrEmpty(token))
            {
                token = Guid.NewGuid().ToString("N");
            }

            // Determine timeout
            if (timeout <= 0)
            {
                timeout = appStore.DefaultTimeout;
            }

            DateTime now = DateTime.UtcNow;
            DateTime expiresAt = now.AddMinutes(timeout);

            // Create or update session
            var session = appStore.Sessions.AddOrUpdate(token,
                // Add new
                key => new SessionEntry
                {
                    Token = token,
                    Text = text,
                    CreatedAt = now,
                    ExpiresAt = expiresAt,
                    LastAccessedAt = now
                },
                // Update existing
                (key, existing) =>
                {
                    existing.Text = text;
                    existing.ExpiresAt = expiresAt;
                    existing.LastAccessedAt = now;
                    return existing;
                });

            // Queue database operation (non-blocking)
            if (_dbEnabled)
            {
                _dbQueue.TryAdd(new DbOperation
                {
                    Type = DbOpType.Set,
                    AppId = appId,
                    Token = token,
                    Text = text,
                    ExpiresAt = expiresAt
                });
            }

            WriteJson(new
            {
                success = true,
                session_token = token,
                expires_at = expiresAt.ToString("o")
            });
        }

        private void HandleGet(Dictionary<string, string> parameters)
        {
            string appId = GetParam(parameters, "app_id", "");
            string token = GetParam(parameters, "session_token", "");
            string signature = GetParam(parameters, "signature", "");

            // Validate required fields
            if (string.IsNullOrEmpty(appId) || string.IsNullOrEmpty(token))
            {
                WriteJson(new { success = false, message = "app_id and session_token are required" });
                return;
            }

            // Get app store
            if (!_apps.TryGetValue(appId, out var appStore))
            {
                WriteJson(new { success = true, found = false });
                return;
            }

            // Validate signature if app has secret
            if (!ValidateSignature(appStore, token, "", signature))
            {
                Response.StatusCode = 401;
                WriteJson(new { success = false, message = "Invalid signature" });
                return;
            }

            // Get session
            if (!appStore.Sessions.TryGetValue(token, out var session))
            {
                WriteJson(new { success = true, found = false });
                return;
            }

            // Check expiry
            if (DateTime.UtcNow > session.ExpiresAt)
            {
                appStore.Sessions.TryRemove(token, out _);
                WriteJson(new { success = true, found = false, reason = "expired" });
                return;
            }

            // Sliding expiry: extend timeout on access
            session.LastAccessedAt = DateTime.UtcNow;
            session.ExpiresAt = DateTime.UtcNow.AddMinutes(appStore.DefaultTimeout);

            WriteJson(new
            {
                success = true,
                found = true,
                text = session.Text,
                expires_at = session.ExpiresAt.ToString("o")
            });
        }

        private void HandleDelete(Dictionary<string, string> parameters)
        {
            string appId = GetParam(parameters, "app_id", "");
            string token = GetParam(parameters, "session_token", "");
            string signature = GetParam(parameters, "signature", "");

            // Validate required fields
            if (string.IsNullOrEmpty(appId) || string.IsNullOrEmpty(token))
            {
                WriteJson(new { success = false, message = "app_id and session_token are required" });
                return;
            }

            // Get app store
            if (!_apps.TryGetValue(appId, out var appStore))
            {
                WriteJson(new { success = true, message = "App not found" });
                return;
            }

            // Validate signature if app has secret
            if (!ValidateSignature(appStore, token, "", signature))
            {
                Response.StatusCode = 401;
                WriteJson(new { success = false, message = "Invalid signature" });
                return;
            }

            // Remove session
            appStore.Sessions.TryRemove(token, out _);

            // Queue database operation
            if (_dbEnabled)
            {
                _dbQueue.TryAdd(new DbOperation
                {
                    Type = DbOpType.Delete,
                    AppId = appId,
                    Token = token
                });
            }

            WriteJson(new { success = true });
        }

        private void HandleClearApp(Dictionary<string, string> parameters)
        {
            string appId = GetParam(parameters, "app_id", "");
            string signature = GetParam(parameters, "signature", "");

            // Validate required fields
            if (string.IsNullOrEmpty(appId))
            {
                WriteJson(new { success = false, message = "app_id is required" });
                return;
            }

            // Get app store
            if (!_apps.TryGetValue(appId, out var appStore))
            {
                WriteJson(new { success = true, cleared_count = 0 });
                return;
            }

            // Validate signature if app has secret
            if (!ValidateSignature(appStore, "", "", signature))
            {
                Response.StatusCode = 401;
                WriteJson(new { success = false, message = "Invalid signature" });
                return;
            }

            // Count and clear
            int count = appStore.Sessions.Count;
            appStore.Sessions.Clear();

            // Queue database operation
            if (_dbEnabled)
            {
                _dbQueue.TryAdd(new DbOperation
                {
                    Type = DbOpType.ClearApp,
                    AppId = appId
                });
            }

            WriteJson(new { success = true, cleared_count = count });
        }

        private void HandleExtend(Dictionary<string, string> parameters)
        {
            string appId = GetParam(parameters, "app_id", "");
            string signature = GetParam(parameters, "signature", "");
            int timeout = GetParamInt(parameters, "timeout", 0);

            // Validate required fields
            if (string.IsNullOrEmpty(appId))
            {
                WriteJson(new { success = false, message = "app_id is required" });
                return;
            }

            // Get app store
            if (!_apps.TryGetValue(appId, out var appStore))
            {
                WriteJson(new { success = true, extended_count = 0 });
                return;
            }

            // Validate signature if app has secret
            if (!ValidateSignature(appStore, "", "", signature))
            {
                Response.StatusCode = 401;
                WriteJson(new { success = false, message = "Invalid signature" });
                return;
            }

            // Determine timeout
            if (timeout <= 0)
            {
                timeout = appStore.DefaultTimeout;
            }

            // Extend all sessions
            DateTime newExpiry = DateTime.UtcNow.AddMinutes(timeout);
            int count = 0;

            foreach (var session in appStore.Sessions.Values)
            {
                session.ExpiresAt = newExpiry;
                session.LastAccessedAt = DateTime.UtcNow;
                count++;
            }

            WriteJson(new { success = true, extended_count = count });
        }

        private void HandlePing()
        {
            WriteJson(new
            {
                success = true,
                message = "pong",
                timestamp = DateTime.UtcNow.ToString("o")
            });
        }

        private void HandleStats(Dictionary<string, string> parameters)
        {
            string appId = GetParam(parameters, "app_id", "");

            if (!string.IsNullOrEmpty(appId))
            {
                // Stats for specific app
                if (_apps.TryGetValue(appId, out var appStore))
                {
                    WriteJson(new
                    {
                        success = true,
                        app_id = appId,
                        total_sessions = appStore.Sessions.Count,
                        created_at = appStore.CreatedAt.ToString("o")
                    });
                }
                else
                {
                    WriteJson(new { success = true, app_id = appId, total_sessions = 0 });
                }
            }
            else
            {
                // Global stats
                var uptime = DateTime.UtcNow - _startTime;

                WriteJson(new
                {
                    success = true,
                    total_apps = _apps.Count,
                    total_sessions = _apps.Values.Sum(a => a.Sessions.Count),
                    memory_mb = Math.Round(GC.GetTotalMemory(false) / 1024.0 / 1024.0, 2),
                    uptime = uptime.ToString(@"d\.hh\:mm\:ss"),
                    db_enabled = _dbEnabled,
                    db_queue_size = _dbQueue.Count
                });
            }
        }

        #endregion

        #region Security

        private bool ValidateSignature(AppStore appStore, string token, string text, string signature)
        {
            // No secret configured = no validation required
            if (string.IsNullOrEmpty(appStore.Secret))
            {
                return true;
            }

            // Secret configured but no signature provided
            if (string.IsNullOrEmpty(signature))
            {
                return false;
            }

            // Compute expected signature: SHA256(secret + "|" + token + "|" + text)
            string input = $"{appStore.Secret}|{token}|{text}";
            string expected = ComputeSha256(input);

            return string.Equals(signature, expected, StringComparison.OrdinalIgnoreCase);
        }

        private string ComputeSha256(string input)
        {
            using (var sha256 = SHA256.Create())
            {
                byte[] bytes = Encoding.UTF8.GetBytes(input);
                byte[] hash = sha256.ComputeHash(bytes);
                return BitConverter.ToString(hash).Replace("-", "").ToLower();
            }
        }

        #endregion

        #region App Management

        private AppStore GetOrCreateAppStore(string appId)
        {
            return _apps.GetOrAdd(appId, id =>
            {
                // Check if we have config for this app
                if (_appConfigs.TryGetValue(id, out var config))
                {
                    return new AppStore
                    {
                        AppId = id,
                        Secret = config.Secret,
                        DefaultTimeout = config.DefaultTimeout
                    };
                }

                // Auto-register with defaults (no secret)
                return new AppStore
                {
                    AppId = id,
                    Secret = null,
                    DefaultTimeout = 20
                };
            });
        }

        private void LoadAppConfigs()
        {
            try
            {
                string configPath = HttpContext.Current.Server.MapPath("~/App_Data/apps.json");

                if (File.Exists(configPath))
                {
                    string json = File.ReadAllText(configPath);
                    var config = JsonConvert.DeserializeObject<AppsConfigFile>(json);

                    if (config?.Apps != null)
                    {
                        foreach (var app in config.Apps)
                        {
                            _appConfigs[app.AppId] = app;
                        }
                    }
                }
            }
            catch
            {
                // Config file is optional - continue without it
            }
        }

        private class AppsConfigFile
        {
            public List<AppConfig> Apps { get; set; }
        }

        #endregion

        #region Cleanup

        private void CleanupExpiredSessions(object state)
        {
            try
            {
                DateTime now = DateTime.UtcNow;
                int totalRemoved = 0;

                foreach (var appStore in _apps.Values)
                {
                    var expiredTokens = appStore.Sessions
                        .Where(kvp => now > kvp.Value.ExpiresAt)
                        .Select(kvp => kvp.Key)
                        .ToList();

                    foreach (var token in expiredTokens)
                    {
                        if (appStore.Sessions.TryRemove(token, out _))
                        {
                            totalRemoved++;

                            // Queue database delete
                            if (_dbEnabled)
                            {
                                _dbQueue.TryAdd(new DbOperation
                                {
                                    Type = DbOpType.Delete,
                                    AppId = appStore.AppId,
                                    Token = token
                                });
                            }
                        }
                    }
                }

                // Optional: Log cleanup stats
                // System.Diagnostics.Debug.WriteLine($"Cleanup removed {totalRemoved} expired sessions");
            }
            catch
            {
                // Swallow cleanup errors - don't crash the timer
            }
        }

        #endregion

        #region Database Operations

        private void InitializeDatabase()
        {
            try
            {
                string configPath = HttpContext.Current.Server.MapPath("~/App_Data/dbconfig.txt");

                if (File.Exists(configPath))
                {
                    _dbConnectionString = File.ReadAllText(configPath).Trim();
                    _dbEnabled = !string.IsNullOrEmpty(_dbConnectionString);
                }
            }
            catch
            {
                _dbEnabled = false;
            }
        }

        private void LoadSessionsFromDatabase()
        {
            if (!_dbEnabled) return;

            try
            {
                using (var conn = new MySqlConnector.MySqlConnection(_dbConnectionString))
                {
                    conn.Open();
                    using (var cmd = conn.CreateCommand())
                    {
                        cmd.CommandText = @"
                            SELECT app_id, token, text, expires_at 
                            FROM state_sessions 
                            WHERE expires_at > UTC_TIMESTAMP()";

                        using (var reader = cmd.ExecuteReader())
                        {
                            while (reader.Read())
                            {
                                string appId = reader.GetString(0);
                                string token = reader.GetString(1);
                                string text = reader.IsDBNull(2) ? "" : reader.GetString(2);
                                DateTime expiresAt = reader.GetDateTime(3);

                                var appStore = GetOrCreateAppStore(appId);
                                appStore.Sessions[token] = new SessionEntry
                                {
                                    Token = token,
                                    Text = text,
                                    CreatedAt = DateTime.UtcNow,
                                    ExpiresAt = expiresAt,
                                    LastAccessedAt = DateTime.UtcNow
                                };
                            }
                        }
                    }
                }
            }
            catch
            {
                // Database load failure is not fatal - start with empty cache
            }
        }

        private void DatabaseWorkerLoop()
        {
            foreach (var op in _dbQueue.GetConsumingEnumerable())
            {
                try
                {
                    ProcessDbOperation(op);
                }
                catch
                {
                    // Log error but continue processing queue
                }
            }
        }

        private void ProcessDbOperation(DbOperation op)
        {
            using (var conn = new MySqlConnector.MySqlConnection(_dbConnectionString))
            {
                conn.Open();
                using (var cmd = conn.CreateCommand())
                {
                    switch (op.Type)
                    {
                        case DbOpType.Set:
                            cmd.CommandText = @"
                                INSERT INTO state_sessions (app_id, token, text, expires_at, updated_at)
                                VALUES (@app_id, @token, @text, @expires_at, UTC_TIMESTAMP())
                                ON DUPLICATE KEY UPDATE 
                                    text = @text, 
                                    expires_at = @expires_at, 
                                    updated_at = UTC_TIMESTAMP()";
                            cmd.Parameters.AddWithValue("@app_id", op.AppId);
                            cmd.Parameters.AddWithValue("@token", op.Token);
                            cmd.Parameters.AddWithValue("@text", op.Text ?? "");
                            cmd.Parameters.AddWithValue("@expires_at", op.ExpiresAt);
                            break;

                        case DbOpType.Delete:
                            cmd.CommandText = "DELETE FROM state_sessions WHERE app_id = @app_id AND token = @token";
                            cmd.Parameters.AddWithValue("@app_id", op.AppId);
                            cmd.Parameters.AddWithValue("@token", op.Token);
                            break;

                        case DbOpType.ClearApp:
                            cmd.CommandText = "DELETE FROM state_sessions WHERE app_id = @app_id";
                            cmd.Parameters.AddWithValue("@app_id", op.AppId);
                            break;
                    }

                    cmd.ExecuteNonQuery();
                }
            }
        }

        #endregion

        #region Helper Methods

        private Dictionary<string, string> ParseRequestParameters()
        {
            var parameters = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

            // Form parameters
            foreach (string key in Request.Form.AllKeys)
            {
                if (!string.IsNullOrEmpty(key))
                {
                    parameters[key] = Request.Form[key];
                }
            }

            // Query string parameters
            foreach (string key in Request.QueryString.AllKeys)
            {
                if (!string.IsNullOrEmpty(key))
                {
                    parameters[key] = Request.QueryString[key];
                }
            }

            // JSON body
            if (Request.ContentType?.Contains("application/json") == true)
            {
                try
                {
                    Request.InputStream.Position = 0;
                    using (var reader = new StreamReader(Request.InputStream))
                    {
                        string json = reader.ReadToEnd();
                        var jsonParams = JsonConvert.DeserializeObject<Dictionary<string, object>>(json);

                        if (jsonParams != null)
                        {
                            foreach (var kvp in jsonParams)
                            {
                                parameters[kvp.Key] = kvp.Value?.ToString() ?? "";
                            }
                        }
                    }
                }
                catch { }
            }

            return parameters;
        }

        private string GetParam(Dictionary<string, string> parameters, string key, string defaultValue)
        {
            return parameters.TryGetValue(key, out var value) ? value : defaultValue;
        }

        private int GetParamInt(Dictionary<string, string> parameters, string key, int defaultValue)
        {
            if (parameters.TryGetValue(key, out var value) && int.TryParse(value, out int result))
            {
                return result;
            }
            return defaultValue;
        }

        private void WriteJson(object obj)
        {
            Response.Write(JsonConvert.SerializeObject(obj));
        }

        private void RejectRequest(string message, int statusCode)
        {
            Response.StatusCode = statusCode;
            Response.ContentType = "application/json";
            Response.Write(JsonConvert.SerializeObject(new { success = false, message }));
            Response.End();
        }

        #endregion
    }
}

Database Schema (Optional)

-- MySQL schema for persistent session storage

CREATE TABLE state_sessions (
    app_id VARCHAR(100) NOT NULL,
    token VARCHAR(100) NOT NULL,
    text LONGTEXT,
    expires_at DATETIME NOT NULL,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (app_id, token),
    INDEX idx_expires (expires_at),
    INDEX idx_app (app_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- Cleanup job (run periodically via MySQL Event or external scheduler)
-- DELETE FROM state_sessions WHERE expires_at < UTC_TIMESTAMP();

App Configuration File (Optional)

// App_Data/apps.json

{
    "apps": [
        {
            "app_id": "abc_book_store",
            "secret": "your-secret-key-here",
            "default_timeout": 30
        },
        {
            "app_id": "client_site_2",
            "secret": null,
            "default_timeout": 20
        },
        {
            "app_id": "dev_app",
            "secret": null,
            "default_timeout": 60
        }
    ]
}

Client Library

Use this client class in your WPA web applications to communicate with the state server:

// StateServerClient.cs

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using Newtonsoft.Json;

namespace MyWebApp
{
    public class StateServerClient
    {
        private readonly string _serverUrl;
        private readonly string _appId;
        private readonly string _secret;

        public StateServerClient(string serverUrl, string appId, string secret = null)
        {
            _serverUrl = serverUrl.TrimEnd('/');
            _appId = appId;
            _secret = secret;
        }

        /// <summary>
        /// Store session data. Returns the session token.
        /// </summary>
        public string Set(string token, string text, int? timeoutMinutes = null)
        {
            var parameters = new NameValueCollection
            {
                { "action", "set" },
                { "app_id", _appId },
                { "session_token", token ?? "" },
                { "text", text ?? "" }
            };

            if (timeoutMinutes.HasValue)
            {
                parameters.Add("timeout", timeoutMinutes.Value.ToString());
            }

            // Add signature if secret is configured
            if (!string.IsNullOrEmpty(_secret))
            {
                parameters.Add("signature", ComputeSignature(token ?? "", text ?? ""));
            }

            var response = PostRequest(parameters);
            return response?.session_token;
        }

        /// <summary>
        /// Retrieve session data. Returns null if not found.
        /// </summary>
        public string Get(string token)
        {
            var parameters = new NameValueCollection
            {
                { "action", "get" },
                { "app_id", _appId },
                { "session_token", token }
            };

            // Add signature if secret is configured
            if (!string.IsNullOrEmpty(_secret))
            {
                parameters.Add("signature", ComputeSignature(token, ""));
            }

            var response = PostRequest(parameters);

            if (response?.success == true && response?.found == true)
            {
                return response.text;
            }

            return null;
        }

        /// <summary>
        /// Retrieve and deserialize session data.
        /// </summary>
        public T Get<T>(string token) where T : class
        {
            string text = Get(token);

            if (string.IsNullOrEmpty(text))
                return null;

            try
            {
                return JsonConvert.DeserializeObject<T>(text);
            }
            catch
            {
                return null;
            }
        }

        /// <summary>
        /// Store serialized object as session data.
        /// </summary>
        public string Set<T>(string token, T data, int? timeoutMinutes = null)
        {
            string text = JsonConvert.SerializeObject(data);
            return Set(token, text, timeoutMinutes);
        }

        /// <summary>
        /// Delete a session.
        /// </summary>
        public bool Delete(string token)
        {
            var parameters = new NameValueCollection
            {
                { "action", "delete" },
                { "app_id", _appId },
                { "session_token", token }
            };

            if (!string.IsNullOrEmpty(_secret))
            {
                parameters.Add("signature", ComputeSignature(token, ""));
            }

            var response = PostRequest(parameters);
            return response?.success == true;
        }

        /// <summary>
        /// Clear all sessions for this app.
        /// </summary>
        public int ClearAll()
        {
            var parameters = new NameValueCollection
            {
                { "action", "clear_app" },
                { "app_id", _appId }
            };

            if (!string.IsNullOrEmpty(_secret))
            {
                parameters.Add("signature", ComputeSignature("", ""));
            }

            var response = PostRequest(parameters);
            return response?.cleared_count ?? 0;
        }

        /// <summary>
        /// Extend expiry for all sessions.
        /// </summary>
        public int ExtendAll(int? timeoutMinutes = null)
        {
            var parameters = new NameValueCollection
            {
                { "action", "extend" },
                { "app_id", _appId }
            };

            if (timeoutMinutes.HasValue)
            {
                parameters.Add("timeout", timeoutMinutes.Value.ToString());
            }

            if (!string.IsNullOrEmpty(_secret))
            {
                parameters.Add("signature", ComputeSignature("", ""));
            }

            var response = PostRequest(parameters);
            return response?.extended_count ?? 0;
        }

        /// <summary>
        /// Health check.
        /// </summary>
        public bool Ping()
        {
            try
            {
                var parameters = new NameValueCollection
                {
                    { "action", "ping" }
                };

                var response = PostRequest(parameters);
                return response?.success == true;
            }
            catch
            {
                return false;
            }
        }

        #region Private Methods

        private dynamic PostRequest(NameValueCollection parameters)
        {
            using (var client = new WebClient())
            {
                byte[] responseBytes = client.UploadValues(_serverUrl, "POST", parameters);
                string responseJson = Encoding.UTF8.GetString(responseBytes);
                return JsonConvert.DeserializeObject<dynamic>(responseJson);
            }
        }

        private string ComputeSignature(string token, string text)
        {
            string input = $"{_secret}|{token}|{text}";

            using (var sha256 = SHA256.Create())
            {
                byte[] bytes = Encoding.UTF8.GetBytes(input);
                byte[] hash = sha256.ComputeHash(bytes);
                return BitConverter.ToString(hash).Replace("-", "").ToLower();
            }
        }

        #endregion
    }
}

Usage in WPA Application

// Global.asax.cs in your main web application

public class Global : HttpApplication
{
    // State server client
    private static StateServerClient _stateClient;

    protected void Application_Start(object sender, EventArgs e)
    {
        // Initialize state server client
        _stateClient = new StateServerClient(
            serverUrl: "http://127.0.0.1:8090",
            appId: "abc_book_store",
            secret: "your-secret-key-here"  // null if no signature required
        );

        // Verify connection
        if (!_stateClient.Ping())
        {
            throw new Exception("Cannot connect to State Server!");
        }
    }

    protected void Application_BeginRequest(object sender, EventArgs e)
    {
        // Get session token from cookie
        string sessionToken = Request.Cookies["sid"]?.Value;

        if (string.IsNullOrEmpty(sessionToken))
        {
            // New visitor - generate token
            sessionToken = Guid.NewGuid().ToString("N");
            Response.Cookies.Add(new HttpCookie("sid", sessionToken)
            {
                HttpOnly = true,
                Expires = DateTime.Now.AddDays(30)
            });
        }

        // Load user session from state server
        var userSession = _stateClient.Get<UserSession>(sessionToken);

        // Store in request context for handlers
        Context.Items["SessionToken"] = sessionToken;
        Context.Items["UserSession"] = userSession;

        // Handle request...
        HandleRequest();
        Response.End();
    }

    // Login handler example
    private void HandleLogin(string username, string password)
    {
        // Validate credentials...
        var user = ValidateUser(username, password);

        if (user != null)
        {
            string sessionToken = (string)Context.Items["SessionToken"];

            // Store user session in state server
            var userSession = new UserSession
            {
                UserId = user.Id,
                Username = user.Username,
                IsAdmin = user.IsAdmin,
                LoginTime = DateTime.UtcNow
            };

            _stateClient.Set(sessionToken, userSession, timeoutMinutes: 60);

            // Update request context
            Context.Items["UserSession"] = userSession;
        }
    }

    // Logout handler example
    private void HandleLogout()
    {
        string sessionToken = (string)Context.Items["SessionToken"];
        _stateClient.Delete(sessionToken);
        Context.Items["UserSession"] = null;
    }
}

public class UserSession
{
    public int UserId { get; set; }
    public string Username { get; set; }
    public bool IsAdmin { get; set; }
    public DateTime LoginTime { get; set; }
}

Deployment

Running the State Server

  1. Create new ASP.NET Web Forms project (empty)
  2. Add Global.asax and Global.asax.cs with the code above
  3. Add App_Data/apps.json for app configuration
  4. Optionally add App_Data/dbconfig.txt with MySQL connection string
  5. Run on dedicated port (e.g., 8090)
IIS Express:
iisexpress /path:C:\StateServer /port:8090

Or configure in IIS:
- Bind to 127.0.0.1:8090 only
- Application Pool: .NET 4.8, Integrated

Web.config

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.web>
    <compilation debug="true" targetFramework="4.8" />
    <httpRuntime targetFramework="4.8" maxRequestLength="10240" />
  </system.web>
</configuration>

Security Considerations

  1. Local Only: Server only accepts connections from 127.0.0.1
  2. Signature Validation: SHA256 signature prevents tampering when secret is configured
  3. No External Access: Never expose state server to public network
  4. Secret Rotation: Change app secrets periodically
  5. Firewall Rules: Block port 8090 from external access

Performance Characteristics

OperationPerformance
Set (cache only)< 1ms
Get< 1ms
Delete< 1ms
Set (with DB queue)< 1ms (non-blocking)
DB write (background)~5-10ms per operation
Cleanup cycleRuns every 1 minute

Comparison with ASP.NET StateServer

FeatureASP.NET StateServerWPA State Server
ProtocolBinary (proprietary)HTTP + JSON
InspectionCannot view dataFull visibility
Multi-tenantNoYes (app isolation)
CustomizationNoneFull control
PersistenceMemory onlyMemory + Database
Signature authNoOptional SHA256
MonitoringLimitedStats API
Learning curveConfigurationCode (transparent)

Conclusion

Building your own Session State Server with WPA architecture provides complete transparency and control over session management. The two-level storage strategy (in-process cache + database) ensures both performance and persistence, while the non-blocking write queue keeps response times minimal.

This approach demonstrates the power of WPA architecture: a fully functional, production-ready service built with zero ASPX pages, pure Application_BeginRequest handling, and complete visibility into every aspect of the system.


This implementation serves as both a practical session state solution and a demonstration of WPA architecture principles applied to infrastructure services.

Photo by A Chosen Soul on Unsplash