See how four routes, zero ASPX files, and pure HTTP handling come together in under 100 lines of code
Introduction
In my previous article on [True Pageless Architecture], I introduced the concept of building ASP.NET Web Forms applications with zero ASPX files—intercepting all requests at Application_BeginRequest in Global.asax.cs.
Theory is great, but seeing it work is better.
This article provides a minimal, working demonstration. We’ll build four routes from scratch, each showcasing a key concept:
| Route | Demonstrates |
|---|---|
/home | Basic HTML response |
/about | Template with placeholder replacement |
/time-now | Frontend page with Fetch API |
/api/time | JSON API endpoint |
No ASPX files. No page lifecycle. Just HTTP in, HTML/JSON out.
No Route Table Required
Unlike ASP.NET MVC or Web API where you configure routes in RouteConfig.cs or use attribute routing, True Pageless Architecture needs no route registration at all.
Every request flows through a single entry point: Application_BeginRequest. You match paths with simple string comparison. That’s it.
protected void Application_BeginRequest(object sender, EventArgs e)
{
string path = Request.Path.ToLower();
if (path == "/home") { /* handle it */ }
if (path == "/about") { /* handle it */ }
if (path == "/api/time") { /* handle it */ }
// Unmatched paths fall through to IIS
}No routing middleware. No attribute decorators. No convention-based magic. You see exactly what happens for each path.
Stage 1: Static HTML Response
Goal: Return a complete HTML page for /home
This is the simplest possible case—receive a request, write HTML, end the response.
Example URL:
https://mywebsite.com/homeGlobal.asax
<%@ Application Language="C#" CodeBehind="Global.asax.cs" Inherits="MyDemo.Global" %>Global.asax.cs
using System;
using System.Web;
namespace MyDemo
{
public class Global : HttpApplication
{
protected void Application_BeginRequest(object sender, EventArgs e)
{
string path = Request.Path.ToLower();
if (path == "/home")
{
Response.ContentType = "text/html";
Response.Write(@"
<!DOCTYPE html>
<html>
<head>
<title>Home</title>
</head>
<body>
<h1>Home</h1>
</body>
</html>");
EndResponse();
}
}
void EndResponse()
{
Response.Flush();
Response.SuppressContent = true;
HttpContext.Current.ApplicationInstance.CompleteRequest();
}
}
}What happens:
- Browser requests
/home Application_BeginRequestfires (the first event in the pipeline)- We check the path, write HTML, and end the response
- No page instantiation, no control tree, no ViewState
Stage 2: Template with Placeholder
Goal: Return HTML for /about with dynamic content replacement
Real pages need dynamic data. Here we demonstrate the simplest templating pattern—string replacement.
Example URL:
https://mywebsite.com/about?userid=5if (path == "/about")
{
int userid = 0;
int.TryParse(Request["userid"], out userid);
User user = Db.GetUser(userid);
string html = @"
<!DOCTYPE html>
<html>
<head>
<title>About</title>
</head>
<body>
<h1>About</h1>
<p>Hello, {{username}}!</p>
</body>
</html>";
html = html.Replace("{{username}}", HttpUtility.HtmlEncode(user.Username));
Response.ContentType = "text/html";
Response.Write(html);
EndResponse();
}Supporting classes:
public class User
{
public int Id { get; set; }
public string Username { get; set; }
}
public static class Db
{
public static User GetUser(int userid)
{
// In production: query your database
// Demo: return mock data
if (userid <= 0)
return new User { Id = 0, Username = "Guest" };
return new User { Id = userid, Username = "John" };
}
}What this demonstrates:
- Reading query parameters (
Request["userid"]) - Simple
{{placeholder}}template pattern - HTML encoding for security
- The pattern scales to full template files loaded from disk
Stage 3: Frontend with Fetch API
Goal: Return an HTML page for /time-now that calls an API endpoint
Modern web apps separate presentation from data. The page loads static HTML with JavaScript, then fetches data from an API.
Example URL:
https://mywebsite.com/time-nowif (path == "/time-now")
{
Response.ContentType = "text/html";
Response.Write(@"
<!DOCTYPE html>
<html>
<head>
<title>Time Now</title>
</head>
<body>
<h1>Time Now</h1>
<p>Server time: <span id=""span_timenow"">--</span></p>
<button type=""button"" onclick=""getTime()"">Get Server Time</button>
<script>
async function getTime() {
const response = await fetch('/api/time');
const data = await response.json();
document.getElementById('span_timenow').textContent = data.time;
}
</script>
</body>
</html>");
EndResponse();
}Key point: The button uses type="button". Without this attribute, browsers default to type="submit", which would trigger a form postback—exactly what we’re avoiding.
Stage 4: API Endpoint
Goal: Handle /api/time and return JSON
The API endpoint returns data, not HTML. Same pattern, different content type.
Example URL:
https://mywebsite.com/api/timeif (path == "/api/time")
{
string json = "{\"time\":\"" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + "\"}";
Response.ContentType = "application/json";
Response.Write(json);
EndResponse();
}
// Output:
// {"time":"2026-01-05 09:36:00"}What happens:
- Button click triggers
getTime()JavaScript function fetch('/api/time')sends request to serverApplication_BeginRequesthandles it, returns JSON- JavaScript updates the DOM with the response
No postback. No page reload. Pure HTTP.
Why Not Response.End()?
You might wonder why we use this pattern:
void EndResponse()
{
Response.Flush();
Response.SuppressContent = true;
HttpContext.Current.ApplicationInstance.CompleteRequest();
}Instead of simply:
Response.End();The problem with Response.End():
Response.End() throws a ThreadAbortException to immediately stop execution. While this works, it has consequences:
- Performance overhead — Exception handling is expensive
- Unpredictable cleanup —
finallyblocks andusingstatements may not execute as expected - ASP.NET itself recommends against it — Microsoft’s documentation suggests
CompleteRequest()as the alternative
What our pattern does:
Response.Flush()— Sends all buffered output to the client immediatelyResponse.SuppressContent = true— Prevents any further content from being writtenCompleteRequest()— Signals ASP.NET to skip remaining pipeline events and go directly toEndRequest
The result is the same—the response ends—but without throwing an exception. Clean, predictable, and efficient.
Complete Code
Here’s everything combined into a single, working Global.asax.cs. Notice how the routing structure is immediately visible—each handler is a single method call:
using System;
using System.Web;
namespace MyDemo
{
public class Global : HttpApplication
{
protected void Application_BeginRequest(object sender, EventArgs e)
{
string path = Request.Path.ToLower();
// ══════════════════════════════════════════════
// Route: /home
// Static HTML response
// ══════════════════════════════════════════════
if (path == "/home")
{
HandleHome();
EndResponse();
return;
}
// ══════════════════════════════════════════════
// Route: /about
// Template with placeholder replacement
// ══════════════════════════════════════════════
if (path == "/about")
{
HandleAbout();
EndResponse();
return;
}
// ══════════════════════════════════════════════
// Route: /time-now
// Frontend with Fetch API integration
// ══════════════════════════════════════════════
if (path == "/time-now")
{
HandleTimeNow();
EndResponse();
return;
}
// ══════════════════════════════════════════════
// Route: /api/time
// JSON API endpoint
// ══════════════════════════════════════════════
if (path == "/api/time")
{
HandleApiTime();
EndResponse();
return;
}
// Unhandled routes fall through to IIS (404, static files, etc.)
}
// ══════════════════════════════════════════════
// Route Handlers
// ══════════════════════════════════════════════
void HandleHome()
{
Response.ContentType = "text/html";
Response.Write(@"
<!DOCTYPE html>
<html>
<head>
<title>Home</title>
</head>
<body>
<h1>Home</h1>
</body>
</html>");
}
void HandleAbout()
{
int userid = 0;
int.TryParse(Request["userid"], out userid);
User user = Db.GetUser(userid);
string html = @"
<!DOCTYPE html>
<html>
<head>
<title>About</title>
</head>
<body>
<h1>About</h1>
<p>Hello, {{username}}!</p>
</body>
</html>";
html = html.Replace("{{username}}", HttpUtility.HtmlEncode(user.Username));
Response.ContentType = "text/html";
Response.Write(html);
}
void HandleTimeNow()
{
Response.ContentType = "text/html";
Response.Write(@"
<!DOCTYPE html>
<html>
<head>
<title>Time Now</title>
</head>
<body>
<h1>Time Now</h1>
<p>Server time: <span id=""span_timenow"">--</span></p>
<button type=""button"" onclick=""getTime()"">Get Server Time</button>
<script>
async function getTime() {
const response = await fetch('/api/time');
const data = await response.json();
document.getElementById('span_timenow').textContent = data.time;
}
</script>
</body>
</html>");
}
void HandleApiTime()
{
// Output: {"time":"2026-01-05 09:36:00"}
string json = "{\"time\":\"" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + "\"}";
Response.ContentType = "application/json";
Response.Write(json);
}
// ══════════════════════════════════════════════
// Response Helper
// ══════════════════════════════════════════════
void EndResponse()
{
Response.Flush();
Response.SuppressContent = true;
HttpContext.Current.ApplicationInstance.CompleteRequest();
}
}
// ══════════════════════════════════════════════
// Supporting Classes
// ══════════════════════════════════════════════
public class User
{
public int Id { get; set; }
public string Username { get; set; }
}
public static class Db
{
public static User GetUser(int userid)
{
// Production: query database
// Demo: return mock data
if (userid <= 0)
return new User { Id = 0, Username = "Guest" };
return new User { Id = userid, Username = "John" };
}
}
}Project Structure
MyDemo/
├── Global.asax ← One line: points to Global.asax.cs
├── Global.asax.cs ← ALL application logic (shown above)
└── Web.config ← Standard configurationThat’s it. No ASPX files. No code-behind pages. No designer files.
What You’ve Seen
| Concept | Demonstrated In |
|---|---|
Request interception at BeginRequest | All routes |
| No route table configuration | Application_BeginRequest |
Direct Response.Write output | All routes |
| Clean response termination | EndResponse() method |
| Template placeholder replacement | /about |
| Query parameter reading | /about |
| Fetch API integration | /time-now |
| JSON API endpoint | /api/time |
Next Steps
This demo shows the core pattern. Production applications would add:
- Route resolution — Map paths to handler classes
- Template loading — Read HTML from files, not inline strings
- Session management — Custom state server or cookie-based
- Error handling — Try-catch with proper HTTP status codes
- Static file passthrough — Let IIS serve CSS, JS, images
See the full True Pageless Architecture article for complete implementation details.
Conclusion
True Pageless Architecture isn’t theoretical—it’s a working pattern you can implement today. Four routes, zero ASPX files, under 100 lines of code.
The ASP.NET pipeline is solid infrastructure. By intercepting requests at Application_BeginRequest, we use that infrastructure directly without the overhead of pages, controls, and ViewState.
HTTP in. HTML/JSON out. Nothing hidden.
