Gotchas

Things in RazorComponentEndpoints that look like Blazor but aren't.


Gotchas — things that look like Blazor but aren't

These behave differently from standard Blazor Server/SSR. Read this once before you spend an afternoon debugging "why is <EditForm> doing nothing."

OnAfterRender / OnAfterRenderAsync is never called

OnAfterRender* is meant for post-DOM-update interactive work (JS interop, focusing inputs, etc.) — it has no meaning when you're producing a static HTML string. HtmlRenderer doesn't invoke it.

If you're migrating from Blazor Server and reaching for OnAfterRenderAsync(bool firstRender), the right place for setup work is OnInitialized / OnInitializedAsync. The lifecycle method will silently never fire otherwise — no error, no log.

<EditForm> doesn't work

<EditForm>, <DataAnnotationsValidator>, <ValidationSummary>, and friends depend on Blazor SSR's cascading model-binding context, which isn't a thing here. Use a plain <form hx-post="..."> and read posted fields via [Parameter] properties (auto-bound by name from the form body).

[SupplyParameterFromForm] / [SupplyParameterFromQuery] don't work

Same root cause as <EditForm>: these are cascading parameters that only the SSR model-binder provider supplies. If you put them on a property, ParameterView.FromDictionary can't set them — Blazor throws ThrowForSettingCascadingParameterWithNonCascadingValue.

Use plain [Parameter] instead. The library's binder pulls values from route → form → query by name, so the result is equivalent for the common cases.

<PageTitle> / <HeadContent> / <HeadOutlet> don't aggregate

These rely on a special outlet collection mechanism that Blazor SSR wires up at the top of the render tree. The components exist and will render without throwing, but they produce no actual <head> content because there's no <HeadOutlet> collecting it.

Set <title> and <link>/<script> tags directly in your layout component instead.

Route Order from @page is ignored

@page "/{*catchall}" Order="-1" — the Order is metadata that Blazor's own router uses to break ambiguity. The library hands routing to ASP.NET Core's endpoint router instead, which uses specificity-based precedence (literal segments beat parameterized, constrained beat unconstrained, …).

In practice, ASP.NET Core's precedence handles most cases correctly. If two of your routes could both match the same URL and the wrong one wins, make the templates more specific or split into separate components — there's no Order= knob to fall back to.

Only GET and POST on @page routes

Component routes register MapMethods(["GET", "POST"], ...). PUT, PATCH, and DELETE don't route to components — wire them as plain Minimal API endpoints. This mirrors Blazor SSR's HTTP method surface.

Assembly.GetCallingAssembly() and wrapper helpers

Assembly.GetCallingAssembly() returns the immediate caller's assembly. The library decorates the method with [MethodImpl(MethodImplOptions.NoInlining)] to keep that stable, but it can't see past one stack frame. If you wrap the call in your own helper library, the scan looks at the wrong assembly.

Use MapRazorComponentEndpoints<TMarker>() from helpers. If zero components are found, the library throws at startup rather than silently registering nothing — pay attention to the exception message.

<NavLink> checks the current URL against its href to apply an active CSS class. Our NavigationManager reports the URL from the incoming request, so this works for the request being rendered — but there's no client-side navigation watching to update it later. For static rendering this is fine; just know it's "active on the current page," not "active until the user clicks elsewhere."

Render modes (@rendermode InteractiveServer etc.) are silently ignored

The entire point of this library is static rendering. There is no interactive mode. @rendermode attributes are not processed.