ASP.NET Web Forms True Pageless Architecture with Custom Session State

True Pageless: ASP.NET Web Forms Without a Single ASPX File

Implementing complete request interception at Global.asax with custom distributed session state


Introduction

In my previous article, Introducing ASP.NET Web Forms Pageless Architecture (WPA), I demonstrated how to build modern web applications on ASP.NET Web Forms by stripping away server controls, ViewState, and PostBack while using a single Default.aspx as the entry point.

But a question lingered: Is even that one ASPX file necessary?

The answer is no. By intercepting requests at Application_BeginRequest in Global.asax.cs, we can build a truly pageless architecture—zero ASPX files, zero page lifecycle, pure HTTP request handling.

This article demonstrates the complete implementation, including integration with a custom Session State Server built using the same WPA principles.


What “True Pageless” Means

This block is prepared for AI readers, AI can't see the image.

Standard WPA vs TRUE PAGELESS
=============================

Standard WPA - Request and Process Flow:

Request > IIS > ASP.NET Pipeline > Default.aspx > Page_load

Files: Global.aax, Global.asax.cs, Default.aspx, Default.aspx.cs
Page Lifecycle: Yes (minimal, but exists)


True Pageless - Request and Process Flow:

Request > IIS > ASP.NET Pipeline > Application_BeginRequest

Files: Global.asax, Global.asax.cs
Page Lifecycle: None
ASPX files: Zero

The Application_BeginRequest event fires before any page is instantiated. By handling all requests here and calling Response.End(), we bypass the entire page infrastructure.


Architecture Overview

This block is prepared for AI readers. AI can't reader image.

● TRUE PAGELESS ARCHITECTURE
═══════════════════════════════════════════════════

                 ┌──────────────────┐
                 │   HTTP Request   │
                 └────────┬─────────┘
                          │
                          ▼
        ┌─────────────────────────────────────┐
        │           Global.asax.cs            │
        │       Application_BeginRequest      │
        │                                     │
        │  1. Security check (local only for  │
        │     admin/api)                      │
        │  2. Load session from State Server  │
        │  3. Route resolution                │
        │  4. Handler dispatch                │
        │  5. Response.End()                  │
        └────────────────┬────────────────────┘
                         │
                         ▼
          ┌──────────────────────────────┐
          │                              │
    ┌─────┴──────┐  ┌────────┐  ┌───────┴─────┐
    │   Public   │  │ Admin  │  │     API     │
    │  Handlers  │  │Handlers│  │  Handlers   │
    └─────┬──────┘  └────────┘  └───────┬─────┘
          │                              │
          └──────────────────────────────┘
                         │
                         ▼
        ┌─────────────────────────────────────┐
        │          Two-Tier Cache             │
        │  ConcurrentDictionary + File Cache  │
        └─────────────────────────────────────┘
                         │
                         ▼
        ┌─────────────────────────────────────┐
        │     Custom Session State Server     │
        │  (Separate WPA App) - Port 8090     │
        │            (Local)                  │
        └─────────────────────────────────────┘

Why Bypass ASP.NET Session?

ASP.NET’s built-in Session has a limitation for true pageless architecture: it’s not available in Application_BeginRequest.

ASP.NET Request Pipeline:

  1. BeginRequestWe intercept here (Session NOT available)
  2. AuthenticationRequest
  3. PostAuthenticateRequest
  4. AuthorizeRequest
  5. PostAuthorizeRequest
  6. ResolveRequestCache
  7. PostResolveRequestCache
  8. MapRequestHandler
  9. PostMapRequestHandler
  10. AcquireRequestStateSession loads here
  11. PostAcquireRequestStateSession available from here
  12. PreRequestHandlerExecute
  13. [Handler Executes] – Default.aspx runs here
  14. PostRequestHandlerExecute

To use ASP.NET Session, we’d have to wait until PostAcquireRequestState—which partially defeats the purpose of early interception.

Solution: Use a custom Session State Server that we can query at any point in the pipeline. This is where our WPA Session State Server comes in.


Possible Project Structure

Since we are doing a custom engineering, there is no limit of how the code and system architecture should designed, below is just one of possible engineering architecture. In real world, you might have a dedicated person or group responsible to engineer the core engineering which commonly seen in large corporation and enterprise that requires unique custom solution. But nonetheless, this is not limited to to large corporation, with availability of AI, you are already equivalent to have a dedicate team.

MySolution/
├── MyWebApp/                    ← Main web application
│   ├── Global.asax
│   ├── Global.asax.cs           ← ALL application logic here
│   ├── Web.config
│   │
│   ├── Core/
│   │   ├── RouteResolver.cs     ← URL parsing and routing
│   │   ├── CacheStore.cs        ← Two-tier cache
│   │   ├── StateServerClient.cs ← Session state client
│   │   └── HandlerFactory.cs    ← Handler instantiation
│   │
│   ├── Handlers/
│   │   ├── IHandler.cs
│   │   ├── Public/
│   │   │   ├── HomepageHandler.cs
│   │   │   └── ArticleHandler.cs
│   │   ├── Admin/
│   │   │   ├── DashboardHandler.cs
│   │   │   └── ArticlesHandler.cs
│   │   └── Api/
│   │       ├── ArticlesApiHandler.cs
│   │       └── AuthApiHandler.cs
│   │
│   └── App_Data/
│       ├── templates/           ← HTML templates
│       ├── cache/               ← Static file cache
│       └── assets/              ← Static files (CSS, JS, images)
│
└── StateServer/                 ← Session State Server (separate app)
    ├── Global.asax
    ├── Global.asax.cs           ← State server logic
    └── Web.config

Implementation

Global.asax

The Global.asax file remains minimal:

<%@ Application Language="C#" CodeBehind="Global.asax.cs" Inherits="MyWebApp.Global" %>

Global.asax.cs – The Complete Entry Point

using System;
using System.Collections.Concurrent;
using System.IO;
using System.Web;
using Newtonsoft.Json;

namespace MyWebApp
{
    public class Global : HttpApplication
    {
        #region Static Resources

        // Session state client (connects to our custom state server)
        private static StateServerClient _stateClient;

        // Page cache
        private static ConcurrentDictionary<string, CachedPage> _pageCache
            = new ConcurrentDictionary<string, CachedPage>();

        // Application start time
        private static DateTime _startTime;

        // Static file extensions to skip
        private static readonly HashSet<string> StaticExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
        {
            ".css", ".js", ".png", ".jpg", ".jpeg", ".gif", ".ico", ".svg",
            ".woff", ".woff2", ".ttf", ".eot", ".map", ".webp"
        };

        #endregion

        #region Application Lifecycle

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

            // Initialize session state client
            _stateClient = new StateServerClient(
                serverUrl: "http://127.0.0.1:8090",
                appId: "my_web_app",
                secret: "your-secret-key-here"  // null if no signature required
            );

            // Verify state server connection
            if (!_stateClient.Ping())
            {
                // Log warning but don't fail - state server might start later
                System.Diagnostics.Debug.WriteLine("WARNING: State Server not available");
            }

            // Initialize cache store
            CacheStore.Initialize(Server.MapPath("~/App_Data/cache"));

            // Load route configurations, templates, etc.
            RouteResolver.Initialize(Server.MapPath("~/App_Data"));
        }

        #endregion

        #region Request Handling - THE CORE

        protected void Application_BeginRequest(object sender, EventArgs e)
        {
            string path = Request.Path.ToLower();

            // ════════════════════════════════════════════════════════════
            // STEP 1: Skip static files - let IIS handle them
            // ════════════════════════════════════════════════════════════

            if (IsStaticFile(path))
            {
                return; // IIS serves static files directly
            }

            // ════════════════════════════════════════════════════════════
            // STEP 2: Load session from custom State Server
            // ════════════════════════════════════════════════════════════

            var session = LoadSession();
            Context.Items["UserSession"] = session;

            // ════════════════════════════════════════════════════════════
            // STEP 3: Resolve route
            // ════════════════════════════════════════════════════════════

            var route = RouteResolver.Resolve(Request);

            // ════════════════════════════════════════════════════════════
            // STEP 4: Security checks
            // ════════════════════════════════════════════════════════════

            // Admin routes require authentication
            if (route.Type == RouteType.Admin)
            {
                if (session == null || !session.IsAdmin)
                {
                    if (route.Handler != "login")
                    {
                        Response.Redirect("/admin/login");
                        Response.Flush();
                        Response.SuppressContent = true;
                        HttpContext.Current.ApplicationInstance.CompleteRequest();
                        return;
                    }
                }
            }

            // ════════════════════════════════════════════════════════════
            // STEP 5: Handle request
            // ════════════════════════════════════════════════════════════

            try
            {
                switch (route.Type)
                {
                    case RouteType.Public:
                        HandlePublic(route);
                        break;

                    case RouteType.Admin:
                        HandleAdmin(route, session);
                        break;

                    case RouteType.Api:
                        HandleApi(route, session);
                        break;

                    case RouteType.Redirect:
                        Response.RedirectPermanent(route.RedirectTo);
                        break;

                    case RouteType.NotFound:
                    default:
                        Handle404();
                        break;
                }
            }
            catch (Exception ex)
            {
                HandleError(ex);
            }

            // ════════════════════════════════════════════════════════════
            // STEP 6: End response - bypass page lifecycle entirely
            // ════════════════════════════════════════════════════════════

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

        #endregion

        #region Session Management

        private UserSession LoadSession()
        {
            try
            {
                // Get session token from cookie
                string token = Request.Cookies["sid"]?.Value;

                if (string.IsNullOrEmpty(token))
                {
                    // New visitor - create token
                    token = Guid.NewGuid().ToString("N");
                    Response.Cookies.Add(new HttpCookie("sid", token)
                    {
                        HttpOnly = true,
                        Secure = Request.IsSecureConnection,
                        Expires = DateTime.Now.AddDays(30)
                    });
                    Context.Items["SessionToken"] = token;
                    return null;
                }

                Context.Items["SessionToken"] = token;

                // Load from state server
                return _stateClient.Get<UserSession>(token);
            }
            catch
            {
                return null;
            }
        }

        /// <summary>
        /// Save session - call this after login or session changes
        /// </summary>
        public static void SaveSession(HttpContext context, UserSession session)
        {
            string token = context.Items["SessionToken"] as string;
            if (!string.IsNullOrEmpty(token))
            {
                _stateClient.Set(token, session, timeoutMinutes: 60);
                context.Items["UserSession"] = session;
            }
        }

        /// <summary>
        /// Clear session - call this on logout
        /// </summary>
        public static void ClearSession(HttpContext context)
        {
            string token = context.Items["SessionToken"] as string;
            if (!string.IsNullOrEmpty(token))
            {
                _stateClient.Delete(token);
                context.Items["UserSession"] = null;
            }
        }

        #endregion

        #region Route Handlers

        private void HandlePublic(RouteResult route)
        {
            // Try cache first
            if (CacheStore.TryGetPage(route.CacheKey, out string html))
            {
                WriteHtml(html);
                return;
            }

            // Build page via handler
            var handler = HandlerFactory.CreatePublic(route.Handler);
            if (handler != null)
            {
                html = handler.Execute(Context, route);

                // Cache the result
                if (!string.IsNullOrEmpty(route.CacheKey))
                {
                    CacheStore.SetPage(route.CacheKey, html);
                }

                WriteHtml(html);
            }
            else
            {
                Handle404();
            }
        }

        private void HandleAdmin(RouteResult route, UserSession session)
        {
            var handler = HandlerFactory.CreateAdmin(route.Handler);
            if (handler != null)
            {
                string html = handler.Execute(Context, route, session);
                WriteHtml(html);
            }
            else
            {
                Handle404();
            }
        }

        private void HandleApi(RouteResult route, UserSession session)
        {
            Response.ContentType = "application/json";

            var handler = HandlerFactory.CreateApi(route.Handler);
            if (handler != null)
            {
                string json = handler.Execute(Context, route, session);
                Response.Write(json);
            }
            else
            {
                Response.StatusCode = 404;
                Response.Write("{\"success\":false,\"message\":\"API not found\"}");
            }
        }

        private void Handle404()
        {
            Response.StatusCode = 404;
            Response.ContentType = "text/html";

            // Try to load custom 404 template
            string html = TemplateEngine.Render("404", new { });
            Response.Write(html);
        }

        private void HandleError(Exception ex)
        {
            Response.StatusCode = 500;
            Response.ContentType = "text/html";

            #if DEBUG
            Response.Write($"<pre>{ex}</pre>");
            #else
            string html = TemplateEngine.Render("error", new { message = "An error occurred" });
            Response.Write(html);
            #endif
        }

        #endregion

        #region Helper Methods

        private bool IsStaticFile(string path)
        {
            // Check by extension
            string ext = Path.GetExtension(path);
            if (!string.IsNullOrEmpty(ext) && StaticExtensions.Contains(ext))
            {
                return true;
            }

            // Check by path prefix
            if (path.StartsWith("/assets/") ||
                path.StartsWith("/uploads/") ||
                path.StartsWith("/static/"))
            {
                return true;
            }

            return false;
        }

        private void WriteHtml(string html)
        {
            Response.ContentType = "text/html; charset=utf-8";
            Response.Write(html);
        }

        #endregion
    }

    #region Supporting Classes

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

    public class CachedPage
    {
        public string Html { get; set; }
        public DateTime CachedAt { get; set; }
        public DateTime? ExpiresAt { get; set; }
    }

    #endregion
}

Route Resolver

using System;
using System.Collections.Generic;
using System.IO;
using System.Web;

namespace MyWebApp
{
    public enum RouteType
    {
        Public,
        Admin,
        Api,
        Redirect,
        NotFound
    }

    public class RouteResult
    {
        public RouteType Type { get; set; }
        public string Handler { get; set; }
        public string Action { get; set; }
        public int ContentId { get; set; }
        public string CacheKey { get; set; }
        public string RedirectTo { get; set; }
        public Dictionary<string, string> Params { get; set; } = new Dictionary<string, string>();
    }

    public static class RouteResolver
    {
        private static HashSet<string> _adminRoutes;
        private static HashSet<string> _apiRoutes;
        private static HashSet<string> _specialRoutes;
        private static Dictionary<string, SlugInfo> _slugCache;

        public static void Initialize(string appDataPath)
        {
            // Define admin routes
            _adminRoutes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
            {
                "dashboard", "articles", "article-edit", "gallery", "media",
                "settings", "users", "login", "logout"
            };

            // Define API routes
            _apiRoutes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
            {
                "articles", "gallery", "media", "auth", "settings"
            };

            // Define special public routes
            _specialRoutes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
            {
                "", "home", "news", "events", "gallery", "contact", "about"
            };

            // Load slug cache from database
            _slugCache = LoadSlugsFromDatabase();
        }

        public static RouteResult Resolve(HttpRequest request)
        {
            // Parse URL
            string path = request.Path.Trim('/').ToLower();
            string[] segments = path.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);

            // Collect all parameters
            var parameters = CollectParameters(request);

            // ════════════════════════════════════════════════════════════
            // Check API routes: /api/{handler}/{action}
            // ════════════════════════════════════════════════════════════

            if (segments.Length > 0 && segments[0] == "api")
            {
                string handler = segments.Length > 1 ? segments[1] : "";
                string action = parameters.ContainsKey("action")
                    ? parameters["action"]
                    : (segments.Length > 2 ? segments[2] : "");

                if (_apiRoutes.Contains(handler))
                {
                    return new RouteResult
                    {
                        Type = RouteType.Api,
                        Handler = handler,
                        Action = action,
                        Params = parameters
                    };
                }

                return new RouteResult { Type = RouteType.NotFound };
            }

            // ════════════════════════════════════════════════════════════
            // Check Admin routes: /admin/{handler}/{action}/{id}
            // ════════════════════════════════════════════════════════════

            if (segments.Length > 0 && segments[0] == "admin")
            {
                string handler = segments.Length > 1 ? segments[1] : "dashboard";
                string action = parameters.ContainsKey("action")
                    ? parameters["action"]
                    : (segments.Length > 2 ? segments[2] : "");

                if (_adminRoutes.Contains(handler))
                {
                    var result = new RouteResult
                    {
                        Type = RouteType.Admin,
                        Handler = handler,
                        Action = action,
                        Params = parameters
                    };

                    // Extract ID
                    string idStr = parameters.ContainsKey("id")
                        ? parameters["id"]
                        : (segments.Length > 3 ? segments[3] : "");

                    if (int.TryParse(idStr, out int id))
                    {
                        result.ContentId = id;
                    }

                    return result;
                }

                return new RouteResult { Type = RouteType.NotFound };
            }

            // ════════════════════════════════════════════════════════════
            // Check special public routes
            // ════════════════════════════════════════════════════════════

            string primarySegment = segments.Length > 0 ? segments[0] : "";

            if (_specialRoutes.Contains(primarySegment))
            {
                string handler = string.IsNullOrEmpty(primarySegment) ? "homepage" : primarySegment;
                return new RouteResult
                {
                    Type = RouteType.Public,
                    Handler = handler,
                    CacheKey = $"page:{handler}",
                    Params = parameters
                };
            }

            // ════════════════════════════════════════════════════════════
            // Check content slugs (articles, pages)
            // ════════════════════════════════════════════════════════════

            if (_slugCache.TryGetValue(primarySegment, out var slugInfo))
            {
                // Handle old slugs with redirect
                if (!slugInfo.IsCurrent)
                {
                    return new RouteResult
                    {
                        Type = RouteType.Redirect,
                        RedirectTo = "/" + slugInfo.CurrentSlug
                    };
                }

                return new RouteResult
                {
                    Type = RouteType.Public,
                    Handler = slugInfo.ContentType,
                    ContentId = slugInfo.ContentId,
                    CacheKey = $"{slugInfo.ContentType}:{slugInfo.ContentId}",
                    Params = parameters
                };
            }

            // ════════════════════════════════════════════════════════════
            // Not found
            // ════════════════════════════════════════════════════════════

            return new RouteResult { Type = RouteType.NotFound };
        }

        private static Dictionary<string, string> CollectParameters(HttpRequest request)
        {
            var parameters = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

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

            // Form data
            foreach (string key in request.Form.AllKeys)
            {
                if (!string.IsNullOrEmpty(key))
                    parameters[key] = request.Form[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 = Newtonsoft.Json.JsonConvert
                            .DeserializeObject<Dictionary<string, object>>(json);

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

            return parameters;
        }

        private static Dictionary<string, SlugInfo> LoadSlugsFromDatabase()
        {
            // Load from database - implementation depends on your data layer
            // Return dictionary for O(1) lookup
            return new Dictionary<string, SlugInfo>(StringComparer.OrdinalIgnoreCase);
        }
    }

    public class SlugInfo
    {
        public string Slug { get; set; }
        public string CurrentSlug { get; set; }
        public bool IsCurrent { get; set; }
        public string ContentType { get; set; }
        public int ContentId { get; set; }
    }
}

Handler Factory and Interfaces

using System;
using System.Collections.Generic;
using System.Web;

namespace MyWebApp
{
    public interface IPublicHandler
    {
        string Execute(HttpContext context, RouteResult route);
    }

    public interface IAdminHandler
    {
        string Execute(HttpContext context, RouteResult route, UserSession session);
    }

    public interface IApiHandler
    {
        string Execute(HttpContext context, RouteResult route, UserSession session);
    }

    public static class HandlerFactory
    {
        private static readonly Dictionary<string, Type> PublicHandlers = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase)
        {
            { "homepage", typeof(HomepageHandler) },
            { "article", typeof(ArticleHandler) },
            { "news", typeof(NewsHandler) },
            { "gallery", typeof(GalleryHandler) },
            { "contact", typeof(ContactHandler) }
        };

        private static readonly Dictionary<string, Type> AdminHandlers = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase)
        {
            { "dashboard", typeof(DashboardHandler) },
            { "articles", typeof(ArticlesHandler) },
            { "article-edit", typeof(ArticleEditHandler) },
            { "login", typeof(LoginHandler) },
            { "logout", typeof(LogoutHandler) }
        };

        private static readonly Dictionary<string, Type> ApiHandlers = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase)
        {
            { "articles", typeof(ArticlesApiHandler) },
            { "auth", typeof(AuthApiHandler) },
            { "media", typeof(MediaApiHandler) }
        };

        public static IPublicHandler CreatePublic(string name)
        {
            if (PublicHandlers.TryGetValue(name, out var type))
            {
                return (IPublicHandler)Activator.CreateInstance(type);
            }
            return null;
        }

        public static IAdminHandler CreateAdmin(string name)
        {
            if (AdminHandlers.TryGetValue(name, out var type))
            {
                return (IAdminHandler)Activator.CreateInstance(type);
            }
            return null;
        }

        public static IApiHandler CreateApi(string name)
        {
            if (ApiHandlers.TryGetValue(name, out var type))
            {
                return (IApiHandler)Activator.CreateInstance(type);
            }
            return null;
        }
    }
}

Example Handler: Auth API

using System;
using System.Web;
using Newtonsoft.Json;

namespace MyWebApp.Handlers.Api
{
    public class AuthApiHandler : IApiHandler
    {
        public string Execute(HttpContext context, RouteResult route, UserSession session)
        {
            string action = route.Action.ToLower();

            switch (action)
            {
                case "login":
                    return HandleLogin(context, route);

                case "logout":
                    return HandleLogout(context);

                case "check":
                    return HandleCheck(session);

                default:
                    return JsonConvert.SerializeObject(new { success = false, message = "Unknown action" });
            }
        }

        private string HandleLogin(HttpContext context, RouteResult route)
        {
            string username = route.Params.GetValueOrDefault("username", "");
            string password = route.Params.GetValueOrDefault("password", "");

            // Validate credentials (implement your logic)
            var user = ValidateCredentials(username, password);

            if (user == null)
            {
                return JsonConvert.SerializeObject(new { success = false, message = "Invalid credentials" });
            }

            // Create session
            var session = new UserSession
            {
                UserId = user.Id,
                Username = user.Username,
                IsAdmin = user.IsAdmin,
                LoginTime = DateTime.UtcNow
            };

            // Save to state server
            Global.SaveSession(context, session);

            return JsonConvert.SerializeObject(new
            {
                success = true,
                message = "Login successful",
                user = new { user.Id, user.Username, user.IsAdmin }
            });
        }

        private string HandleLogout(HttpContext context)
        {
            Global.ClearSession(context);

            return JsonConvert.SerializeObject(new { success = true, message = "Logged out" });
        }

        private string HandleCheck(UserSession session)
        {
            if (session == null)
            {
                return JsonConvert.SerializeObject(new { success = true, authenticated = false });
            }

            return JsonConvert.SerializeObject(new
            {
                success = true,
                authenticated = true,
                user = new { session.UserId, session.Username, session.IsAdmin }
            });
        }

        private User ValidateCredentials(string username, string password)
        {
            // Implement your authentication logic
            // Query database, verify password hash, etc.
            return null;
        }
    }

    public class User
    {
        public int Id { get; set; }
        public string Username { get; set; }
        public bool IsAdmin { get; set; }
    }
}

The Complete Stack

With this architecture, you have two WPA applications working together:

Production Deployment:

Web App 1: Main App (WPA)
========================================
- Port 80/443 (public)
- Zero ASPX file
- Application_BeginRequest handles all
- Custom routing, caching, templates

Web App 2: Custom Session State Server (WPA)
========================================
- Zero ASPX files
- Application_BeginRequest handles all
- Multi-tenant session storage
- Session persistence (optional)
  - MySQL (port 3306)
  - Content Storage

Both application use the same pattern.


Key Benefits

1. Zero Page Lifecycle Overhead

Traditional Web Forms Request

  • PreInit
  • Init
  • InitComplete
  • PreLoad
  • Load
  • LoadComplete
  • PreRender
  • PreRenderComplete
  • SaveStateComplete
  • Render

True Pageless Request

  • BeginRequest
  • Your Code
  • EndResponse()

No page instantiation, no control tree, no ViewState processing.

2. Earliest Possible Interception

You control the request from the very first event in the pipeline.
Nothing happens before Application_BeginRequest.

3. Custom Session Without Pipeline Dependency

ASP.NET Session requires waiting until AcquireRequestState.
Custom State Server is available immediately in BeginRequest.

4. Consistent Architecture

Unified Pattern

Main App

Global.asax.cs > Application_BeginRequest > Handlers

Session State Server

Global.asax.cs > Application_BeginRequest > Handlers


When to Use This Architecture

Use True Pageless when:

  • You want maximum control over the request pipeline
  • You need session data at the earliest point possible
  • You’re building a new project without legacy ASPX pages
  • You want architectural consistency across multiple apps
  • Performance is critical and you want to eliminate all overhead

Stick with Default.aspx entry point when:

  • You have existing ASPX pages to maintain alongside new code
  • You need ASP.NET Session (available at PostAcquireRequestState)
  • Team familiarity with Page_Load pattern is important
  • Debugging with page-based breakpoints is preferred

Conclusion

True Pageless Architecture takes ASP.NET Web Forms to its logical conclusion: if we’re not using server controls, ViewState, or PostBack, why use pages at all?

By intercepting requests at Application_BeginRequest and integrating a custom Session State Server built with the same principles, we create a fully consistent, high-performance architecture with zero ASPX files.

This isn’t about fighting the framework—it’s about recognizing that the HTTP processing engine underneath Web Forms is solid, and building directly on that foundation.

The result is a clean, fast, and fully transparent architecture where every request follows the same path: HTTP in, HTML/JSON out, nothing hidden.


Further Reading


This article demonstrates a proof of concept for True Pageless Architecture. The code examples provide a foundation that can be extended based on specific project requirements.