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

This is a complete architecture reference, guideline and code convention for building modern web applications on Vanilla ASP.NET Web Forms without Server Controls, ViewState, or PostBack. It covers API endpoint patterns, Fetch API integration, file uploads, Server-Sent Events, WebSocket, and background task management.

# Vanilla ASP.NET Web Forms API Endpoint+ FetchAPI Architecture

## Overview

This project uses a **hybrid architecture** that combines ASP.NET WebForms infrastructure with modern client-side API patterns. This approach eliminates ViewState, server controls, and postbacks while retaining WebForms benefits (routing, authentication, master pages).

---

## Core Principle: NO Traditional WebForms Patterns

| ❌ AVOID | ✅ USE INSTEAD |
|----------|----------------|
| `<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()` or `XMLHttpRequest` |
| Code-behind event handlers | API endpoint actions |

### ⚠️ 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 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 the CamelCase ("propertyName").

---

## File Structure Pattern

### Master Page as Template

Use ASP.NET Master Pages (`.master`) as the shared layout template. 
The master page holds the common HTML structure — `<head>`, navigation, 
footer, script references — while each content page supplies its own 
markup through `ContentPlaceHolder` regions.

### Page Naming

#### Frontend Page Naming

Frontend with HTML, JavaScript and CSS only

```
FrontPage.aspx
FrontPage.aspx.cs <-- almost empty, except user login detection
FrontPage.aspx.designer.cs <-- empty component, because there is no server control
```

#### API Page Naming

Primary Choice, append with "Api" as a pair to the Frontend Page. Easy to manage.

```
FrontPageApi.aspx <-- blank, nothing
FrontPageApi.aspx.cs <-- backend C# api handling
FrontPageApi.aspx.designer.cs <-- empty component, because there is no server control
```

Secondary Choice, prefix with "api":
```
apiFrontPage.aspx <-- blank, nothing
apiFrontPage.aspx.cs <-- backend C# api handling
apiFrontPage.aspx.designer.cs <-- empty component, because there is no server control
```

---

## Frontend Pattern (.aspx)

### Key Frontend Patterns

**1. Data Loading (GET)**
```javascript
const response = await fetch(`${API_URL}?action=get_list&id=${id}`);
const data = await response.json();
if (data.success) {
    // Render data.items to DOM
}
```

**2. Data Saving (POST with FormData)**
```javascript

// uses the async/await syntax
// making asynchronous code look and behave more like synchronous code.
// Linear Flow: You read the code from top to bottom. There is no "nesting" or "callback hell."
// Variable Scoping: Variables like data or response are available in the same scope, making it easier to use them later in the function without passing them through a chain of .then() blocks.
// Error Handling: You can use standard try/catch blocks, which many developers find more intuitive than a trailing .catch() method.
async function doRegister() {

    // Prevent double submit
    const btn = document.getElementById('btnRegister');
    btn.disabled = true;
    btn.textContent = 'Creating account...';

    // append the form field (if relevant) or fake form fields as honeypot to trap spambot
    const form = document.getElementById('registerForm');

    const formData = new FormData(form);
    formData.append('action', 'register');
    formData.append('username', username);
    formData.append('email', email);
    formData.append('password', password);
    formData.append('subscribe_newsletter', newsletter);

    try {
        const 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');
        }

        const data = await response.json();

        if (data.success) {
            // task success
            btn.textContent = 'Done';
        } else {
            // task failed
            btn.disabled = false;
            btn.textContent = 'Register';
        }
    } catch (error) {
        console.error('Fetch error:', error);
        // network error
        btn.disabled = false;
        btn.textContent = 'Register';
    }
}
```

**3. File Upload (XMLHttpRequest with FormData)**
```javascript
const formData = new FormData();
formData.append('action', 'upload');
formData.append('file', fileInput.files[0]);

const xhr = new XMLHttpRequest();
xhr.open('POST', API_URL);
xhr.onload = function() {
    const data = JSON.parse(xhr.responseText);
    // Handle response
};
xhr.send(formData);
```

---

## Backend Api Pattern 

### Structure

Front page (.aspx)

Deletes all frontend markup, leave only the first line, the page directive declaration:

```html
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="apiPage.aspx.cs" Inherits="myweb.apiBackup" %>
```

The code behind (.aspx.cs)

```csharp
public partial class SomePageApi : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        try
        {
            // 1. Auth check
            if (AppSession.LoginUser == null || !AppSession.LoginUser.IsAdmin)
            {
                WriteError("Unauthorized", 401);
                return;
            }

            // 2. Route by action parameter
            string action = (Request["action"] + "").ToLower().Trim();
            
            switch (action)
            {
                case "get_list":    GetList();     break;
                case "get_item":    GetItem();     break;
                case "save":        SaveItem();    break;
                case "delete":      DeleteItem();  break;
                default:            WriteError($"Unknown action: {action}", 400); break;
            }
        }
        catch (Exception ex)
        {
            WriteError(ex.Message, 500);
        }

        EndResponse();
    }

    #region Helper Methods (Copy to all API files)

    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();
    }

    void WriteJson(object obj)
    {
        // no naming conversion, preserves names exactly as declared:
        Response.ContentType = "application/json";
        Response.Write(JsonConvert.SerializeObject(obj));
    }

    void WriteSuccess(string message = "Success")
    {
        WriteJson(new { success = true, message });
    }

    void WriteError(string message, int statusCode = 400)
    {
        Response.StatusCode = statusCode;
        WriteJson(new { success = false, message });
    }

    #endregion

    #region API Actions

    void GetList()
    {
        int parentId = dp.IntParse(Request["parent_id"]);
        // ... fetch from database
        WriteJson(new { success = true, items = resultList });
    }

    void SaveItem()
    {
        // Read parameters
        int id = dp.IntParse(Request["id"]);
        string name = (Request["name"] + "").Trim();
        
        // Validate
        if (string.IsNullOrEmpty(name))
        {
            WriteError("Name is required");
            return;
        }
        
        // Save to database using MySqlExpress
        using (MySqlConnection conn = new MySqlConnection(config.ConnString))
        {
            conn.Open();
            using (MySqlCommand cmd = new MySqlCommand())
            {
                cmd.Connection = conn;
                MySqlExpress m = new MySqlExpress(cmd);
                m.Save("table_name", itemObject);
            }
        }
        
        WriteSuccess("Saved");
    }

    #endregion
}
```

---

## API Response Format

All API responses follow this JSON structure:

```javascript
// Success
{ "success": true, "message": "...", "data": {...} }

// Success with list
{ "success": true, "items": [...] }

// Error
{ "success": false, "message": "Error description" }
```

---

## 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
```

---

## File Upload Pattern

**Frontend:**
```javascript
async function uploadFile() {
    const fileInput = document.getElementById('fileUpload');
    if (!fileInput.files.length) return;
    
    const formData = new FormData();
    formData.append('action', 'upload');
    formData.append('parent_id', parentId);
    formData.append('file', fileInput.files[0]);
    
    // Use XMLHttpRequest for progress tracking
    const xhr = new XMLHttpRequest();
    
    xhr.upload.onprogress = (e) => {
        if (e.lengthComputable) {
            const pct = Math.round((e.loaded / e.total) * 100);
            document.getElementById('progress').textContent = pct + '%';
        }
    };
    
    xhr.onload = () => {
        const data = JSON.parse(xhr.responseText);
        if (data.success) showToast('Uploaded', 'success');
        else showToast(data.message, 'error');
    };
    
    xhr.open('POST', API_URL);
    xhr.send(formData);
}
```

**Backend:**
```csharp
void UploadFile()
{
    if (Request.Files.Count == 0)
    {
        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; // skip empty files if desired

        string fileName = Path.GetFileName(file.FileName);
        string savePath = Server.MapPath("~/uploads/" + fileName);

        try
        {
            file.SaveAs(savePath);

            uploadedFiles.Add(new
            {
                success = true,
                fileName = fileName,
                filePath = "/uploads/" + fileName
            });
        }
        catch (Exception ex)
        {
            // Log exception; decide whether to continue or fail the whole batch
            uploadedFiles.Add(new
            {
                success = false,
                fileName = fileName,
                message = ex.Message
            });
        }
    }

    // Return array of results (most flexible for client)
    WriteJson(uploadedFiles);
    // Or, if you prefer a single success flag:
    // bool allSuccess = uploadedFiles.All(f => (bool)f.success);
    // WriteJson(new { success = allSuccess, files = uploadedFiles });
}
```

---

## 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 |

**Recommendation:** Default to **SSE** for progress reporting (simpler, automatic reconnection). Use **WebSocket** only when bi-directional communication is required.

---

### Architecture: Hybrid HTTP + Streaming

Real-time features combine HTTP requests with streaming connections:
- **HTTP Request**: Start task, stop task, retrieve initial data
- **SSE/WebSocket**: Monitor progress, receive real-time updates

```
┌─────────────┐     HTTP POST (start_task)      ┌─────────────┐
│   Frontend  │ ─────────────────────────────▶  │   Backend   │
│             │     { taskId: 123 }              │   API       │
│             │ ◀─────────────────────────────   │             │
│             │                                  │             │
│             │     SSE/WebSocket Connection     │             │
│             │ ◀════════════════════════════   │             │
│             │     { progress: 50%, ... }       │             │
└─────────────┘                                  └─────────────┘
```

A **single API endpoint** handles both HTTP and streaming requests.

---

### Server-Sent Events (SSE) — Recommended

#### Page Directive (CRITICAL)

```html
<%@ Page Language="C#" EnableSessionState="ReadOnly" AutoEventWireup="true" 
    CodeBehind="SomeApi.aspx.cs" Inherits="..." %>
```

⚠️ **`EnableSessionState="ReadOnly"`** is mandatory to permit concurrent HTTP requests while an SSE connection is active. Without this, ASP.NET session locking will block all other requests.

#### Backend: SSE Detection & Handling

```csharp
public partial class SomeApi : System.Web.UI.Page
{
    static ConcurrentDictionary<int, TaskInfo> dicTaskInfo = new ConcurrentDictionary<int, TaskInfo>();

    protected void Page_Load(object sender, EventArgs e)
    {
        // 1. Check for SSE request FIRST
        if (Request.Headers["Accept"] == "text/event-stream" || Request["stream"] == "true")
        {
            HandleSSERequest();
            return;
        }

        // 2. Normal HTTP API handling
        try
        {
            if (!IsAuthenticated()) { WriteError("Unauthorized", 401); return; }

            string action = (Request["action"] + "").ToLower().Trim();
            switch (action)
            {
                case "start_task":  StartTask();  break;
                case "stop_task":   StopTask();   break;
                default: WriteError("Unknown action", 400); break;
            }
        }
        catch (Exception ex) { WriteError(ex.Message, 500); }

        EndResponse();
    }
}
```

#### Backend: SSE Handler

```csharp
void HandleSSERequest()
{
    if (!IsAuthenticated())
    {
        Response.StatusCode = 401;
        EndResponse();
        return;
    }

    if (!int.TryParse(Request["task_id"] + "", out int taskId))
    {
        Response.StatusCode = 400;
        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);  // Poll interval
        }
    }
    catch (HttpException) { /* Client disconnected */ }
    finally { EndResponse(); }
}

void SendSSEEvent(string eventType, string data)
{
    if (!Response.IsClientConnected) return;
    Response.Write($"event: {eventType}\ndata: {data}\n\n");
    Response.Flush();
}
```

#### Frontend: SSE Connection

```javascript
const API_URL = '/pages/SomeApi.aspx';
let eventSource = null;
let currentTaskId = 0;

// Start task via HTTP POST with FormData, then connect SSE
async function startTask() {
    const formData = new FormData();
    formData.append('action', 'start_task');

    try {
        const response = await fetch(API_URL, {
            method: 'POST',
            // Do NOT set Content-Type header when using FormData
            // The browser automatically sets multipart/form-data with boundary
            body: formData
        });

        if (!response.ok) {
            throw new Error(`Server responded with status ${response.status}`);
        }

        const data = await response.json();

        if (data.success) {
            currentTaskId = data.taskId;
            connectSSE(currentTaskId);
        } else {
            console.error('Task start failed:', data);
            // Optionally show user feedback here
        }
    } catch (error) {
        console.error('Error starting task:', error);
        // Optionally show user feedback here
    }
}

function connectSSE(taskId) {
    if (eventSource) return;

    eventSource = new EventSource(`${API_URL}?stream=true&task_id=${taskId}`);

    eventSource.addEventListener('connected', (e) => {
        console.log('SSE connected:', e.data);
    });

    eventSource.addEventListener('progress', (e) => {
        const data = JSON.parse(e.data);
        updateProgress(data.percentComplete, data.status);
    });

    eventSource.addEventListener('completed', (e) => {
        const data = JSON.parse(e.data);
        updateProgress(100, 'Completed');
        closeSSE();
    });

    eventSource.onerror = () => {
        console.error('SSE error');
        closeSSE();
    };
}

function closeSSE() {
    if (eventSource) {
        eventSource.close();
        eventSource = null;
    }
}

// Cleanup on page unload
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: WebSocket Detection & Handling

```csharp
protected void Page_Load(object sender, EventArgs e)
{
    // 1. Check for WebSocket request FIRST
    if (Context.IsWebSocketRequest)
    {
        Context.AcceptWebSocketRequest(HandleWebSocket);
        return;
    }

    // 2. Normal HTTP API handling
    // ... same as SSE pattern
}

async Task HandleWebSocket(AspNetWebSocketContext context)
{
    WebSocket webSocket = context.WebSocket;

    if (!IsAuthenticated())
    {
        await webSocket.CloseAsync(WebSocketCloseStatus.PolicyViolation, 
            "Unauthorized", CancellationToken.None);
        return;
    }

    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);
            
            if (message.StartsWith("taskid:"))
            {
                int taskId = int.Parse(message.Substring(7));
                await SendProgressUpdates(webSocket, taskId);
            }
        }
        else if (result.MessageType == WebSocketMessageType.Close)
        {
            await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, 
                "", CancellationToken.None);
            break;
        }
    }
}

async Task SendProgressUpdates(WebSocket webSocket, int taskId)
{
    while (webSocket.State == WebSocketState.Open)
    {
        if (dicTaskInfo.TryGetValue(taskId, out var taskInfo))
        {
            string json = JsonConvert.SerializeObject(taskInfo);
            byte[] bytes = Encoding.UTF8.GetBytes(json);
            
            await webSocket.SendAsync(new ArraySegment<byte>(bytes),
                WebSocketMessageType.Text, true, CancellationToken.None);

            if (taskInfo.IsCompleted) break;
        }
        await Task.Delay(250);
    }
}
```

#### Frontend: WebSocket Connection

```javascript
let webSocket = null;

function connectWebSocket(taskId) {
    if (webSocket) return;

    const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
    const wsUrl = `${protocol}//${location.host}${API_URL}`;

    webSocket = new WebSocket(wsUrl);

    webSocket.onopen = () => {
        webSocket.send(`taskid:${taskId}`);
    };

    webSocket.onmessage = (event) => {
        const data = JSON.parse(event.data);
        updateProgress(data.percentComplete, data.status);
        
        if (data.isCompleted) {
            webSocket.close(1000, "Task completed");
        }
    };

    webSocket.onclose = () => { webSocket = null; };
    webSocket.onerror = (err) => { console.error('WebSocket error:', err); };
}
```

---

### TaskInfo Class

Standard structure for tracking background task state:

```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; } = "";
    
    // For graceful cancellation
    public bool RequestCancel { get; set; } = false;
    public bool IsCancelled { get; set; } = false;
}
```

---

### Background Task with Cancellation Support

```csharp
void StartTask()
{
    int taskId = GetNewTaskId();
    
    var taskInfo = new TaskInfo { TaskId = taskId };
    dicTaskInfo[taskId] = taskInfo;

    // Start background work (fire-and-forget)
    _ = Task.Run(() => DoWork(taskId));

    // Return immediately with task ID
    WriteJson(new { success = true, taskId });
}

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;
}

void StopTask()
{
    int taskId = dp.IntParse(Request["task_id"]);
    if (dicTaskInfo.TryGetValue(taskId, out var taskInfo))
    {
        taskInfo.RequestCancel = true;
        WriteSuccess("Stop requested");
    }
    else
    {
        WriteError("Task not found");
    }
}
```

---

### 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 |
| Browser support | Excellent | Excellent |
| Implementation complexity | Simple | More complex |
| Automatic reconnection | ✅ Built-in | Manual implementation required |

**Default to SSE** for progress reporting. Use WebSocket only when the client must send messages during an active connection.

Or use normal form post (fetchapi) to send message and SSE to receive changes or updates from server.

Photo by Beyzaa Yurtkuran from Pexels.