Complete Architecture Reference for Pageless ASP.NET Web Forms in MD (Markdown) Format

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>&copy; {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