Skip to content

ServerIntegration

Edgar Mesquita edited this page Aug 7, 2026 · 8 revisions

Server Integration

eQuantic.UI integrates with ASP.NET Core through a fluent API for service registration, middleware configuration, and HTML shell customization.

Quick Start

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddUI(options =>
{
    options.ScanAssembly(typeof(Program).Assembly)
           .WithSsr()
           .UseTailwind()              // Theme
           .UseLucideIcons()           // Icons
           .UseChartJs()               // Charts
           .UseApexCharts()            // Charts
           .ConfigureHtmlShell(shell =>
           {
               shell.SetTitle("My App")
                    .SetHtmlClass("dark")
                    .AddHeadTag("<meta name=\"theme-color\" content=\"#3b82f6\">");
           });
});

var app = builder.Build();

app.UseStaticFiles();
app.UseServerActions();                // Server Actions middleware
app.MapUI();                           // SPA routing & package endpoints

app.Run();

AddUI

AddUI() is the main entry point that registers all core services:

builder.Services.AddUI(options => { ... });

What it registers:

  • UIOptions (singleton) - Configuration
  • IServerActionRegistry - Scans assemblies for [ServerAction] methods
  • IServerActionAuthorizationService - Authorization for server actions
  • IServerRenderingService - SSR rendering engine
  • IAppTheme - Default eQuantic theme (overridden by AddTailwind())
  • IComponentAssetProvider<T> - Auto-scanned from assemblies (see Assets)
  • SignalR services

UIOptions

Fluent configuration API for the UI framework.

ScanAssembly

Scans an assembly for [Page] components, [ServerAction] methods, and IComponentAssetProvider<T> implementations.

options.ScanAssembly(typeof(Program).Assembly);

Multiple assemblies can be scanned:

options.ScanAssembly(typeof(Program).Assembly)
       .ScanAssembly(typeof(SharedComponents).Assembly);

WithSsr

Enables or disables Server-Side Rendering globally. Default is true.

options.WithSsr();           // Enable (default)
options.WithSsr(false);      // Disable

Individual pages can opt-out:

[Page("/interactive", DisableSsr = true)]
public class InteractivePage : StatefulComponent { }

WithAssetProvider<T>

Explicitly registers an IComponentAssetProvider<T> implementation. Useful for providers from external assemblies not covered by ScanAssembly.

options.WithAssetProvider<ChartJsAssetProvider>();

See Assets for details on the asset provider system.

ConfigureHtmlShell

Configures the HTML template that wraps all pages.

options.ConfigureHtmlShell(shell =>
{
    shell.SetTitle("My App")
         .SetHtmlClass("dark")
         .SetBaseStyles("body { margin: 0; }")
         .AddHeadTag("<link rel=\"icon\" href=\"/favicon.ico\">")
         .AddHeadTag("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">");
});

Properties

Property Type Default Description
EnableSsr bool true Global SSR toggle
EnableDefaultCss bool true Inject default eQuantic CSS (set false with Tailwind)
HtmlShell HtmlShellOptions - HTML template configuration

HtmlShellOptions

Method Description
SetTitle(string) Page <title>
SetHtmlClass(string) Class on <html> element (e.g., "dark")
SetBaseStyles(string) Base <style> block
AddHeadTag(string) Raw HTML tag injected into <head>

SetBaseStyles replaces only the COSMETIC defaults. The structural invariant — #app as a determinate frame (min-height: 100dvh; display: grid), the web mirror of the native window so a page root asking Height = Fill gets the viewport on both targets — is emitted by the shell template itself, before app styles. An app can still override it deliberately; it can no longer wipe it by accident.

Middleware

UseServerActions

Registers the Server Actions middleware for handling RPC calls from the browser.

app.UseServerActions();

Server Actions are methods marked with [ServerAction] that execute server-side and return results to the client:

[ServerAction]
public async Task<List<Todo>> LoadTodos()
{
    using var db = new AppDbContext();
    return await db.Todos.ToListAsync();
}

MapUI

Maps SPA routing — every [Page] route gets an endpoint, and a fallback serves the HTML shell for everything else.

app.MapUI();

The fallback answers an unknown route with a true HTTP 404 — never a 200 that merely looks like one — so crawlers and monitors learn the truth. What renders with that status:

  • The app's own 404 page, when one is declared. Route an ordinary write-once page at "/404" and it becomes the not-found page — SSR'd and client-mounted for every unknown URL, with the app's theme, transpiled into the app's own bundle like any page:

    [Page("/404", Title = "Not found — My App")]
    public sealed class NotFoundScreen : StatelessComponent
    {
        public override VisualNode Build(ComponentContext context) => /* any page */;
    }

    A page routed at "/500" is registered the same way: in production, when SSR of the requested page fails, that page renders with status 500.

  • A styled built-in, otherwise. The runtime paints a minimal theme-aware not-found page (tokens via var(--eq-color-*), OS light/dark fallback when the app selected no theme). The same shared renderer backs the boot error page and the no-page welcome screen — none of them depend on any stylesheet existing.

Browsing straight to /404 hits a mapped page and answers 200; only the fallback speaks 404.

UseTailwind

Enables Tailwind CSS services and dynamic endpoints (theme scripts, dark mode scripts).

builder.Services.AddUI(options =>
{
    options.UseTailwind();
});

UseChartJs / UseApexCharts

Enables services and CDN script endpoints for chart libraries.

builder.Services.AddUI(options =>
{
    options.UseChartJs()
           .UseApexCharts();
});

Hot reload (web)

On in Development (or forced with options.HotReload = true): the server watches the app's *.cs, re-runs the SDK's own eqc target on a save, and tells every connected browser to refresh over SSE (/_equantic/hmr). ~5s from save to pixels with a warm MSBuild.

It had shipped broken in three independent ways, each silent — found by driving the whole circuit in a real browser (the server half worked; the browser half had never been run):

What Symptom
the refreshed page HYDRATED the stale SSR it reloaded and showed the OLD pixels — the one thing you check. The SSR comes from the server's still-running assembly; only the JS was rebuilt. A reload triggered by hot reload now MOUNTS (renders client-side with the new code) instead of adopting the old DOM.
the reload marker was only written when legacy _state existed every write-once page (no _state bag) kept hydrating — same stale pixels
EventSource.onerror → close() any transient drop and hot reload was silently dead for that tab. Removed: the browser reconnects by itself, and abandons the prod 404 on its own.

Plus two robustness holes on the server: the parked SSE request never spoke (idle-dropped by Kestrel and proxies after a couple of minutes — a : ping comment now flows every 20s), and the rebuild read its pipes only after waiting (a failure with >64KB of output deadlocked until the 2-minute timeout — the pipes are read concurrently now, and a successful rebuild logs its duration).

Limit worth knowing: the server's own C# (server actions, SSR bodies) still runs the old assembly — the eqc target refreshes the CLIENT. For server-side edits, run under dotnet watch run: .NET hot reload patches the running server in-process, and this pipeline keeps handling the client half.

The error overlay — a C# stack, because the developer wrote C#

An uncaught error in development raises the Next.js-style modal — message, code frame, call stack — except the stack it shows is C#: Screens/PaymentsPage.cs:441, with the failing lines of the C# file rendered and highlighted. A minified JS stack is noise from a machine the developer never asked to meet.

How it works: the browser walks the error's JS stack through TWO maps. The bundle's own .js.map lands in the TS intermediate (Bun does not compose input maps), and the eqc-generated .ts.map beside that intermediate lands in the C# — file, line, and the source text itself, embedded in the map, served in development at /_equantic/src-map/{name} and 404ing in production. Frames that cannot map all the way stay labeled (js) — a true statement about where mapping stopped beats a guessed C# line.

It had been BUILT in June and had never run once, in four independent ways:

What Why it was silent
the module installed itself via an import side effect nothing ever imported it — it was never in the bundle
the map fetch appended .map after the ?v= cache-buster 404 on every fetch, quietly, since the day it was written
there was no second hop even working, it would have shown TS — a language the dev also never wrote
static helpers had no stage-one map at all their emitter used a local builder the map generator never saw, and their branch returned before map generation ran

Precision today is MEMBER-level: the frame lands in the right file and on the containing member's line (the emitter records mappings per member, not per statement). Statement-level density is the natural next pass.

Server-Side Rendering (SSR)

When SSR is enabled, the framework:

  1. Finds the matching [Page] component for the route
  2. Creates the component instance (with DI support)
  3. Collects asset dependencies (see Assets)
  4. Collects SEO metadata (see below)
  5. Renders the component tree to HTML
  6. Serializes state for client-side hydration
  7. Serves the complete HTML page

SEO & Metadata

Components implement IHandleMetadata for dynamic SEO:

public class BlogPost : StatelessComponent, IHandleMetadata
{
    public void ConfigureMetadata(SeoBuilder seo)
    {
        seo.Title("Blog Post Title")
           .Description("A summary of the post...")
           .Canonical("https://example.com/blog/post")
           .OpenGraph("type", "article")
           .Twitter("card", "summary_large_image");
    }
}

SeoBuilder methods:

Method Description
Title(string) Page title
Description(string) Meta description
Canonical(string) Canonical URL
Image(string, string?) OG image
Keywords(string) Meta keywords
Robots(bool, bool) Index/follow directives
OpenGraph(string, string) OG property
Twitter(string, string) Twitter card property

The recommended way to register UI features is within the AddUI fluent block:

builder.Services.AddUI(options =>
{
    options
        .UseTailwind()
        .UseLucideIcons()
        .UseChartJs()
        .UseApexCharts()
        .ScanAssembly(typeof(Program).Assembly);
});

Middleware order:

app.UseStaticFiles();
app.UseRouting();
app.UseServerActions();    // Before MapUI
app.MapUI();               // SPA fallback + Package endpoints (last)

Clone this wiki locally