-
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()
.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() 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 byAddTailwind()) -
IComponentAssetProvider<T>- Auto-scanned from assemblies (see Assets) - 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); // DisableIndividual 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 can no longer 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();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.
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.
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.
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)