Authoring patterns
How to write pages, fragments, layouts, parameters, auth, and redirects with RazorComponentEndpoints.
Authoring patterns
Parameters from the request
Plain [Parameter] properties get auto-bound by name from the request. Source priority: route values → form body → query string.
@page "/items/{Id:int}"
<h1>Item @Id (search: @Q)</h1>
@code {
[Parameter] public int Id { get; set; } // from route value {Id}
[Parameter] public string? Q { get; set; } // from ?q=...
[Parameter] public string? Title { get; set; } // from form field "Title" on POST
}
Types are converted via TypeDescriptor.GetConverter — anything with a string converter works (primitives, Guid, DateTime, enum, …).
Layouts
@* Components/Layouts/MainLayout.razor *@
@inherits LayoutComponentBase
<!DOCTYPE html>
<html>
<head><title>My app</title></head>
<body>@Body</body>
</html>
@page "/"
@layout MainLayout
<h1>Home</h1>
Layout discovery uses LayoutAttribute exactly the way Blazor's RouteView does. Fragments should not declare @layout — they need to render bare for htmx swaps.
Reading HttpContext
@code {
[CascadingParameter] public HttpContext Ctx { get; set; } = default!;
}
HttpContext is supplied as a root cascading value, so any component anywhere in the tree can grab it. Use it to inspect headers, route values, or Ctx.User.
Authorization
<AuthorizeView> and [Authorize] work out of the box. The library supplies Task<AuthenticationState> as a root cascading value, sourced from HttpContext.User. Whatever authentication middleware you've configured (cookies, JWT, Windows auth, …) is what <AuthorizeView> sees.
<AuthorizeView>
<Authorized>Welcome, @context.User.Identity?.Name.</Authorized>
<NotAuthorized><a href="/login">Sign in</a></NotAuthorized>
</AuthorizeView>
For policy-based authorization (Policy="..."), configure policies as you normally would: services.AddAuthorization(o => o.AddPolicy(...)). AddRazorComponentEndpoints() already calls AddAuthorization() and registers the AuthenticationStateProvider.
Server-side redirect
Inject NavigationManager and call NavigateTo. The library catches the underlying NavigationException and emits the right response:
- Normal request → HTTP 302 with
Locationheader. - htmx request (
HX-Request: true) → HTTP 204 withHX-Redirectheader, which tells htmx to do a full browser navigation rather than swap in the redirected page's body.
@page "/old-todos"
@inject NavigationManager Nav
@code {
protected override void OnInitialized() => Nav.NavigateTo("/");
}
404 page
Mark one component per assembly with [NotFoundPage]. It gets registered as the ASP.NET Core fallback endpoint and is rendered with status 404.
@* Components/Pages/NotFound.razor *@
@layout PageShell
@attribute [NotFoundPage]
<h1>Not found</h1>
<p>The path <code>@Ctx.Request.Path</code> doesn't go anywhere.</p>
@code {
[CascadingParameter] public HttpContext Ctx { get; set; } = default!;
}
[NotFoundPage] does not need @page — it isn't a route. The library registers it as the fallback.
DELETE / PUT / PATCH / empty-response endpoints
Component routes only register GET and POST (matching Blazor SSR's surface). Other methods, or endpoints that don't render a component, use plain Minimal API:
app.MapDelete("/api/todos/{id:int}", (int id, TodoStore store) =>
{
store.Delete(id);
return Results.Content("", "text/html"); // htmx swaps in nothing → row disappears
});
Wrapper helpers / scanning a specific assembly
If you call MapRazorComponentEndpoints() from your Program.cs, the library scans the assembly containing Program — what you want. If you call it from a helper in another assembly, Assembly.GetCallingAssembly() returns the wrong thing. The typed overload makes the source explicit:
app.MapRazorComponentEndpoints<Program>(); // scan the assembly containing Program
app.MapRazorComponentEndpoints<SomeMarker>(); // scan whatever assembly defines SomeMarker
If the scan finds zero @page components, the library throws at startup with a message pointing you at the typed overload. Silent emptiness was the original bug; loud failure is the fix.