This is a complete architecture reference, guideline and code convention for building modern web applications on Pageless ASP.NET Web Forms without Master Page, ASPX page files, Server Controls, ViewState, or PostBack. It covers C# string-based HTML template rendering, API endpoint patterns, Fetch API integration, file uploads, Server-Sent Events, WebSocket, and background task management.
# Complete Architecture Reference for Pageless ASP.NET Web Forms in MD (Markdown) Format
## Overview
This project uses a **Pageless Architecture** — rendering full HTML pages entirely in C# using `StringBuilder` and `Response.Write`, intercepted at the `Global.asax.cs` pipeline. No `.aspx` markup files, no master pages, no ViewState, no server controls, no page lifecycle.
Every HTTP request flows through `Global.asax.cs`, where a `switch` statement routes the path to a static handler method. The handler builds the complete HTML document — from `<!DOCTYPE html>` to `</html>` — as a C# string and writes it directly to the response stream.
---
## Core Principle: NO Traditional WebForms Patterns
| ❌ AVOID | ✅ USE INSTEAD |
|----------|----------------|
| `.aspx` markup files | C# string-based HTML rendering |
| Master pages (`.master`) | `PageTemplate` class (C#) |
| `<asp:Button>`, `<asp:TextBox>` | Plain HTML: `<button>`, `<input>` |
| `OnClick="btnSave_Click"` | `onclick="saveItem()"` (JS function) |
| ViewState | Client-side state, re-fetch from API |
| Postback / `IsPostBack` | Fetch API calls |
| `UpdatePanel` / AJAX Toolkit | Native `fetch()` |
| Code-behind event handlers | API endpoint actions |
| Page lifecycle (`Page_Load`, etc.) | Pipeline interception at `Global.asax.cs` |
### ⚠️ CRITICAL: Button Type Declaration
```html
<!-- ❌ WRONG: Triggers postback (default type="submit") -->
<button onclick="saveItem()">Save</button>
<!-- ✅ CORRECT: Executes JavaScript only, no postback -->
<button type="button" onclick="saveItem()">Save</button>
```
**Always use `type="button"`** on all `<button>` elements. Without it, the browser defaults to `type="submit"`, which triggers a form postback and breaks the Fetch API pattern.
---
## Default JSON Library
Newtonsoft.JSON
```csharp
using Newtonsoft.Json;
Response.ContentType = "application/json";
Response.Write(JsonConvert.SerializeObject(obj));
```
JSON naming convention: direct matching of C# class fields or properties. Use default standard.
It can be PascalCase (`PropertyName`).
If the fields are primarily matching MySQL columns, use snake_case (`property_name`).
Never use CamelCase (`propertyName`).
---
## Data Model Class Convention
Database model classes are prefixed with `ob` (object). The preferred pattern uses **private fields** in snake_case (matching MySQL column names) with **public properties** in PascalCase (matching C# conventions):
```csharp
public class obBook
{
int id = 0;
string title = "";
string author = "";
int year = 0;
DateTime date_created = DateTime.MinValue;
DateTime date_modified = DateTime.MinValue;
public int Id { get { return id; } set { id = value; } }
public string Title { get { return title; } set { title = value; } }
public string Author { get { return author; } set { author = value; } }
public int Year { get { return year; } set { year = value; } }
public DateTime DateCreated { get { return date_created; } set { date_created = value; } }
public DateTime DateModified { get { return date_modified; } set { date_modified = value; } }
}
```
`MySqlExpress` maps MySQL columns to the private fields by matching snake_case names — no attribute mapping or naming configuration needed. C# code accesses the data through PascalCase public properties. Both layers work automatically with the same class.
---
## Pipeline Interception — Where Pageless Rendering Begins
In Pageless Architecture, requests are intercepted at `Global.asax.cs` pipeline events before any page handler is instantiated. There are two interception points:
| Entry Point | Session Available | Use Case |
|-------------|-------------------|----------|
| `Application_BeginRequest` | ❌ No | Stateless operations: APIs, webhooks, static content |
| `Application_PostAcquireRequestState` | ✅ Yes | Pages needing login status, user-specific content |
### Routing — Global.asax.cs
All routes are defined as a `switch` statement. This is the routing table for the entire application:
```csharp
public class Global : System.Web.HttpApplication
{
// Stateless routes (no session)
protected void Application_BeginRequest(object sender, EventArgs e)
{
string path = Request.Path.ToLower().Trim().TrimEnd('/');
switch (path)
{
case "/api-health":
RH.HealthApi.HandleRequest();
return;
}
}
// Session-aware routes
protected void Application_PostAcquireRequestState(object sender, EventArgs e)
{
if (HttpContext.Current?.Session != null)
AppSession.TryRestoreFromCookie();
string path = Request.Path.ToLower().Trim().TrimEnd('/');
switch (path)
{
// Page routes
case "/":
case "/home":
RH.HomePage.HandleRequest();
return;
case "/about":
RH.AboutPage.HandleRequest();
return;
case "/books":
RH.BookPage.HandleRequest();
return;
// API routes
case "/bookapi":
RH.BookPageApi.HandleRequest();
return;
}
}
}
```
Two routes per feature — one page, one API. Add more `case` entries as you add features. No route configuration files, no attribute routing — you look at the `switch` statement and see every URL the application responds to.
---
## The Two-Handler Pattern
Every feature follows this pattern:
| Handler | Purpose | Returns |
|---------|---------|---------|
| **Page Handler** (`BookPage`) | Renders the full HTML page | `text/html` — complete document |
| **API Handler** (`BookApi`) | Processes Fetch API calls | `application/json` or `text/html` fragment |
One page, one API. That's the entire architecture for any feature.
---
## File Structure Pattern
Since there are no `.aspx` files, all code lives in `.cs` class files:
```
/Global.asax.cs ← routing table
/App_Code/
/engine/
config.cs ← connection string, app settings
ApiHelper.cs ← shared response helpers
PageTemplate.cs ← shared HTML template (replaces master page)
/App_Code/
/engine/
/engine/ob
/engine/Models
obBook.cs ← data model
/App_Code/RH/
/engine/RH/
HomePage.cs ← page handler
AboutPage.cs ← page handler
BookPage.cs ← page handler
BookPageApi.cs ← API handler
/css/
site.css
/js/
site.js
books.js
```
---
## ApiHelper — Shared Response Utilities
Every handler uses `ApiHelper` for response writing and termination:
```csharp
using Newtonsoft.Json;
using System;
using System.Web;
namespace System
{
public static class ApiHelper
{
static HttpRequest Request
{
get
{
if (HttpContext.Current == null)
throw new InvalidOperationException("ApiHelper called outside of an HTTP request context.");
return HttpContext.Current.Request;
}
}
static HttpResponse Response
{
get
{
if (HttpContext.Current == null)
throw new InvalidOperationException("ApiHelper called outside of an HTTP request context.");
return HttpContext.Current.Response;
}
}
public static string GetBaseUrl()
{
Uri url = Request.Url;
return $"{url.Scheme}://{url.Host}{(url.IsDefaultPort ? "" : ":" + url.Port)}";
}
public static void EndResponse()
{
// So IIS will skip handling custom errors
Response.TrySkipIisCustomErrors = true;
try
{
Response.Flush();
}
catch { /* client already disconnected — ignore */ }
Response.SuppressContent = true;
// The most reliable way in WebForms / IIS-integrated pipeline
HttpContext.Current.ApplicationInstance.CompleteRequest();
}
public static void WriteJson(object obj)
{
Response.ContentType = "application/json";
Response.Write(JsonConvert.SerializeObject(obj));
}
public static void WriteSuccess(string message = "Success")
{
WriteJson(new { success = true, message });
}
public static void WriteError(string message, int statusCode = 400)
{
Response.StatusCode = statusCode;
WriteJson(new { success = false, message });
}
}
}
```
`EndResponse()` is the Pageless equivalent of `Response.End()` — but without the `ThreadAbortException`. It flushes buffered content, prevents additional output, and tells ASP.NET to skip to `EndRequest` cleanup.
---
## PageTemplate — Replaces the Master Page
The `PageTemplate` class generates the shared HTML shell — `<head>`, navigation, footer, scripts — that wraps every page. It serves the same role as a `.master` file, but as a plain C# class.
```csharp
using System;
using System.Collections.Generic;
using System.Text;
using System.Web;
namespace System.engine
{
public class PageTemplate
{
// --- Page Meta / SEO ---
string _title = "";
public string Title
{
get
{
if (!_title.Contains("My Website"))
{
return _title + " - My Website";
}
return _title;
}
set
{
_title = value;
}
}
public string Description = "Welcome to My Website.";
public string FaviconIco = "/favicon.ico";
public string Favicon32 = "/media/favicon-32x32.png";
public string Favicon16 = "/media/favicon-16x16.png";
public string AppleTouchIcon = "/media/favicon-180x180.png";
public string Manifest = "/media/site.webmanifest";
public string MsAppTileColor = "#E0F3EF";
public string MsAppTileImage = "/media/favicon-150x150.png";
public string ThemeColor = "#E0F3EF";
public string OgType = "website";
public string OgUrl = "https://mywebsite.com";
public string OgImage = "https://mywebsite.com/media/og-image.png";
public int OgImageWidth = 1200;
public int OgImageHeight = 630;
public string TwitterCard = "summary_large_image";
// --- CSS / Script Lists (path only, not full tag) ---
public List<string> lstTopCss = new List<string>();
public List<string> lstTopScript = new List<string>();
public List<string> lstBottomScript = new List<string>();
// --- Extra Raw HTML ---
public string ExtraHeaderText = "";
public string ExtraFooterText = "";
// ==============================
// GenerateHtmlHeader
// ==============================
public string GenerateHtmlHeader()
{
string encodedTitle = HttpUtility.HtmlEncode(Title);
string encodedDesc = HttpUtility.HtmlEncode(Description);
StringBuilder sb = new StringBuilder();
sb.Append($@"<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8' />
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
<title>{encodedTitle}</title>
<meta name='description' content='{encodedDesc}'>
<!-- Favicon -->
<link rel='icon' href='{FaviconIco}' sizes='48x48'>
<link rel='icon' type='image/png' sizes='32x32' href='{Favicon32}'>
<link rel='icon' type='image/png' sizes='16x16' href='{Favicon16}'>
<link rel='apple-touch-icon' sizes='180x180' href='{AppleTouchIcon}'>
<link rel='manifest' href='{Manifest}'>
<meta name='msapplication-TileColor' content='{MsAppTileColor}'>
<meta name='msapplication-TileImage' content='{MsAppTileImage}'>
<meta name='theme-color' content='{ThemeColor}'>
<!-- Open Graph / Facebook -->
<meta property='og:type' content='{OgType}'>
<meta property='og:url' content='{OgUrl}'>
<meta property='og:title' content='{encodedTitle}'>
<meta property='og:description' content='{encodedDesc}'>
<meta property='og:image' content='{OgImage}'>
<meta property='og:image:width' content='{OgImageWidth}'>
<meta property='og:image:height' content='{OgImageHeight}'>
<!-- Twitter -->
<meta name='twitter:card' content='{TwitterCard}'>
<meta name='twitter:url' content='{OgUrl}'>
<meta name='twitter:title' content='{encodedTitle}'>
<meta name='twitter:description' content='{encodedDesc}'>
<meta name='twitter:image' content='{OgImage}'>
<link rel='stylesheet' href='/css/site.css' />
");
// Top CSS
foreach (string css in lstTopCss)
{
sb.AppendLine($" <link rel='stylesheet' href='{css}' />");
}
// Top Script
foreach (string js in lstTopScript)
{
sb.AppendLine($" <script src='{js}'></script>");
}
// Extra header text (raw HTML)
if (ExtraHeaderText.Length > 0)
{
sb.AppendLine(ExtraHeaderText);
}
sb.Append(@"</head>
<body>
");
// --- Navigation Bar ---
sb.Append(RenderNavbar());
// --- Open Main Content Container ---
sb.Append(@" <main class='site-main'>
");
return sb.ToString();
}
// ==============================
// GenerateHtmlFooter
// ==============================
public string GenerateHtmlFooter()
{
StringBuilder sb = new StringBuilder();
sb.Append($@"
</main>
<footer class='site-footer'>
<div class='footer-inner'>
<p>© {DateTime.Now.Year} My Website</p>
<p>
<a href='/about'>About</a> -
<a href='/contact'>Contact</a>
</p>
</div>
</footer>
<script src='/js/site.js'></script>
");
// Bottom Script
foreach (string js in lstBottomScript)
{
sb.AppendLine($" <script src='{js}'></script>");
}
// Extra footer text (raw HTML)
if (ExtraFooterText.Length > 0)
{
sb.AppendLine(ExtraFooterText);
}
sb.Append(@"
</body>
</html>");
return sb.ToString();
}
// ==============================
// Navigation Bar
// ==============================
string RenderNavbar()
{
StringBuilder sb = new StringBuilder();
sb.Append(@" <header class='site-header'>
<div class='header-inner'>
<a href='/' class='site-logo'>
<img src='/media/logo-40x40.png' />
</a>
<a href='/' class='site-logo'>My Website</a>
<button class='nav-toggle' onclick='toggleNav()' aria-label='Menu'>
<span></span><span></span><span></span>
</button>
<nav class='site-nav' id='siteNav'>
<a href='/'>Home</a>
<a href='/about'>About</a>
<a href='/contact'>Contact</a>
");
// Session-aware content
if (HttpContext.Current.Session != null)
{
bool loggedIn = HttpContext.Current.Session["UserId"] != null;
if (loggedIn)
{
string username = HttpContext.Current.Session["Username"] + "";
sb.Append($" <a href='/u/{HttpUtility.HtmlAttributeEncode(username)}'>Profile</a>\n");
sb.Append(" <a href='/logout'>Logout</a>\n");
}
else
{
sb.Append(" <a href='/login'>Login</a>\n");
sb.Append(" <a href='/register'>Register</a>\n");
}
}
sb.Append(@" </nav>
</div>
</header>
<div class='nav-overlay' id='navOverlay' onclick='toggleNav()'></div>
");
return sb.ToString();
}
}
}
```
### Using PageTemplate in a Page Handler
Every page handler follows the same pattern — configure metadata, render begin, append page content, render end:
```csharp
public class HomePage
{
public static void HandleRequest()
{
HttpResponse Response = HttpContext.Current.Response;
StringBuilder sb = new StringBuilder();
PageTemplate pt = new PageTemplate()
{
Title = "Home",
Description = "Welcome to our website."
};
// Shared header + navbar + container open
sb.Append(pt.GenerateHtmlHeader());
// --- Page-specific content ---
sb.Append("<h1>Welcome</h1>");
sb.Append("<p>Browse our latest content below.</p>");
// --- End page-specific content ---
// Shared footer + scripts + close
sb.Append(pt.GenerateHtmlFooter());
Response.ContentType = "text/html; charset=utf-8";
Response.Write(sb.ToString());
ApiHelper.EndResponse();
}
}
```
### Adding Page-Specific CSS and JavaScript
Use `lstTopCss`, `lstTopScript`, and `lstBottomScript` to inject page-specific resources:
```csharp
PageTemplate pt = new PageTemplate()
{
Title = "Book Catalog",
Description = "Browse our collection of books."
};
pt.lstTopCss.Add("/css/books.css");
pt.lstBottomScript.Add("/js/books.js");
sb.Append(pt.GenerateHtmlHeader());
// ... page content ...
sb.Append("....");
sb.Append(pt.GenerateHtmlFooter());
```
### Equivalence to Master Page
| Master Page Concept | Pageless Equivalent |
|---------------------|---------------------|
| `.master` file | `PageTemplate` class |
| `<head>` section | `GenerateHtmlHeader()` |
| Navigation bar | `RenderNavbar()` |
| `<asp:ContentPlaceHolder>` | Gap between `GenerateHtmlHeader()` and `GenerateHtmlFooter()` |
| Footer + closing tags | `GenerateHtmlFooter()` |
| `ContentPlaceHolder` for head | `lstTopCss`, `lstTopScript`, `ExtraHeaderText` |
---
## Frontend Pattern — Fetch API
### Data Loading (GET)
```javascript
const API_URL = '/bookapi';
const response = await fetch(`${API_URL}?action=get_list&id=${id}`);
const data = await response.json();
if (data.success) {
// Render data.items to DOM
}
```
### Data Saving (POST with FormData)
```javascript
async function saveBook() {
var title = document.getElementById('book-title').value.trim();
var author = document.getElementById('book-author').value.trim();
var formData = new FormData();
formData.append('action', 'save-book');
formData.append('title', title);
formData.append('author', author);
try {
var response = await fetch(API_URL, {
method: 'POST',
// Important: Do NOT set Content-Type header when using FormData
// The browser will automatically set it to multipart/form-data with correct boundary
body: formData
});
if (!response.ok) {
throw new Error('Server responded with an error status');
}
var data = await response.json();
if (data.success) {
alert(data.message);
} else {
alert(data.message);
}
} catch (error) {
console.error('Fetch error:', error);
}
}
```
### File Upload (XMLHttpRequest with Progress)
```javascript
async function uploadFile() {
var fileInput = document.getElementById('fileUpload');
if (!fileInput.files.length) return;
var formData = new FormData();
formData.append('action', 'upload');
formData.append('file', fileInput.files[0]);
// Use XMLHttpRequest for progress tracking
var xhr = new XMLHttpRequest();
xhr.upload.onprogress = function (e) {
if (e.lengthComputable) {
var pct = Math.round((e.loaded / e.total) * 100);
document.getElementById('progress').textContent = pct + '%';
}
};
xhr.onload = function () {
var data = JSON.parse(xhr.responseText);
if (data.success) alert('Uploaded');
else alert(data.message);
};
xhr.open('POST', API_URL);
xhr.send(formData);
}
```
### HTML Escaping in JavaScript
```javascript
function escapeHtml(text) {
var div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
```
---
## Backend API Pattern
### API Handler Structure
```csharp
public class BookPageApi
{
public static void HandleRequest()
{
var Request = HttpContext.Current.Request;
string action = (Request["action"] + "").ToLower().Trim();
try
{
switch (action)
{
case "get-books-html": GetBooksHtml(); break;
case "get-books-json": GetBooksJson(); break;
case "get-book": GetBook(); break;
case "save-book": SaveBook(); break;
case "delete-book": DeleteBook(); break;
default: ApiHelper.WriteError($"Unknown action: {action}", 400); break;
}
}
catch (Exception ex)
{
ApiHelper.WriteError(ex.Message, 500);
}
ApiHelper.EndResponse();
}
}
```
### READ — Return HTML Fragment
The server pre-renders HTML. The frontend dumps it into a container with `innerHTML`:
**Backend:**
```csharp
static void GetBooksHtml()
{
HttpResponse Response = HttpContext.Current.Response;
StringBuilder sb = new StringBuilder();
List<obBook> lstBook = GetBooksFromDatabase();
foreach (var b in lstBook)
{
sb.Append($@"
<div class='card-book'>
<strong>{HttpUtility.HtmlEncode(b.Title)}</strong><br>
Author: {HttpUtility.HtmlEncode(b.Author)}<br>
Year: {b.Year}<br>
<button type='button' onclick='editBook({b.Id})'>Edit</button>
<button type='button' onclick='deleteBook({b.Id})'>Delete</button>
</div>");
}
Response.ContentType = "text/html; charset=utf-8";
Response.Write(sb.ToString());
}
```
**Frontend:**
```javascript
async function getAllBooksHtml() {
var formData = new FormData();
formData.append('action', 'get-books-html');
try {
var response = await fetch(API_URL, {
method: 'POST',
body: formData
});
var html = await response.text();
document.getElementById('div-my-books').innerHTML = html;
} catch (e) {
alert('Failed to load books');
}
}
```
### READ — Return JSON
The server returns data. The frontend renders it in JavaScript:
**Backend:**
```csharp
static void GetBooksJson()
{
List<obBook> lstBook = GetBooksFromDatabase();
ApiHelper.WriteJson(new
{
success = true,
message = "Success",
books = lstBook
});
}
```
**Frontend:**
```javascript
async function getAllBooksJson() {
var formData = new FormData();
formData.append('action', 'get-books-json');
try {
var response = await fetch(API_URL, {
method: 'POST',
body: formData
});
var data = await response.json();
if (!data.success) {
alert(data.message);
return;
}
var blocks = [];
for (var i = 0; i < data.books.length; i++) {
var b = data.books[i];
blocks.push(
"<div class='card-book'>" +
"<strong>" + escapeHtml(b.Title) + "</strong><br>" +
"Author: " + escapeHtml(b.Author) + "<br>" +
"Year: " + b.Year + "<br>" +
"<button type='button' onclick='editBook(" + b.Id + ")'>Edit</button> " +
"<button type='button' onclick='deleteBook(" + b.Id + ")'>Delete</button>" +
"</div>"
);
}
document.getElementById('div-my-books').innerHTML = blocks.join('');
} catch (e) {
alert('Failed to load books');
}
}
```
### CREATE + UPDATE (2-in-1): SaveBook
If `id` is empty or zero → INSERT (Create). If `id` has a value → UPDATE.
**Backend:**
```csharp
static void SaveBook()
{
HttpRequest Request = HttpContext.Current.Request;
int id = 0;
int.TryParse(Request.Form["id"] + "", out id);
string title = (Request.Form["title"] + "").Trim();
string author = (Request.Form["author"] + "").Trim();
int year = 0;
int.TryParse(Request.Form["year"] + "", out year);
// Validation
if (string.IsNullOrEmpty(title))
{
ApiHelper.WriteError("Title is required");
return;
}
if (string.IsNullOrEmpty(author))
{
ApiHelper.WriteError("Author is required");
return;
}
if (year < 1 || year > DateTime.Now.Year)
{
ApiHelper.WriteError("Please enter a valid year");
return;
}
using (MySqlConnection conn = new MySqlConnection(config.ConnString))
{
conn.Open();
using (MySqlCommand cmd = new MySqlCommand())
{
cmd.Connection = conn;
MySqlExpress m = new MySqlExpress(cmd);
Dictionary<string, object> dic = new Dictionary<string, object>();
dic["title"] = title;
dic["author"] = author;
dic["year"] = year;
if (id <= 0)
{
// CREATE — no id, insert new row
m.Insert("books", dic);
ApiHelper.WriteSuccess("Book added");
}
else
{
// UPDATE — has id, update existing row
m.Update("books", dic, "id", id);
ApiHelper.WriteSuccess("Book updated");
}
}
}
}
```
**Frontend:**
```javascript
async function saveBook() {
var id = document.getElementById('book-id').value.trim();
var title = document.getElementById('book-title').value.trim();
var author = document.getElementById('book-author').value.trim();
var year = document.getElementById('book-year').value.trim();
var formData = new FormData();
formData.append('action', 'save-book');
formData.append('id', id); // empty = create, has value = update
formData.append('title', title);
formData.append('author', author);
formData.append('year', year);
try {
var response = await fetch(API_URL, {
method: 'POST',
body: formData
});
var data = await response.json();
if (data.success) {
alert(data.message);
clearForm();
getAllBooksHtml(); // Refresh the list
} else {
alert(data.message);
}
} catch (e) {
alert('Something went wrong. Please try again.');
}
}
```
### DELETE
**Backend:**
```csharp
static void DeleteBook()
{
HttpRequest Request = HttpContext.Current.Request;
int id = 0;
int.TryParse(Request.Form["id"] + "", out id);
if (id <= 0)
{
ApiHelper.WriteError("Invalid book ID");
return;
}
using (MySqlConnection conn = new MySqlConnection(config.ConnString))
{
conn.Open();
using (MySqlCommand cmd = new MySqlCommand())
{
cmd.Connection = conn;
MySqlExpress m = new MySqlExpress(cmd);
var p = new Dictionary<string, object>();
p["@id"] = id;
m.Execute("DELETE FROM books WHERE id = @id;", p);
}
}
ApiHelper.WriteSuccess("Book deleted");
}
```
**Frontend:**
```javascript
async function deleteBook(id) {
if (!id) {
id = document.getElementById('book-id').value.trim();
}
if (!id) {
alert('No book selected');
return;
}
if (!confirm('Delete this book?')) return;
var formData = new FormData();
formData.append('action', 'delete-book');
formData.append('id', id);
try {
var response = await fetch(API_URL, {
method: 'POST',
body: formData
});
var data = await response.json();
if (data.success) {
alert(data.message);
clearForm();
getAllBooksHtml();
} else {
alert(data.message);
}
} catch (e) {
alert('Something went wrong. Please try again.');
}
}
```
---
## API Response Format
All API responses follow this JSON structure:
```javascript
// Success
{ "success": true, "message": "..." }
// Success with data
{ "success": true, "message": "...", "data": {...} }
// Success with list
{ "success": true, "items": [...] }
// Error
{ "success": false, "message": "Error description" }
```
---
## File Upload Pattern
**Frontend:**
```javascript
async function uploadFile() {
var fileInput = document.getElementById('fileUpload');
if (!fileInput.files.length) return;
var formData = new FormData();
formData.append('action', 'upload');
formData.append('parent_id', parentId);
formData.append('file', fileInput.files[0]);
var xhr = new XMLHttpRequest();
xhr.upload.onprogress = function (e) {
if (e.lengthComputable) {
var pct = Math.round((e.loaded / e.total) * 100);
document.getElementById('progress').textContent = pct + '%';
}
};
xhr.onload = function () {
var data = JSON.parse(xhr.responseText);
if (data.success) alert('Uploaded');
else alert(data.message);
};
xhr.open('POST', API_URL);
xhr.send(formData);
}
```
**Backend:**
```csharp
static void UploadFile()
{
HttpRequest Request = HttpContext.Current.Request;
if (Request.Files.Count == 0)
{
ApiHelper.WriteError("No file uploaded");
return;
}
var uploadedFiles = new List<object>();
for (int i = 0; i < Request.Files.Count; i++)
{
HttpPostedFile file = Request.Files[i];
if (file.ContentLength == 0)
continue;
string fileName = Path.GetFileName(file.FileName);
string savePath = HttpContext.Current.Server.MapPath("~/uploads/" + fileName);
try
{
file.SaveAs(savePath);
uploadedFiles.Add(new
{
success = true,
fileName = fileName,
filePath = "/uploads/" + fileName
});
}
catch (Exception ex)
{
uploadedFiles.Add(new
{
success = false,
fileName = fileName,
message = ex.Message
});
}
}
ApiHelper.WriteJson(uploadedFiles);
}
```
---
## Fire-and-Forget Background Task
For non-blocking background work that doesn't need to return results to frontend:
```csharp
_ = Task.Run(() => DoWork(taskId)); // Fire-and-forget, returns immediately
```
---
## Real-Time Communication (WebSocket & SSE)
For long-running tasks that require progress reporting, use **Server-Sent Events (SSE)** or **WebSocket** connections instead of polling.
### Communication Methods
| Method | Direction | Use Case |
|--------|-----------|----------|
| **HTTP/Fetch API** | Request → Response | CRUD operations, start/stop tasks |
| **WebSocket** | Bi-directional | Real-time chat, interactive updates |
| **Server-Sent Events (SSE)** | Server → Client only | Progress reporting, status updates |
**Default to SSE** for progress reporting. Use WebSocket only when bi-directional communication is required.
### Architecture: Hybrid HTTP + Streaming
```
┌─────────────┐ HTTP POST (start_task) ┌─────────────┐
│ Frontend │ ─────────────────────────────▶ │ Backend │
│ │ { taskId: 123 } │ API │
│ │ ◀───────────────────────────── │ │
│ │ │ │
│ │ SSE/WebSocket Connection │ │
│ │ ◀════════════════════════════ │ │
│ │ { progress: 50%, ... } │ │
└─────────────┘ └─────────────┘
```
### TaskInfo Class
```csharp
class TaskInfo
{
public int TaskId { get; set; }
public int PercentComplete { get; set; } = 0;
public string Status { get; set; } = "Running";
public bool IsCompleted { get; set; } = false;
public bool HasError { get; set; } = false;
public string ErrorMessage { get; set; } = "";
public bool RequestCancel { get; set; } = false;
public bool IsCancelled { get; set; } = false;
}
```
### Server-Sent Events (SSE) — Recommended
In Pageless Architecture, SSE is handled within the same API handler by checking the request headers:
**Backend:**
```csharp
public class TaskApi
{
static ConcurrentDictionary<int, TaskInfo> dicTaskInfo
= new ConcurrentDictionary<int, TaskInfo>();
public static void HandleRequest()
{
var Request = HttpContext.Current.Request;
// 1. Check for SSE request FIRST
if (Request.Headers["Accept"] == "text/event-stream"
|| Request["stream"] == "true")
{
HandleSSERequest();
return;
}
// 2. Normal HTTP API handling
string action = (Request["action"] + "").ToLower().Trim();
try
{
switch (action)
{
case "start-task": StartTask(); break;
case "stop-task": StopTask(); break;
default: ApiHelper.WriteError("Unknown action", 400); break;
}
}
catch (Exception ex) { ApiHelper.WriteError(ex.Message, 500); }
ApiHelper.EndResponse();
}
static void HandleSSERequest()
{
var Request = HttpContext.Current.Request;
var Response = HttpContext.Current.Response;
if (!int.TryParse(Request["task_id"] + "", out int taskId))
{
Response.StatusCode = 400;
ApiHelper.EndResponse();
return;
}
// SSE Headers
Response.ContentType = "text/event-stream";
Response.CacheControl = "no-cache";
Response.AddHeader("Connection", "keep-alive");
Response.Buffer = false;
try
{
SendSSEEvent("connected", $"Subscribed to task {taskId}");
while (Response.IsClientConnected)
{
if (dicTaskInfo.TryGetValue(taskId, out var taskInfo))
{
SendSSEEvent("progress",
JsonConvert.SerializeObject(taskInfo));
if (taskInfo.IsCompleted)
{
SendSSEEvent("completed",
JsonConvert.SerializeObject(taskInfo));
break;
}
}
Thread.Sleep(250);
}
}
catch (HttpException) { /* Client disconnected */ }
finally { ApiHelper.EndResponse(); }
}
static void SendSSEEvent(string eventType, string data)
{
var Response = HttpContext.Current.Response;
if (!Response.IsClientConnected) return;
Response.Write($"event: {eventType}\ndata: {data}\n\n");
Response.Flush();
}
static void StartTask()
{
int taskId = GetNewTaskId();
var taskInfo = new TaskInfo { TaskId = taskId };
dicTaskInfo[taskId] = taskInfo;
_ = Task.Run(() => DoWork(taskId));
ApiHelper.WriteJson(new { success = true, taskId });
}
static void StopTask()
{
var Request = HttpContext.Current.Request;
int taskId = 0;
int.TryParse(Request["task_id"] + "", out taskId);
if (dicTaskInfo.TryGetValue(taskId, out var taskInfo))
{
taskInfo.RequestCancel = true;
ApiHelper.WriteSuccess("Stop requested");
}
else
{
ApiHelper.WriteError("Task not found");
}
}
static void DoWork(int taskId)
{
if (!dicTaskInfo.TryGetValue(taskId, out var taskInfo)) return;
try
{
for (int i = 0; i <= 100; i += 10)
{
if (taskInfo.RequestCancel)
{
taskInfo.IsCancelled = true;
break;
}
taskInfo.PercentComplete = i;
Thread.Sleep(500); // Simulate work
}
}
catch (Exception ex)
{
taskInfo.HasError = true;
taskInfo.ErrorMessage = ex.Message;
}
taskInfo.IsCompleted = true;
}
}
```
**Frontend:**
```javascript
const API_URL = '/taskapi';
let eventSource = null;
async function startTask() {
var formData = new FormData();
formData.append('action', 'start-task');
try {
var response = await fetch(API_URL, {
method: 'POST',
body: formData
});
var data = await response.json();
if (data.success) {
connectSSE(data.taskId);
}
} catch (error) {
console.error('Error starting task:', error);
}
}
function connectSSE(taskId) {
if (eventSource) return;
eventSource = new EventSource(`${API_URL}?stream=true&task_id=${taskId}`);
eventSource.addEventListener('connected', function (e) {
console.log('SSE connected:', e.data);
});
eventSource.addEventListener('progress', function (e) {
var data = JSON.parse(e.data);
updateProgress(data.PercentComplete, data.Status);
});
eventSource.addEventListener('completed', function (e) {
var data = JSON.parse(e.data);
updateProgress(100, 'Completed');
closeSSE();
});
eventSource.onerror = function () {
console.error('SSE error');
closeSSE();
};
}
function closeSSE() {
if (eventSource) {
eventSource.close();
eventSource = null;
}
}
window.addEventListener('beforeunload', closeSSE);
```
### WebSocket — For Bi-Directional Communication
Use WebSocket when the client needs to send messages to the server during an active connection.
**Backend:**
```csharp
public class ChatApi
{
public static void HandleRequest()
{
// 1. Check for WebSocket request FIRST
if (HttpContext.Current.IsWebSocketRequest)
{
HttpContext.Current.AcceptWebSocketRequest(HandleWebSocket);
return;
}
// 2. Normal HTTP API handling
// ... same pattern as above
}
static async Task HandleWebSocket(AspNetWebSocketContext context)
{
WebSocket webSocket = context.WebSocket;
byte[] buffer = new byte[1024];
while (webSocket.State == WebSocketState.Open)
{
var result = await webSocket.ReceiveAsync(
new ArraySegment<byte>(buffer), CancellationToken.None);
if (result.MessageType == WebSocketMessageType.Text)
{
string message = Encoding.UTF8.GetString(buffer, 0, result.Count);
// Process message and send response
string reply = ProcessMessage(message);
byte[] replyBytes = Encoding.UTF8.GetBytes(reply);
await webSocket.SendAsync(
new ArraySegment<byte>(replyBytes),
WebSocketMessageType.Text, true, CancellationToken.None);
}
else if (result.MessageType == WebSocketMessageType.Close)
{
await webSocket.CloseAsync(
WebSocketCloseStatus.NormalClosure,
"", CancellationToken.None);
break;
}
}
}
}
```
**Frontend:**
```javascript
let webSocket = null;
function connectWebSocket(taskId) {
if (webSocket) return;
var protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
var wsUrl = protocol + '//' + location.host + API_URL;
webSocket = new WebSocket(wsUrl);
webSocket.onopen = function () {
webSocket.send('taskid:' + taskId);
};
webSocket.onmessage = function (event) {
var data = JSON.parse(event.data);
updateProgress(data.PercentComplete, data.Status);
if (data.IsCompleted) {
webSocket.close(1000, 'Task completed');
}
};
webSocket.onclose = function () { webSocket = null; };
webSocket.onerror = function (err) { console.error('WebSocket error:', err); };
}
```
### SSE vs WebSocket Decision Matrix
| Criteria | SSE | WebSocket |
|----------|-----|-----------|
| Progress reporting | ✅ Best choice | Works but overkill |
| Server → Client only | ✅ Best choice | Unnecessary complexity |
| Bi-directional chat | ✗ Not supported | ✅ Best choice |
| Automatic reconnection | ✅ Built-in | Manual implementation |
| Implementation complexity | Simple | More complex |
**Default to SSE** for progress reporting. Use WebSocket only when the client must send messages during an active connection. Or use normal Fetch API POST to send messages and SSE to receive updates.
---
## The Rendering Pipeline at a Glance
```
HTTP Request arrives at IIS
│
▼
IIS parses raw TCP bytes into HttpContext.Current.Request
│
▼
ASP.NET pipeline begins
│
├──► Application_BeginRequest
│ └── Stateless API routes handled here (no session)
│
├──► AuthenticateRequest, AuthorizeRequest, etc.
│
├──► AcquireRequestState
│ └── SessionStateModule loads session data
│
├──► Application_PostAcquireRequestState
│ └── Session-aware page routes handled here
│ │
│ ▼
│ PageTemplate generates <head> + navbar
│ │
│ ▼
│ Handler method builds page-specific content
│ via StringBuilder (database queries, loops, conditionals)
│ │
│ ▼
│ PageTemplate generates footer + scripts + closing tags
│ │
│ ▼
│ Response.Write(sb.ToString())
│ │
│ ▼
│ ApiHelper.EndResponse()
│ └── CompleteRequest() → skip to EndRequest
│
├──► [Page handler execution — SKIPPED]
│
├──► EndRequest (cleanup)
│
▼
HTTP Response sent to browser
```
The page handler execution step — where conventional Web Forms would instantiate a `System.Web.UI.Page` subclass, build the control tree, process ViewState, and run the full page lifecycle — is entirely skipped. The request goes from "session available" to "response sent" in a single method call.
Photo by zhang kaiyv on Unsplash
