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 Location header.
  • htmx request (HX-Request: true) → HTTP 204 with HX-Redirect header, 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.