Blazor Routing & Navigation Manager: Unleash Your Web Dev Skills!

Blazor Routing: Components, Params, and Page Flow

Learn Blazor routing with hands‑on code: @page, params, query strings, , NavigationManager, guards, and 404s.

.NET Development Blazor·By amarozka · October 23, 2025

Blazor Routing: Components, Params, and Page Flow

Have you ever clicked a link in a Blazor app and got a full page reload or, worse, a blank page? That’s not a ghost in the machine – that’s routing done wrong. Let’s fix it for good.

You’ll learn how to:

  • Map routes with @page and route constraints.
  • Pass data with route params and query strings.
  • Move the user with NavigationManager (programmatically) without full reloads.
  • Use <NavLink> so active links highlight on their own.
  • Add auth checks with AuthorizeRouteView and custom guards.
  • Handle 404s, wildcards, and route fallbacks.
  • Test routes with bUnit.

I’ll keep it hands‑on with real code you can paste into your app. When I say “works in my project,” I mean it.

Quick setup (so examples just run)

The snippets below work in Blazor Server and Blazor WebAssembly (hosted or standalone). I’ll assume the standard App.razor router:

<!-- App.razor -->
<CascadingAuthenticationState>
    <Router AppAssembly="typeof(App).Assembly">
        <Found Context="routeData">
            <RouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)" />
        </Found>
        <NotFound>
            <LayoutView Layout="typeof(MainLayout)">
                <h1>Not found</h1>
                <p>Sorry, there’s nothing here.</p>
            </LayoutView>
        </NotFound>
    </Router>
</CascadingAuthenticationState>

Tip: In .NET 8 you can also use AuthorizeRouteView (shown later) to wire in auth directly at the router.

1) Route basics with @page

A component becomes routable when you add @page with a template.

@page "/todos"
<h3>Todo list</h3>

Multiple templates are fine (handy for short and long forms of the same path):

@page "/orders"
@page "/orders/{id:int}"

@code {
    [Parameter] public int? Id { get; set; }
}

Route constraints (type filters) help Blazor pick the right component:

  • int, long, decimal, guid, bool, datetime
  • Example: @page "/users/{userId:guid}"

If a constraint fails, the route doesn’t match. That often explains “why did I land on NotFound?”.

Catch‑all (grab the rest of the path):

@page "/files/{*path}"
@code { [Parameter] public string? Path { get; set; } }

Great for virtual folders or log viewers.

Optional params? Use two @page lines (Blazor doesn’t support optional params in one template):

@page "/products"
@page "/products/{id:int}"

@code { [Parameter] public int? Id { get; set; } }

2) Read route params in a component

Use [Parameter] and matching names:

@page "/orders/{id:int}"
<h3>Order @Id</h3>

@code {
    [Parameter] public int Id { get; set; }
}

Need custom parsing or side effects when the param changes? Hook a lifecycle method:

@page "/reports/{year:int}/{month:int}"

<h3>Report @year-@month</h3>

@code {
    [Parameter] public int year { get; set; }
    [Parameter] public int month { get; set; }

    protected override async Task OnParametersSetAsync()
    {
        // load data each time the url changes to a new year/month
        await LoadAsync(year, month);
    }
}

Gotcha: Parameter names are case‑insensitive in the template but case‑sensitive in your C# code.

3) Query strings with [SupplyParameterFromQuery]

Map query keys to parameters without hand‑parsing the URI.

@page "/search"

<h3>Search</h3>
<input @bind="q" placeholder="term" />
<button @onclick="Apply">Apply</button>

<p>Term: @q</p>
<p>Page: @pageNumber</p>

@code {
    [SupplyParameterFromQuery] public string? q { get; set; }
    [SupplyParameterFromQuery(Name = "page")] public int pageNumber { get; set; } = 1;

    private void Apply()
    {
        // push query back into the url
        var uri = QueryHelpers.AddQueryString("/search", new Dictionary<string, string?>
        {
            ["q"] = q,
            ["page"] = pageNumber.ToString()
        });
        Nav.NavigateTo(uri); // see Nav wrapper below
    }

    [Inject] private NavigationManager Nav { get; set; } = default!;
}

QueryHelpers lives in Microsoft.AspNetCore.WebUtilities.

4) Links that know when they are active

Use <NavLink> instead of <a> to get an active CSS class out of the box.

<NavLink href="/" Match="NavLinkMatch.All">Home</NavLink>
<NavLink href="/orders">Orders</NavLink>
  • Match="All" marks the link active only on the exact path.
  • Without it, the link is active for any child path (e.g., /orders/123).

Style the active link:

.navbar a.active { font-weight: 600; border-bottom: 2px solid currentColor; }

5) Move the user programmatically with NavigationManager

Sometimes a button needs to change the page without a link. Inject NavigationManager and call NavigateTo.

@code {
    [Inject] private NavigationManager Nav { get; set; } = default!;

    void GoToOrder(int id) => Nav.NavigateTo($"/orders/{id}");
}

Do a soft vs hard change

Nav.NavigateTo("/reports", forceLoad: false); // soft SPA change (no full reload)
Nav.NavigateTo("/reports", forceLoad: true);  // hard reload the document

Replace current history entry (avoid back button ping‑pong):

Nav.NavigateTo("/login", replace: true);

Listen for URI changes

@code {
    [Inject] NavigationManager Nav { get; set; } = default!;
    private string? _current;

    protected override void OnInitialized()
    {
        _current = Nav.Uri;
        Nav.LocationChanged += OnLocationChanged;
    }

    private void OnLocationChanged(object? sender, LocationChangedEventArgs e)
    {
        _current = e.Location; // react to changes
        StateHasChanged();
    }

    public void Dispose() => Nav.LocationChanged -= OnLocationChanged;
}

I often use this to cancel in‑flight loads when the user moves away.

6) Route guards: auth and custom checks

a) Auth with AuthorizeRouteView

Protect pages based on roles/policies right in App.razor:

<Router AppAssembly="typeof(App).Assembly">
    <Found Context="routeData">
        <AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)">
            <NotAuthorized>
                <RedirectToLogin />
            </NotAuthorized>
        </AuthorizeRouteView>
    </Found>
    <NotFound>
        <LayoutView Layout="typeof(MainLayout)">
            <p>Nothing here.</p>
        </LayoutView>
    </NotFound>
</Router>

Then mark components:

@attribute [Authorize]
@* or *@
@attribute [Authorize(Roles = "Admin")]

b) Custom “can I enter?” check

For non‑auth rules (e.g., feature flags, tenant state), I keep a small guard base class:

public abstract class GuardedComponentBase : ComponentBase
{
    [Inject] protected NavigationManager Nav { get; set; } = default!;
    protected virtual ValueTask<bool> CanEnterAsync() => ValueTask.FromResult(true);

    protected override async Task OnInitializedAsync()
    {
        if (!await CanEnterAsync())
        {
            Nav.NavigateTo("/forbidden", replace: true);
        }
    }
}

Use it in a page:

@page "/beta"
@inherits GuardedComponentBase

<h3>Beta area</h3>

@code {
    [Inject] IFeatureFlags Flags { get; set; } = default!;

    protected override async ValueTask<bool> CanEnterAsync()
        => await Flags.IsEnabledAsync("beta");
}

7) Leaving a page with unsaved work (NavigationLock)

Warn users before they lose changes:

@page "/edit/{id:int}"
@using Microsoft.AspNetCore.Components.Routing

<NavigationLock ConfirmExternalNavigation="true"
                OnBeforeInternalNavigation="OnBeforeInternalNavigation" />

<EditForm Model="_model" OnValidSubmit="SaveAsync"> ... </EditForm>

@code {
    private bool _dirty;

    private ValueTask OnBeforeInternalNavigation(LocationChangingContext ctx)
    {
        if (_dirty)
        {
            // show your dialog here; cancel and do it later if user chooses to stay
            ctx.PreventNavigation();
        }
        return ValueTask.CompletedTask;
    }
}

This has saved me from many “where did my form go?” bug reports.

8) 404s, wildcards, and fallbacks

You already saw <NotFound> in App.razor. You can go further and mount a special component that shows helpful links, a search box, and a log token.

Wildcard routing example (serve files from storage):

@page "/docs/{*slug}"
@code { [Parameter] public string? slug { get; set; } }

Deep links with hosting

  • Blazor Server: the default endpoint handles deep links. Nothing to add.
  • Blazor WASM: make sure the server (or static host) serves index.html for unknown paths (SPA fallback). In ASP.NET Core host, that’s MapFallbackToFile("index.html").

9) URIs, base path, and relative links

If your app runs under a sub‑path like /app, set the base tag in wwwroot/index.html (WASM) or _Host.cshtml (Server):

<base href="/app/" />

Then use relative links: href="orders" instead of href="/orders". That way your app works both at root and under a sub‑path.

To build a URI safely (with query), use QueryHelpers as shown earlier. Avoid string concatenation for query strings.

10) Layouts and nested sections

Routes render into a layout by default (see DefaultLayout). You can override per page:

@layout AdminLayout

Or set a layout attribute on a base class:

[Layout(typeof(AdminLayout))]
public abstract class AdminPageBase : ComponentBase { }

Then @inherits AdminPageBase in pages that need the admin chrome.

11) Patterns that keep routes tidy

A few rules I follow in real projects:

  • Centralize route strings in one static class to avoid typos:
  public static class Routes
  {
      public const string Home = "/";
      public const string Orders = "/orders";
      public static string Order(int id) => $"/orders/{id}";
  }
  • Prefer NavLink over <a> inside the app. Reserve <a> for external links.
  • Use replace:true after login/logout so the back button doesn’t carry you to a stale page.
  • Handle unknown ids (e.g., /orders/99999) with a friendly message, not a blank page.
  • Avoid heavy work in OnInitialized if it depends on route params. Use OnParametersSetAsync so a param change reloads data.
  • Be strict with constraints to reduce ambiguous matches.

12) Testing routes with bUnit

You can assert that a path renders the expected component.

using Bunit;
using Microsoft.AspNetCore.Components.Routing;

[Fact]
public void Orders_id_route_renders_details()
{
    using var ctx = new TestContext();
    ctx.Services.AddSingleton<NavigationManager>(new TestNavigationManager());

    var cut = ctx.RenderComponent<Router>(ps => ps
        .Add(p => p.AppAssembly, typeof(App).Assembly)
        .Add<Found>(p => p.Found, parms =>
            parms.AddChildContent<RouteView>(rvps => rvps
                .Add(p => p.DefaultLayout, typeof(MainLayout))))
        .Add<NotFound>(p => p.NotFound, parms =>
            parms.AddChildContent("Not found"))
    );

    ctx.Services.GetRequiredService<NavigationManager>()
       .NavigateTo("/orders/42");

    cut.MarkupMatches(markup => markup.Contains("Order 42"));
}

This gives you safety when refactoring paths.

Visual cheat sheet

+---------------------+        click/link/programmatic       +-------------------+
|   /orders           | ---------------------------------->  |   OrdersList.razor|
+---------------------+                                      +-------------------+
           |                                                    |
           | /orders/123                                       | reads [Parameter] Id=123
           v                                                    v
+---------------------+                                      +-------------------+
| /orders/{id:int}    |  ---- constraint match (int) ---->   | OrderDetails.razor|
+---------------------+                                      +-------------------+

Print this section and stick it near your desk. It helps junior devs a lot.

FAQ: common routing questions

How do I get the current URI?

Inject NavigationManager and use Nav.Uri.

What’s the difference between <NavLink> and <a>?

<NavLink> updates the active class and uses SPA‑style changes. <a> should be used for external links or when you need a full reload.

Can I map query strings to [Parameter]?

Yes, with [SupplyParameterFromQuery]. See the search example above.

How do I move the user after saving a form?

Call Nav.NavigateTo(targetPath). If you don’t want the last page in history, pass replace:true.

How do I block leaving a page with unsaved data?

Add NavigationLock and handle OnBeforeInternalNavigation to show a confirm dialog.

Do I need both /orders and /orders/{id} components?

Not always. You can put both views in one component and switch the view based on whether Id is set.

Why does my link reload the whole site?

You likely used <a href="/path"> or forceLoad:true. Use <NavLink> or a button that calls Nav.NavigateTo without forceLoad.

Are route names supported?

Not for components. I keep a Routes static class with helpers (shown above) to reduce string reuse.

Can I run code when the user hits the back button?

Subscribe to Nav.LocationChanged and react there.

How do I support deep links on a static host?

Serve index.html for unknown paths so the router can take over.

Conclusion: solid, simple routing for real apps

If a link lands on the wrong page, if the back button behaves weird, or if a form loses data, users lose trust. With the tools above – clear @page templates, constraints, query mapping, <NavLink>, NavigationManager, AuthorizeRouteView, and NavigationLock – you can build smooth page changes that feel instant and safe. Copy a snippet, try it in your app today, and tell me which tip saved you the most time. Got a trick of your own? Drop it in the comments – I read them all.

Leave a Reply

Your email address will not be published. Required fields are marked *