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.
---
## HTTP Errors Pass-Through
Pageless Architecture handles all requests through `Global.asax.cs` rather than physical `.aspx` files. By default, IIS intercepts responses with error status codes (404, 403, 500, etc.) and replaces them with its own error pages — overriding whatever your handler produced.
To prevent this, add the following to `web.config`:
```xml
<system.webServer>
<httpErrors existingResponse="PassThrough" />
</system.webServer>
```
This tells IIS to leave the application's response untouched, allowing code behind handler to remain the single source of truth for all output — including error pages.
---
## 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
<!-- Triggers postback (default type="submit") -->
<button onclick="saveItem()">Save</button>
<!-- Executes JavaScript only, no postback -->
<button type="button" onclick="saveItem()">Save</button>
```
---
## 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, every request is intercepted in `Application_BeginRequest` and routed by a single `switch` statement. The built-in ASP.NET session module is bypassed entirely — session state is provided by a custom in-process store (see [Custom Session State](#custom-session-state) below) which is available immediately at `BeginRequest`, so there is no need to wait for `AcquireRequestState` or `PostAcquireRequestState`.
| Entry Point | Role |
|-------------|------|
| `Application_Start` | One-time init: connection string, DB migration, start `SessionSweeper` background task |
| `Application_BeginRequest` | The single routing point. Custom session is restored here via `AppSession.TryRestoreFromCookie()` before the route switch dispatches to a handler |
### 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
{
protected void Application_BeginRequest(object sender, EventArgs e)
{
string path = Request.Path.ToLower().Trim().TrimEnd('/');
switch (path)
{
case "/":
case "/home":
RH.HomePage.HandleRequest();
return;
case "/api-health":
RH.HealthApi.HandleRequest();
return;
case "/about":
RH.AboutPage.HandleRequest();
return;
case "/books":
RH.BookPage.HandleRequest();
return;
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.
---
## Custom Session State
The built-in ASP.NET `SessionStateModule` is **disabled**. All session state is held in a single application-wide `public static ConcurrentDictionary` keyed by a random session id stored in an `ssid` cookie. This makes session data available the instant a request enters `Application_BeginRequest`. IIS/ASP.NET built-in Session State infrastructure is not reliable in pageless mode, do not wait for `AcquireRequestState`, no `EnableSessionState="true"` on handlers, no `IRequiresSessionState` marker interface.
### Why custom
| Built-in ASP.NET Session | Custom `SessionStore` |
|---|---|
| Locked behind `AcquireRequestState` — forces routing into `PostAcquireRequestState` | Available at `BeginRequest` — single routing point |
| Per-request reader/writer lock serializes async handlers | Lock-free `ConcurrentDictionary` |
| Hard to inspect, debug, or sweep | Plain dictionary — enumerable, sweepable, easy to log |
| Tied to InProc / StateServer / SQL provider | Trivially swappable for any backing store |
### Three-layer model
Session lives in three places, in order of speed:
```
cookies (ssid + lsid) ──► ConcurrentDictionary (RAM) ──► login_sessions table (DB)
▲ ▲ │
└──────────────────────────────┴──────────────────────────────┘
rehydrate on cookie hit
```
1. **`ssid` cookie** — a random 48-char id, the lookup key into the in-memory dictionary. Lifespan: in-memory only (lost on app-pool recycle).
2. **`SessionStore.Sessions`** — `public static ConcurrentDictionary<string, StateObject>`. Each `StateObject` wraps another `ConcurrentDictionary<string, object>` for arbitrary per-user data plus a `LastAccessUtc` timestamp.
3. **`login_sessions` DB table** — persistent "Remember Me" record holding `(user_id, token, date_expiry, cookie_persistent)`. Referenced by an `lsid` cookie. Survives app restarts.
### Activation — first request
```csharp
// SessionStore.Current — called transparently on first read/write
public static StateObject Current
{
get
{
HttpContext ctx = HttpContext.Current;
string sid = ctx.Request.Cookies[CookieName]?.Value;
StateObject state;
if (string.IsNullOrEmpty(sid) || !Sessions.TryGetValue(sid, out state))
{
sid = NewId(); // 48-char random hex
state = new StateObject();
Sessions[sid] = state; // <-- ConcurrentDictionary
ctx.Response.Cookies.Set(new HttpCookie(CookieName, sid)
{
HttpOnly = true,
Secure = ctx.Request.IsSecureConnection,
SameSite = SameSiteMode.Lax
});
}
state.LastAccessUtc = DateTime.UtcNow;
return state;
}
}
```
### Resuming session — DB → ConcurrentDictionary → cookie
When the in-memory entry is gone (app-pool recycle, idle sweep, cold machine) but the user still holds a valid `lsid` cookie, the next request walks back up the chain:
```csharp
// Called once per request from Application_BeginRequest
public static void TryRestoreFromCookie()
{
if (IsLoggedIn) return; // already in ConcurrentDictionary
obUser user = UserSession.TryRestoreFromCookie(); // ── reads "lsid" cookie
// ── SELECT … FROM login_sessions
// JOIN users WHERE token=@t AND not expired
if (user != null)
LoginUser = user; // ── writes user back into StateObject
// (lives in the ConcurrentDictionary)
}
```
Flow: **`lsid` cookie → DB lookup → `obUser` materialized → stored into the per-session `StateObject` inside the `ConcurrentDictionary` → subsequent requests in the same app-pool lifetime hit RAM directly.** The cookie's expiry is rolled forward only when remaining lifetime drops below 1/12 of the original window, keeping DB writes infrequent.
### Accessing session from handlers
`AppSession` is a thin static facade — handlers never touch `HttpContext.Session`:
```csharp
public static class AppSession
{
public static obUser LoginUser
{
get { return SessionStore.Current?[AppSessionKeys.LoginUser] as obUser; }
set { var s = SessionStore.Current; if (s != null) s[AppSessionKeys.LoginUser] = value; }
}
public static bool IsLoggedIn => LoginUser != null;
}
```
Inside any page or API handler:
```csharp
if (!AppSession.IsLoggedIn) { Response.Redirect("/login"); return; }
obUser me = AppSession.LoginUser;
```
### Lifecycle
- **Logout** — `SessionStore.Abandon()` removes the entry from the dictionary and expires the `ssid` cookie; `UserSession.DeletePersistentSession()` deletes the `login_sessions` row and expires the `lsid` cookie.
- **Idle cleanup** — `SessionSweeper` runs hourly via `HostingEnvironment.QueueBackgroundWorkItem`, dropping `StateObject` entries idle for more than two hours and deleting expired `login_sessions` rows. Sweeping the dictionary is just a `foreach` over `KeyValuePair`s — no special API needed.
- **App-pool recycle** — the dictionary is gone, but any user with a live `lsid` cookie is transparently restored on their next request.
### Web.config — disable built-in session module
```xml
<system.web>
<sessionState mode="Off" />
</system.web>
```
This removes the per-request `AcquireRequestState` lock entirely, which is what makes single-point routing in `BeginRequest` clean.
---
## 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
/engine/
config.cs ← connection string, app settings
ApiHelper.cs ← shared response helpers
PageTemplate.cs ← shared HTML template (replaces master page)
/engine/
/engine/ob
/engine/Models
obBook.cs ← data model
/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";
// --- 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' />
");
// 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>
");
// 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 — read from custom AppSession (not HttpContext.Session)
if (AppSession.IsLoggedIn)
{
string username = AppSession.LoginUser.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."
};
string extraHeaderText = @"
<link href='/css/books.css' rel='stylesheet'>
<script src='/js/books.js'></script>
";
pt.ExtraHeaderText = extraHeaderText;
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);
}
```
Photo by zhang kaiyv on Unsplash
