Skip to content

@page discovery

Routes don't have to be hand-declared in one tree - scan assemblies for @page / [Route] components so templates stay colocated with their pages.

Point Brouter at your assemblies

AppAssembly and AdditionalAssemblies mirror the built-in Router's parameters: every component annotated with [Route] (which is what @page compiles to) is discovered and matched alongside any hand-declared <Broute> children - Razor class libraries and lazily-loaded assemblies included. Growing the AdditionalAssemblies list (with a re-render) registers new routes at runtime.

@* Counter.razor - the route lives next to the page *@
@page "/counter/{start:int}"

<h1>Count: @Start</h1>

@code {
    [Parameter] public int Start { get; set; }                       // from {start:int}
    [Parameter, SupplyParameterFromQuery] public string? Tab { get; set; } // from ?tab=
}

@* App router *@
<Brouter AppAssembly="@typeof(App).Assembly" AdditionalAssemblies="@_lazyLoaded">
    <Broute Path="/" RedirectTo="/home" />   @* hand-declared routes still work *@
</Brouter>

Parameter binding is identical

Discovered and hand-declared routes bind the same way: route segments to same-named [Parameter] properties, query values to [Parameter, SupplyParameterFromQuery] properties. Brouter binds the query values itself, so this works even where the framework's query supplier isn't registered. Two framework rules to keep in mind: pair [SupplyParameterFromQuery] with [Parameter] on routed components, and keep its property types within the set the framework supplier can parse - for anything else (enums, for example) use Brouter's opt-in [BrouterQuery] instead (see route parameters).

Shadowing & ambiguity

Registering two routes that match exactly the same URLs throws - mirroring the built-in router's AmbiguousMatchException - instead of silently picking one. The one allowed pairing: a hand-declared <Broute> with the exact template of a discovered @page shadows it, which is how you attach a Guard or Loader to an existing page without touching it.

@* ReportsPage declares @page "/reports" - this shadow adds a guard to it: *@
<Broute Path="/reports" Guard="@RequireSignIn" Component="@typeof(ReportsPage)" />

Lazy route loading

OnNavigateAsync runs before matching on every navigation (initial deep links included). Returned assemblies are scanned and registered within the same navigation, so the URL that triggered the load lands on the freshly-loaded page - the WebAssembly LazyAssemblyLoader scenario without the grow-a-list-and-re-render dance.

<Brouter OnNavigateAsync="@LoadRouteAssemblies" Navigating="@LoadingUi">…</Brouter>

private async ValueTask<IEnumerable<Assembly>?> LoadRouteAssemblies(BrouterNavigationContext ctx)
{
    if (ctx.To.Path.StartsWith("/reports"))
        return await Lazy.LoadAssembliesAsync(["Reports.wasm"]);
    return null;
}

Discovery reflects over the given assemblies - like the built-in Router, keep routable components preserved when trimming.