UseTraxDashboard
Maps the Trax Dashboard Blazor components and serves them at /trax, gated by the
authorization posture chosen in AddTraxDashboard.
Call this after app.Build() during application startup.
It refuses to start without a posture. The dashboard can queue, run and cancel trains and
change scheduler settings, so the host has to say who may use it, in the configure callback
of AddTraxDashboard:
| Posture | Effect |
|---|---|
RequirePolicy("<name>") | Every dashboard endpoint requires the named policy. The policy must be registered with AddAuthorization, or UseTraxDashboard throws at startup. |
RequireRoles("<role>", ...) | Every dashboard endpoint requires one of the roles. Combined with RequirePolicy, both apply. |
AllowAnonymousDashboard() | The dashboard adds no authorization of its own, and a warning is logged on every start. A fallback policy or an ingress rule in front of it still applies. Use it only when one of those is the gate, or for local development. |
With none of them, UseTraxDashboard throws InvalidOperationException naming the three.
AllowAnonymousDashboard() together with either of the others throws as a contradiction.
It refuses to start without the Scheduler. The dashboard's pages queue, run, cancel and
inspect work through IOperationsService, which only
AddScheduler registers. When the built
provider has no IOperationsService, UseTraxDashboard throws InvalidOperationException
naming AddScheduler(), rather than mapping pages that fail on their first request.
"Any authenticated user" is not a posture of its own. On a host with public sign-up it is the same as anonymous. If that is what you mean, register a policy that says so and name it:
builder.Services.AddAuthorization(o =>
o.AddPolicy("TraxDashboard", p => p.RequireAuthenticatedUser())
);
builder.AddTraxDashboard(o => o.RequirePolicy("TraxDashboard"));The mount path is fixed. Every dashboard page carries a compile-time @page "/trax/..."
route template, and MapRazorComponents<App>() applies no prefix, so the pages are reachable
at /trax and nowhere else. routePrefix does not move them.
Signature
public static RazorComponentsEndpointConventionBuilder UseTraxDashboard(
this WebApplication app,
string routePrefix = "/trax",
string? title = null
)Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
routePrefix | string | No | "/trax" | The prefix used to build the sidebar navigation links, and nothing else. Leading and trailing slashes are normalized. Leave it at the default. |
title | string? | No | null | Overrides the dashboard title. null keeps the title from DashboardOptions. |
Returns
The RazorComponentsEndpointConventionBuilder for the dashboard's endpoints: its pages and
the Blazor circuit hub (/_blazor). Conventions added to it, such as RequireHost or a rate
limiter, apply to all of them on top of the posture; they do not replace it. The return type
was WebApplication before the posture was required, so code that chained middleware calls
onto it now calls them on app instead.
Example
builder.Services.AddAuthorization(o =>
o.AddPolicy("TraxAdmin", p => p.RequireRole("Admin"))
);
builder.AddTraxDashboard(o => o.RequirePolicy("TraxAdmin"));
var app = builder.Build();
app.UseTraxDashboard(title: "Order Processing Dashboard")
.RequireHost("admin.example.com");
app.Run();The dashboard is served at https://yourapp/trax.
Passing a different routePrefix produces a broken dashboard rather than a moved one. The
pages stay at /trax while every sidebar link points at the new prefix, so the dashboard is
reachable only by typing /trax and every navigation link inside it 404s.
What It Configures
UseStaticFiles(): serves static assets (CSS, JS)UseAntiforgery(): CSRF protection for Blazor formsMapStaticAssets(): maps static web assets from the dashboard RCLMapRazorComponents<App>().AddInteractiveServerRenderMode(): maps Blazor components with Interactive Server rendering, and applies the posture to every endpoint it maps
Static assets (MapStaticAssets()) are outside the posture. They are the host's whole static
asset manifest, not only the dashboard's files, and gating them would gate the host's own
login page.
Remarks
- Must be called after
builder.Build()and beforeapp.Run(). - The
routePrefixis normalized:"trax","/trax", and"/trax/"all resolve to"/trax". It is written toDashboardOptions.RoutePrefix, which is read in exactly one place,DashboardSidebar, to build the navigation links. This argument is the only way to set it. - The posture is checked on each page request and on the circuit hub's negotiate and
connect. Navigation inside an established circuit does not go back through the endpoint, so
the dashboard also re-checks the posture inside the circuit, against the host's
AuthenticationStateProvider: when the dashboard's root component attaches the circuit, whenever the provider reports a change, every minute, and before each persisted-operation write. Before every inbound circuit message (a click, a change, an interop call) it reads the latest verdict, and once the user is refused it closes the circuit and the page reloads through the endpoint. A failure to evaluate the posture (no provider registered, a policy handler that throws) is a refusal. WithAllowAnonymousDashboard()there is nothing to re-check. - The in-circuit check has three limits. The dashboard registers no
AuthenticationStateProvider, so it sees a revoked role or a sign-out only when the host's provider does; ASP.NET Core's defaultServerAuthenticationStateProviderkeeps the user the connection arrived with, so a host that wants revocation inside an open dashboard registers a revalidating provider or names a policy whose handlers read live state. There is noHttpContext: a policy handler that reads it as the resource getsnull, and the policy's authentication schemes are not re-run. Only the posture is re-checked: conventions added to the returned builder, such asRequireHost, apply at the endpoint only. - Authentication is still the host's. With no scheme that can challenge, a gated request fails rather than being served.
- The dashboard requires the Scheduler, which in turn requires a data provider (UsePostgres or UseInMemory).