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
- 1 Security check (local only for admin/api)
- 2 Load session from State Server
- 3 Route resolution
- 4 Handler dispatch
- 5 Response.End()
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.
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.
Project Structure
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.End();
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.End();
}
#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:
- Zero ASPX files
- Application_BeginRequest handles all
- Custom routing, caching, templates
- Zero ASPX files
- Application_BeginRequest handles all
- Multi-tenant session storage
- Session persistence (optional)
- Content storage
Key Benefits
1. Zero Page Lifecycle Overhead
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
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.
