-
Notifications
You must be signed in to change notification settings - Fork 1
ServerIntegration
eQuantic.UI integrates with ASP.NET Core through a fluent API for service registration, middleware configuration, and HTML shell customization.
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() 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) -
IThemeController- The light/dark hand during SSR (UseInitialThemeMode(...)); the browser's own takes over at hydration - SignalR services
Fluent configuration API for the UI framework.
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);Enables or disables Server-Side Rendering globally. Default is true.
options.WithSsr(); // Enable (default)
options.WithSsr(false); // DisableThe light/dark mode the server renders in — what the browser paints before any JavaScript runs.
Light unless set.
options.UseInitialThemeMode(ThemeMode.Dark);This exists so nothing has to guess. A component offering a theme toggle resolves IThemeController;
in the browser that is the controller which stamps data-theme, but during SSR there is no browser,
and a component that resolved nothing had to assume a mode — an assumption that decides the markup
the reader sees first, so guessing wrong means the first paint is the wrong theme and hydration
corrects it in front of them.
Applying a mode on the server is deliberately inert: the controller is a singleton, so
remembering one request's toggle would hand that visitor's choice to the next visitor's first paint.
An app that wants per-request memory (a cookie, a header) registers its own IThemeController —
this one is registered with TryAdd, so yours wins.
Individual pages can opt-out:
[Page("/interactive", DisableSsr = true)]
public class InteractivePage : StatefulComponent { }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.
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\">");
});| 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 |
| 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.
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();
}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.
Enables Tailwind CSS services and dynamic endpoints (theme scripts, dark mode scripts).
builder.Services.AddUI(options =>
{
options.UseTailwind();
});Enables services and CDN script endpoints for chart libraries.
builder.Services.AddUI(options =>
{
options.UseChartJs()
.UseApexCharts();
});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
: pingcomment every 20s so idle-dropping proxies and Kestrel keep the parked request alive; the browser's ownEventSourcereconnection 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.
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).
When SSR is enabled, the framework:
- Finds the matching
[Page]component for the route - Creates the component instance (with DI support)
- Collects asset dependencies (see Assets)
- Collects SEO metadata (see below)
- Renders the component tree to HTML
- Serializes state for client-side hydration
- Serves the complete HTML page
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)