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:

PostureEffect
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

ParameterTypeRequiredDefaultDescription
routePrefixstringNo"/trax"The prefix used to build the sidebar navigation links, and nothing else. Leading and trailing slashes are normalized. Leave it at the default.
titlestring?NonullOverrides 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

  1. UseStaticFiles(): serves static assets (CSS, JS)
  2. UseAntiforgery(): CSRF protection for Blazor forms
  3. MapStaticAssets(): maps static web assets from the dashboard RCL
  4. MapRazorComponents<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 before app.Run().
  • The routePrefix is normalized: "trax", "/trax", and "/trax/" all resolve to "/trax". It is written to DashboardOptions.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. With AllowAnonymousDashboard() 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 default ServerAuthenticationStateProvider keeps 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 no HttpContext: a policy handler that reads it as the resource gets null, and the policy's authentication schemes are not re-run. Only the posture is re-checked: conventions added to the returned builder, such as RequireHost, 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).