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

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

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:
BeginRequest– We intercept here (Session NOT available)AuthenticationRequestPostAuthenticateRequestAuthorizeRequestPostAuthorizeRequestResolveRequestCachePostResolveRequestCacheMapRequestHandlerPostMapRequestHandlerAcquireRequestState– Session loads herePostAcquireRequestState– Session available from herePreRequestHandlerExecute- [Handler Executes] – Default.aspx runs here
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
PreInitInitInitCompletePreLoadLoadLoadCompletePreRenderPreRenderCompleteSaveStateCompleteRender
True Pageless Request
BeginRequestYour CodeEndResponse()
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
- Introducing ASP.NET Web Forms Pageless Architecture (WPA) – Complete WPA implementation with Default.aspx entry point
- Building Your Own Session State Server with ASP.NET Web Forms WPA Architecture – Full session state server implementation and client library
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.
