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

WPA Session State Server
Client App (WPA)
Global.asax
BeginRequest
SessionClient
Port 8080
HTTP POST
JSON
State Server (WPA)
Global.asax
BeginRequest
ConcurrentDict
Port 8090 (127.0.0.1)
Background Process
BlockingQueue โ†’ MySQL

Storage Strategy

Level 1: In-Process Cache (Primary)

Two-level ConcurrentDictionary for app isolation:

Data Structure
ConcurrentDictionary<app_id, AppStore>
“tgc_website” โ”€โ”€โ–บ AppStore
ConcurrentDictionary<token, SessionEntry>
“token_abc” โ”€โ”€โ–บ { text, expiry }
“token_def” โ”€โ”€โ–บ { text, expiry }
“token_xyz” โ”€โ”€โ–บ { text, expiry }
“client_site_2” โ”€โ”€โ–บ AppStore
ConcurrentDictionary<token, SessionEntry>
“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/
apps.json โ† App configuration (optional)

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.End();
        }

        #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