eQuantic.UI - Architecture[!NOTE] This page describes the web pipeline. The framework also targets native (macOS/iOS/Android) through the same component sources. See Write-Once Components for the shared architecture and Photon for the GPU engine. eQuantic.UI is a self-contained UI framework for .NET that compiles C# into optimized JavaScript, eliminating dependencies on Node.js, npm, Vite, or any external frontend tool.
The diagram above is a mermaid ` fence: GitHub draws it here, and the documentation site draws the SAME fence through the SDK's own Mermaid component. 1.
✅ 100% .NET - Zero external dependencies (Node.js, npm, etc)
2.
✅ Self-Contained - ASP.NET Core serves and compiles everything
3.
✅ Familiar - Routing via attributes (like Controllers)
4.
✅ Modern - SPA experience with SSR when needed
5.
✅ Performant - Intelligent compilation (static vs dynamic)
<!-- eQuantic.UI.Sdk inherits Microsoft.NET.Sdk.Web -->
<Project Sdk="eQuantic.UI.Sdk/1.0.0">
<TargetFramework>net9.0</TargetFramework>
Under the hood:
<!-- eQuantic.UI.Sdk/Sdk/Sdk.props -->
<!-- Inherits full Web SDK -->
<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk.Web" />
<!-- Adds UI compilation -->
<EnableEQuanticUICompilation>true</EnableEQuanticUICompilation>
<EQuanticOutputPath>wwwroot/_equantic/</EQuanticOutputPath>
1.2 Build Pipeline Integration
MSBuild Standard Pipeline (Microsoft.NET.Sdk.Web)
Custom Target: CompileEQuanticUI (BeforeTargets="Build")
1. Roslyn parse /Pages/**/*.cs
2. Detect StatefulComponent/StatelessComponent/ComponentState classes
3. Generate TypeScript intermediate (.ts files)
└─ Human-readable (debugging)
├─ bun build *.ts --outdir wwwroot/_equantic
├─ Automatic Tree-shaking
5. Generate manifest.json
Output: bin/ + wwwroot/_equantic/
Why TypeScript Intermediate?
C# (source) → TypeScript (intermediate) → JavaScript (output)
Developer Type Safety Runtime
writes C# + Debug-friendly Optimized
Benefits:
•
✅ Two-layer type checking (C# + TS)
•
✅ Source maps from C# → TS → JS (full debugging)
•
✅ Leverage Bun's optimization engine
•
✅ Future: could support direct TS authoring too
Bun Performance:
# Traditional Node.js build
2. Compilation Strategy: Static vs Dynamic
2.1 Problem: Single Bundle vs Code Splitting
Challenge: We don't want a giant bundle.js, but we also don't want hundreds of small files.
Solution: Hybrid Compilation Strategy
A. Static Shell (Compiled at Build-Time)
What compiles statically:
•
Component structure (Component tree)
Output: {ComponentName}.static.js
// Counter.static.js (generated at build)
export const CounterStatic = {
// Template structure (no runtime needed)
props: { className: "counter" },
{ type: "Heading", props: { text: "Counter" } },
{ type: "TextInput", props: { id: "msg", placeholder: "..." } },
{ type: "Button", props: { id: "dec", text: "-" } },
{ type: "Text", props: { id: "count", text: "0" } },
{ type: "Button", props: { id: "inc", text: "+" } },
// Style (CSS-in-JS compiled)
.counter { padding: 20px; }
.count-display { font-size: 24px; font-weight: bold; }
B. Dynamic Logic (Compiled at Build-Time, runs Client-Side)
What compiles as dynamic logic:
Output: {ComponentName}.logic.js
// Counter.logic.js (generated at build)
export class CounterLogic {
this._component = component;
this._component.update({ count: this._count });
this._component.update({ count: this._count });
_onMessageChange(value) {
// No update needed if doesn't reflect in UI
C. Server Actions (Run Server-Side)
What does NOT compile to JS:
•
Authentication/Authorization
Solution: Server Actions Pattern
public class TodoList : StatefulComponent
// Server Action - runs on server
public async Task<List<Todo>> LoadTodos()
using var db = new AppDbContext();
return await db.Todos.ToListAsync();
public async Task<Todo> AddTodo(string title)
using var db = new AppDbContext();
var todo = new Todo { Title = title };
await db.SaveChangesAsync();
public class TodoListState : ComponentState<TodoList>
private List<Todo> _todos = [];
protected override void OnMount()
private async Task LoadInitialData()
_todos = await Component.LoadTodos();
private async Task HandleAdd(string title)
var newTodo = await Component.AddTodo(title);
SetState(() => _todos.Add(newTodo));
public override IComponent Build(RenderContext context)
Children = _todos.Select(t =>
(IComponent)new TodoItem { Todo = t }
Compilation:
export class TodoListLogic {
// Generates call to server action
this._todos = await this._serverActions.invoke("LoadTodos", []);
const newTodo = await this._serverActions.invoke("AddTodo", [title]);
this._todos.push(newTodo);
this._component.update({ todos: this._todos });
Goal: Optimize loading without exploding request count.
Level 1: Core Runtime (loads on all pages)
/_equantic/runtime.js (~15kb gzipped)
Level 2: Component Library (lazy load per route)
/_equantic/components.js (~30kb gzipped)
- Button, TextBox, Container, etc
- Components used by multiple pages
Level 3: Page Bundles (lazy load per route)
/_equantic/pages/Counter.js
- Counter.static.js (structure)
- Counter.logic.js (behavior)
- Counter-specific widgets
Level 4: External Assets (CDNs/Shared Scripts)
https://cdn.example.com/library.js
- Declared via IRequireAssets
- Deduplicated by AssetCollection
Level 4: Shared Chunks (automatic code splitting)
- auth.chunk.js (if multiple pages use auth)
- api.chunk.js (shared API logic)
Loading Example:
<!-- Request: GET /counter -->
<script src="/_equantic/runtime.js"></script>
<script src="/_equantic/components.js"></script>
<script src="/_equantic/pages/Counter.js"></script>
<!-- SPA Navigation: /counter → /todos -->
<script src="/_equantic/pages/TodoList.js"></script>
3. Server Actions: Client ↔ Server Communication
3.1 Problem: Avoiding Manual Endpoints
Anti-pattern (we want to avoid):
public class TodoController : ControllerBase
public Task<Todo> AddTodo([FromBody] AddTodoRequest req) { }
async function addTodo(title) {
const response = await fetch('/api/todos', {
body: JSON.stringify({ title })
return await response.json();
We Want (type-safe, zero boilerplate):
public class TodoList : StatefulComponent
public async Task<Todo> AddTodo(string title)
private async Task HandleAdd()
var todo = await Widget.AddTodo("New item");
// ↑ Type-safe, auto-serialization
3.2 Implementation: Server Actions Bridge
A. Compilation Time
Compiler detects methods with [ServerAction]:
public class Counter : StatefulComponent
public async Task<int> IncrementOnServer(int current)
// Simulate server-side logic
Generates:
export class CounterLogic {
async incrementOnServer(current) {
return await this._serverActions.invoke(
"Counter/IncrementOnServer", // Action ID
B. Runtime: Server Actions Endpoint
Automatic middleware exposing /api/_equantic/actions:
// eQuantic.UI.Server/ServerActionsMiddleware.cs
public class ServerActionsMiddleware
private readonly IServerActionRegistry _registry;
public async Task InvokeAsync(HttpContext context)
if (context.Request.Path == "/api/_equantic/actions")
var request = await JsonSerializer
.DeserializeAsync<ServerActionRequest>(context.Request.Body);
// request.ActionId = "Counter/IncrementOnServer"
// request.Arguments = [5]
var action = _registry.GetAction(request.ActionId);
// Invoke method via reflection (or compiled expression)
var result = await action.InvokeAsync(request.Arguments);
await context.Response.WriteAsJsonAsync(new {
C. Client Runtime Bridge
// runtime.js - Server Actions Bridge
class ServerActionsClient {
async invoke(actionId, args) {
const response = await fetch("/api/_equantic/actions", {
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ actionId, arguments: args }),
const data = await response.json();
throw new Error(data.error);
3.3 Real-Time Push: Scope
The current release does not include a [ServerEvent] subscription model: Server Actions are request/response. SignalR services are registered and used internally by the framework; real-time server→client push is on the Roadmap. Components declare their own external dependencies (scripts, stylesheets) without manual injection in the HTML shell. Two patterns are supported:
Pattern 1: IRequireAssets, where a component declares its own assets:
public class CodeBlock : StatelessComponent, IRequireAssets
public void ConfigureAssets(AssetBuilder assets)
assets.AddStylesheet("https://cdn.example.com/prism.css", id: "prism-theme");
assets.AddScript("https://cdn.example.com/prism.js");
assets.AddInlineScript("function init(){ ... }");
Pattern 2: IComponentAssetProvider<T>, an external provider for third-party components:
public class ChartJsAssetProvider : IComponentAssetProvider<ChartCanvas>
public void ConfigureAssets(AssetBuilder assets)
assets.AddScript("https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js");
Providers are auto-registered during AddUI() via assembly scanning, or explicitly via WithAssetProvider<T>().
Framework Flow:
1.
ServerRenderingService walks the component tree, collecting IRequireAssets and IComponentAssetProvider<T> assets.
2.
AssetCollection deduplicates entries by Key (CSS first, then JS).
3.
UIExtensions injects the resulting tags into the <head> of the page.
4.
Pages without asset-requiring components get zero extra tags.
4. Diff vs ASP.NET WebForms
4.1 Learnings from WebForms
What WebForms did well:
•
✅ Event model (onClick, onChange)
•
✅ Server controls with state
•
✅ Postback for server logic
What WebForms did poorly:
•
❌ Giant ViewState (increases payload)
•
❌ Full-page postback (not SPA)
•
❌ HTML generated server-side (slow)
•
❌ Limited/Difficult JavaScript
4.2 How eQuantic.UI Improves This
Aspect
WebForms
eQuantic.UI
State Management
ViewState (hidden field)
Client-side state + Server Actions
Rendering
Server-side HTML generation
Client-side rendering (Virtual DOM)
Updates
Full postback
Partial updates (SPA)
JS Integration
UpdatePanel/ScriptManager
Native JavaScript compilation
Event Handling
Server postback
Client-side + Server Actions selective
Performance
Every click = server roundtrip
Client-side logic, server when needed
Bundle Size
N/A (server-rendered)
Minimal (~15kb runtime)
WebForms-like DX:
// Familiar for WebForms devs
public class Counter : StatefulComponent
private void OnButtonClick() // ← Like WebForms!
// But runs client-side, no postback!
Modern SPA Performance:
// Compiled to optimized JS
// Runs in browser, no postback
// Only calls server when really needed
5.1 Routing via Attributes
[Page("/count")] // Multiple routes
public class Counter : StatefulComponent { }
[Page("/user/{id:int}")] // Route parameters
public class UserProfile : StatefulComponent
public int Id { get; set; } // Auto-binding
// Pages/Admin/Dashboard.cs
[Page("/admin/dashboard")]
[Authorize(Roles = "Admin")] // Authorization
public class AdminDashboard : StatefulComponent { }
5.2 Program.cs Registration
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddUI(options => {
options.ScanAssembly(typeof(Program).Assembly);
var app = builder.Build();
app.MapUI(); // Auto-discovery via [Page] attributes + SPA fallback
5.3 Navigation (Client-Side)
Client-side navigation is handled by the runtime router: link clicks and the Link component navigate without a reload, with typed route params, guards, prefetch and scroll restoration. A typed programmatic Navigator API is not part of the current release.
├── MyApp.csproj # eQuantic.UI.Sdk
├── Program.cs # ASP.NET Core host
├── Pages/ # Page components
│ ├── Home.cs # [Page("/")]
│ ├── Counter.cs # [Page("/counter")]
│ └── Dashboard.cs # [Page("/admin/dashboard")]
├── Components/ # Reusable UI components
├── Services/ # Backend services (DI)
├── Models/ # Shared models
└── wwwroot/ # Static assets
├── _equantic/ # Generated (build output)
dotnet new install eQuantic.UI.Templates
dotnet new equantic-app -n MyApp
dotnet new equantic-page -n UserProfile -o Pages
dotnet new equantic-component -n DataGrid -o Components
# → Hot reload on .cs changes
dotnet publish -c Release
1. Developer edits Counter.cs
2. dotnet watch detects change
3. MSBuild task recompiles Counter.cs → Counter.js
4. File watcher notifies browser (SSE at `/_equantic/hmr`)
5. Browser fetches updated Counter.js
6. Hot Module Replacement
7. UI updates without losing state
7.1 Embedded Bun for TypeScript Compilation
Decision: Use Bun as Embedded Build Tool
Why Bun:
•
✅ Single executable - distributes with SDK
•
✅ Ultra fast - 10-100x faster than Node.js
•
✅ Native TypeScript - compiles TS without config
•
✅ Embedded Bundler - no need for Webpack/Vite
•
✅ Self-contained - no npm install needed
•
✅ Small footprint - ~90MB (vs Node.js ~200MB)
Architecture:
│ └── eqc-compiler.ts # TypeScript compiler wrapper
└── eQuantic.UI.Build.targets
Build Pipeline with Bun:
MSBuild Task: CompileEQuanticUI
1. C# Roslyn parse (Pages/**/*.cs + library sources)
2. Generate TypeScript intermediate
3. Embedded Bun bundles → wwwroot/_equantic/
See Build Flow for the full pipeline, step by step.