Navigation & history
Everything on IBrouter for moving around imperatively - and what
<BrouterLink> adds over a plain anchor.
Navigate, and know how it went
Navigate(url) is fire-and-forget with optional replace (no new
history entry), forceLoad (full page load), and historyState.
NavigateAsync(url) additionally awaits the whole pipeline and reports
a BrouterNavigationOutcome - so calling code can react when a guard cancelled
or redirected, something Blazor's NavigationManager can't tell you.
| Outcome status | Meaning |
|---|---|
| Succeeded | Destination rendered. |
| Cancelled | A guard/lock called Cancel() - URL unchanged. |
| Redirected | A guard redirected; RedirectedTo has the final URL. |
| NotFound | Nothing matched (after NotFound handling ran). |
| Failed | The pipeline threw; Exception carries it. |
| Superseded | A newer navigation started before this one finished. |
History & entry state
Back() / Forward() move through history;
BackAsync(delta) / ForwardAsync(delta) are awaitable and can jump
several entries at once. Each entry can carry state: pass
historyState: when navigating (or HistoryState on a link) and
read it back from brouter.Location.HistoryState - it survives Back/Forward
per entry, unlike anything keyed to the URL. Serialize to JSON for structured payloads.
The router also classifies every navigation as Push, Replace, or
Pop (Back/Forward) - exposed as NavigationType on guard/hook
contexts and used to pick view-transition direction.
brouter.Navigate("/history-state", historyState: "draft-42"); <BrouterLink Href="/history-state" HistoryState="from-a-link">…</BrouterLink> // later, e.g. after the user presses Back onto that entry: var state = brouter.Location.HistoryState;
Functional query updates
Updating one query parameter shouldn't mean rebuilding the whole URL.
NavigateWithQuery hands you a BrouterQueryBuilder over the
current query - mutate what you need, everything else (other params, the fragment) is
preserved, and the history entry is replaced by default so paging doesn't pollute
Back. The builder API: Set (null removes), SetAll (multi-value,
?tag=a&tag=b), Remove, Clear, plus
Get/Contains for reading.
brouter.NavigateWithQuery(q => q.Set("page", page + 1)); brouter.NavigateWithQuery(q => q.SetAll("tag", tags).Remove("page"));
Named routes & relative URLs
A Named route can be targeted without spelling its URL:
NavigateToName(name, parameters) and ResolveUrl(name, parameters)
fill the template (parameters that aren't in the template are appended as query pairs;
enumerable values emit repeated pairs). Pair them with the generated
BrouterRoutes.Names constants - see the
generator page. URLs are also
route-relative everywhere Brouter accepts one (links, Navigate,
guard redirects): ./sibling, ../up, . and
.. resolve against the current path by segment math, clamped at the root.
brouter.NavigateToName(BrouterRoutes.Names.Counter,
new Dictionary<string, object?> { ["init"] = 7 });
<BrouterLink Href="../sibling">Up one, over one</BrouterLink>When the router may not be mounted
Everything above acts on the mounted <Brouter>, so it throws while none is
- before the router initializes, after it's disposed, or in a scope that never renders one.
Code that can't be sure (a scoped service reacting to an event, a teardown path) checks
IsMounted or calls the Try... counterpart, which skips the call and
returns false instead (TryNavigateAsync returns null, and
TryResolveUrl sets its url to null). Only the mount state is forgiven: once mounted, a
Try... call is the original, so an unknown route name still throws.
brouter.TryNavigate("/login"); // false when nothing is mounted var outcome = await brouter.TryNavigateAsync("/admin"); // null when nothing is mounted if (brouter.TryResolveUrl("user", out var url, new Dictionary<string, object?> { ["id"] = 42 })) { … }
BrouterLink
<BrouterLink> renders a real anchor - Ctrl/middle/Shift clicks keep
native browser behavior; only plain left clicks are intercepted.
| Parameter | Behavior |
|---|---|
| Href | Destination; route-relative forms re-resolve after every navigation. |
| Match | Active detection: Prefix (default, segment-boundary aware) or All (exact). Root / always matches exactly. |
| ActiveClass | Class added when active (default "active"); also sets aria-current="page". |
| Replace | Replaces the current history entry instead of pushing. |
| Preload | Intent / Viewport / Render - see Data loading. |
| HistoryState | State attached to the entry created by the click. |
Navigating to the URL you're already on
It does far less than it looks like it does. Blazor reuses a component whose route and
parameters didn't change - the built-in Router behaves the same way - so
Navigate(currentUrl) re-runs the pipeline but leaves the very instance you were
trying to refresh exactly where it was. Three deliberate operations cover what people
actually mean by it, none of them a navigation:
| Call | What it rebuilds |
|---|---|
RevalidateAsync() | Loader data only. Components and their state stay, content stays on screen while it refreshes. See Data loading. |
ClearKeepAlive(includeActive: true) | The component instances, and nothing else: no pipeline, no guards, no loaders - the rebuilt chain keeps the data it already had. See Lifecycle & keep-alive. |
ReloadAsync() | Both: the chain is disposed and matched again, so enter guards, RedirectTo and loaders all re-run (ctx.IsReload). No leave guard can veto it - nothing is being left. |
None of the three moves the URL, the history entry or the scroll position by itself, and none
fires OnNavigating/OnNavigated - the one exception is a guard that
redirects during a reload, which commits a real navigation like any other redirect. The
pipeline page has the
step-by-step comparison.
Not-found handling
When nothing matches, the router follows NotFoundUrl (this demo's
/404 page) or renders an inline <NotFound> fragment without
moving the URL; the OnNotFound callback fires either way. Brouter also
integrates .NET 10's NavigationManager.NotFound(): a component that
discovers mid-render that its entity doesn't exist can hand control to the router's
not-found pipeline while the URL stays put - try it on the outcomes page.