Skip to content

ServerIntegration

Edgar Mesquita edited this page Aug 8, 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()
           .UseTheme(PhotonTheme.Instance)  // Write-once theme
           .UseTailwind()              // Utility CSS (build-time)
           .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 - The selected write-once theme (UseTheme(...); PhotonTheme by default — see DesignSystem)
  • 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 (height: 100dvh; display: grid, children min-height: 0), the web mirror of the native window — is emitted by the shell template itself, before app styles. An APP page (root Height = Fill) gets exactly one viewport and scrolls internally; a DOCUMENT page (auto-height root) overflows the frame and the body scrolls as it always did. An app can still override the rule deliberately; it cannot 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();

On the client, navigation is a full SPA router: no-reload navigation, typed route params, persistent layout via reconcile-on-navigate, guards, prefetch and scroll restoration — verified end-to-end by the Playwright suite.

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.

Behaviors that make the circuit reliable:

  • A reload triggered by hot reload MOUNTS (renders client-side with the new code) instead of hydrating — the SSR still comes from the server's running assembly, so adopting the old DOM would show the old pixels.
  • The SSE channel sends a : ping comment every 20s so idle-dropping proxies and Kestrel keep the parked request alive; the browser's own EventSource reconnection handles transient drops.
  • The rebuild reads its output pipes concurrently and logs its duration.

Scope: this pipeline refreshes the CLIENT — the server's own C# (server actions, SSR bodies) runs the loaded assembly. 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.

Precision 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).

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