Skip to content

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:

HookFires
IBrouter.OnNavigatingBefore every navigation; can Cancel()/Redirect() like a guard. This demo subscribes for analytics-style logging.
IBrouter.OnNavigatedAfter each successful navigation - page titles, telemetry.
IBrouter.OnErrorOn unhandled pipeline exceptions (loader, guard, lifecycle). Cancel/redirect are control flow, not errors.
Brouter OnMatch / OnNotFoundRouter-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.