Guards & navigation locks
Decide whether a navigation may happen - per route on the way in, per route on the way out, from the component that's about to be left, or globally.
Enter guards
Guard runs before a route (and its children) is entered - root→leaf down the
matched chain. A guard is an async Func<BrouterNavigationContext, ValueTask>:
call ctx.Cancel() to stop the navigation or ctx.Redirect(url) to
send it elsewhere (redirect targets resolve route-relatively against the destination, and a
redirect to the current target is a no-op, so loops can't form). Long-running guards should
honor ctx.CancellationToken - it fires when a newer navigation supersedes this
one. Guards do not run again for RevalidateAsync() or preloads -
but they do for ReloadAsync(), whose chain is matched from
nothing: a guard that means to refuse the rebuilt page should Redirect
rather than Cancel, since a cancelled reload simply puts the page back.
<Broute Path="g1" Guard="@GuardEvenSecond"> ... </Broute> private ValueTask GuardEvenSecond(BrouterNavigationContext ctx) { if (DateTime.Now.Second % 2 != 0) ctx.Redirect("/403"); return ValueTask.CompletedTask; }
Leave guards
LeaveGuard runs when a matched route is about to be left - leaf→root,
and before anything else in the pipeline (global hooks, enter guards).
Crucially it is preventive: cancellation happens before the URL commits, so there
is no address-bar flicker and no history corruption. The classic use is an unsaved-changes
editor. A leave guard does not fire when the same route stays matched with
different parameters - for that, use a navigation lock (below).
<Broute Path="/editor" LeaveGuard="GuardEditorLeave"> ... </Broute> private ValueTask GuardEditorLeave(BrouterNavigationContext ctx) { if (demoState.IsEditorDirty) ctx.Cancel(); return ValueTask.CompletedTask; }
Component-level navigation locks
Sometimes the component owns the veto, not the route declaration. Implement
IBrouterRoute (or derive from BrouterRouteBase) and override
OnDeactivatingAsync / OnRenavigatingAsync: both are awaited
before the navigation commits, may show a custom dialog, and cancel or redirect through
their context. OnRenavigatingAsync closes the gap leave guards can't see -
parameter changes on the same route (/lock/1 → /lock/2). The
context's CancellationToken fires if a newer navigation supersedes the pending
one, letting an open dialog dismiss itself. A thrown exception fails closed: the
navigation is cancelled.
public partial class NavigationLockPage : BrouterRouteBase { protected override async Task OnDeactivatingAsync(BrouterRouteDeactivatingContext ctx) { if (_dirty && !await ShowDialogAsync(ctx.To.Path, ctx.CancellationToken)) ctx.Cancel(); } }
Declarative redirects & global hooks
RedirectTo turns a route into a pure redirect (guards on the route still run
first) - this demo maps / → /home and /lock →
/lock/1 that way. Cross-cutting concerns hang off the router service instead
of any single route:
| Hook | Fires |
|---|---|
| IBrouter.OnNavigating | Before every navigation; can Cancel()/Redirect() like a guard. This demo subscribes for analytics-style logging. |
| IBrouter.OnNavigated | After each successful navigation - page titles, telemetry. |
| IBrouter.OnError | On unhandled pipeline exceptions (loader, guard, lifecycle). Cancel/redirect are control flow, not errors. |
| Brouter OnMatch / OnNotFound | Router-component callbacks when a route matches / nothing matches. |
Leaving the app entirely (tab close, reload, external link) can't be intercepted
by guards - arm the browser's confirmation instead via
BrouterOptions.ConfirmExternalNavigation or at runtime with
brouter.SetConfirmExternalNavigationAsync(true), as the editor demo does while
dirty.