WasmMvcRuntime runs a complete ASP.NET MVC application inside a WebAssembly Web Worker in the browser. The main thread acts as a thin display surface — it never executes .NET code, controller logic, or database queries.
| Thread | Role | Blocks UI? |
|---|---|---|
| Main Thread | DOM rendering, SPA routing, event listeners, CSS/script activation | Never |
| Runtime Worker | .NET WASM execution: MVC pipeline, EF Core, Identity, SignalR | N/A (off-thread) |
| Data Worker | OPFS file I/O: database snapshots, session persistence, offline queue | N/A (off-thread) |
┌──────────────────────────────────────────────────────────────────┐
│ Browser │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Main Thread (Display Surface) │ │
│ │ index.html → main.js → SPA Router │ │
│ │ • Link interception (history.pushState) │ │
│ │ • Form submission handler │ │
│ │ • Frame buffer + requestAnimationFrame rendering │ │
│ │ • CSS promotion + onload wait → document-order scripts │ │
│ │ • SignalR client proxy │ │
│ │ • Cross-tab auth sync (BroadcastChannel) │ │
│ └──────────────┬─────────────────────────────┬───────────────┘ │
│ │ postMessage() │ postMessage() │
│ ┌──────────────▼───────────────┐ ┌──────────▼──────────────┐ │
│ │ Runtime Worker │ │ Data Worker │ │
│ │ (.NET WASM via dotnet.js) │ │ (OPFS I/O) │ │
│ │ │ │ │ │
│ │ ┌────────────────────────┐ │ │ • Database snapshots │ │
│ │ │ .NET Runtime │ │ │ • Session persistence │ │
│ │ │ │ │ │ • Offline change queue │ │
│ │ │ MvcEngine │ │ │ • File operations │ │
│ │ │ ├─ Controller Scan │ │ │ │ │
│ │ │ ├─ Route Matching │ │ └──────────────────────────┘ │
│ │ │ ├─ Action Invocation │ │ │
│ │ │ └─ View Rendering │ │ ┌──────────────────┐ │
│ │ │ │ │ │ Origin Private │ │
│ │ │ RazorTemplateEngine │ │ │ File System │ │
│ │ │ EF Core + SQLite │ │ │ (Persistent) │ │
│ │ │ Identity + Sessions │ │ │ │ │
│ │ │ SignalR (in-process) │ │ │ • identity.db │ │
│ │ └────────────────────────┘ │ │ • app-data.db │ │
│ └──────────────────────────────┘ │ • sessions.json │ │
│ └──────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
1. Browser loads index.html (static file from CDN)
2. index.html imports main.js (ES module)
3. main.js spawns Web Worker (cepha-runtime-worker.js)
4. Worker boots .NET WASM runtime (dotnet.js)
5. Worker registers JSImport/JSExport bindings
6. Worker calls Program.Main() → CephaApp.Create() → app.RunAsync()
7. RunAsync:
a. Restores SQLite database from OPFS
b. Restores session state from OPFS
c. Ensures EF Core tables exist (auto-migration)
d. Navigates to initial URL path
e. Enters infinite event loop (Task.Delay(Timeout.Infinite))
8. Main thread intercepts link clicks → postMessage to Worker
9. Worker: MvcEngine.ProcessRequestAsync() → Controller → View → HTML
10. Worker: postMessage HTML frame to Main thread
11. Main thread: applies frame via requestAnimationFrame
The MVC engine renders complete HTML for each navigation. The main thread receives this as a "frame" and applies it to the DOM:
- Controller executes → produces
ViewResultwith model data - RazorTemplateEngine renders
.cshtmltemplate with model/ViewData - Layout applied →
@RenderBody()+@RenderSection()merged - HTML frame sent to main thread via
postMessage activateScripts()processes the frame:- CSS
<link>elements promoted and awaited (prevents FOUC) <script>elements cloned and executed in document order- Each script's
onloadis awaited before the next executes
- CSS
- Frame applied via
requestAnimationFrame(consistent 60fps)
Links are intercepted at the main thread level:
clickevents on<a>elements →history.pushState()+ Worker message<form>submissions → serialize form data + Worker messagepopstateevents (browser back/forward) → Worker message- Worker processes the MVC request → returns new HTML frame
Main Thread ←→ Runtime Worker:
navigate(path) → HTML frame
submit(action, formData) → HTML frame or redirect instruction
hub-connect(hubName) → connectionId
hub-invoke(hub, method, args) → result JSON
hub-event(hub, method, args) → broadcast to listeners
auth-sync() → re-render current page
Runtime Worker ←→ Data Worker (via Main Thread relay):
cephaDb.persist(base64) → ack
cephaDb.restore() → base64 database snapshot
opfs.write(path, data) → ack
opfs.read(path) → data
All C#↔JavaScript communication uses the System.Runtime.InteropServices.JavaScript APIs:
JSExport — C# methods callable from JavaScript:
[JSExport]
public static async Task<string> HandleNavigate(string path)
{
return await MvcEngine.ProcessRequestAsync(path);
}JSImport — JavaScript functions callable from C#:
[JSImport("globalThis.cephaInterop.updateFrame")]
public static partial void UpdateFrame(string html);This avoids dependency on Blazor's IJSRuntime and the Blazor component infrastructure.
| Decision | Rationale |
|---|---|
| Web Worker hosting | .NET execution never blocks UI. Even complex DB queries or slow controllers cannot cause jank. |
Embedded .cshtml resources |
Avoids shipping the Razor compilation toolchain to the browser. Pattern-matching covers the most common Razor features. |
| OPFS for persistence | Unlike localStorage (5MB, synchronous, main-thread only), OPFS supports large files, async access, and Worker-thread access. |
| Frame-based rendering | Simpler than virtual DOM diffing. Each navigation produces a complete HTML document — no incremental patching needed. |
| JSImport/JSExport | Direct interop without Blazor's marshaling layer. Lower overhead, explicit contract. |
| File | Purpose |
|---|---|
MvcEngine.cs |
Controller discovery, route matching, action invocation |
RazorTemplateEngine.cs |
.cshtml rendering |
CephaApp.cs |
Application bootstrap, DB restore, event loop |
JsInterop.cs |
JSImport declarations |
JsExports.cs |
JSExport entry points |
main.js |
SPA router, frame pipeline, activateScripts |
cepha-runtime-worker.js |
.NET WASM host in Web Worker |