ASP.NET Core Blazor routing (original) (raw)

This article explains Blazor app request routing with guidance on static versus interactive routing, ASP.NET Core endpoint routing integration, navigation events, and route templates and constraints for Razor components.

Routing in Blazor is achieved by providing a route template to each accessible component in the app with an @page directive. When a Razor file with an @page directive is compiled, the generated class is given a RouteAttribute specifying the route template. At runtime, the router searches for component classes with a RouteAttribute and renders whichever component has a route template that matches the requested URL.

The following HelloWorld component uses a route template of /hello-world, and the rendered webpage for the component is reached at the relative URL /hello-world.

HelloWorld.razor:

@page "/hello-world"

<h1>Hello World!</h1>

The preceding component loads in the browser at /hello-world regardless of whether or not you add the component to the app's UI navigation as a link.

Static versus interactive routing

This section applies to Blazor Web Apps.

If prerendering is enabled, the Blazor router (Router component, <Router> in Routes.razor) performs static routing to components during static server-side rendering (static SSR). This type of routing is called static routing.

When an interactive render mode is assigned to the Routes component, the Blazor router becomes interactive after static SSR with static routing on the server. This type of routing is called interactive routing.

Static routers use endpoint routing and the HTTP request path to determine which component to render. When the router becomes interactive, it uses the document's URL (the URL in the browser's address bar) to determine which component to render. This means that the interactive router can dynamically change which component is rendered if the document's URL dynamically changes to another valid internal URL, and it can do so without performing an HTTP request to fetch new page content.

Interactive routing also prevents prerendering because new page content isn't requested from the server with a normal page request. For more information, see ASP.NET Core Blazor prerendered state persistence.

ASP.NET Core endpoint routing integration

This section applies to Blazor Web Apps operating over a circuit.

A Blazor Web App is integrated into ASP.NET Core Endpoint Routing. An ASP.NET Core app is configured with endpoints for routable components and the root component to render for those endpoints with MapRazorComponents in the Program file. The default root component (first component loaded) is the App component (App.razor):

app.MapRazorComponents<App>();

This section applies to Blazor Server apps operating over a circuit.

Blazor Server is integrated into ASP.NET Core Endpoint Routing. An ASP.NET Core app is configured to accept incoming connections for interactive components with MapBlazorHub in the Program file:

app.UseRouting();

app.MapBlazorHub();
app.MapFallbackToPage("/_Host");

This section applies to Blazor Server apps operating over a circuit.

Blazor Server is integrated into ASP.NET Core Endpoint Routing. An ASP.NET Core app is configured to accept incoming connections for interactive components with MapBlazorHub in Startup.Configure.

The typical configuration is to route all requests to a Razor page, which acts as the host for the server-side part of the Blazor Server app. By convention, the host page is usually named _Host.cshtml in the Pages folder of the app.

The route specified in the host file is called a fallback route because it operates with a low priority in route matching. The fallback route is used when other routes don't match. This allows the app to use other controllers and pages without interfering with component routing in the Blazor Server app.

For information on configuring MapFallbackToPage for non-root URL server hosting, see ASP.NET Core Blazor app base path.

Route templates

The Router component enables routing to Razor components and is located in the app's Routes component (Components/Routes.razor).

The Router component enables routing to Razor components. The Router component is used in the App component (App.razor).

When a Razor component (.razor) with an @page directive is compiled, the generated component class is provided a RouteAttribute specifying the component's route template.

When the app starts, the assembly specified as the Router's AppAssembly is scanned to gather route information for the app's components that have a RouteAttribute.

At runtime, the RouteView component:

Optionally specify a DefaultLayout parameter with a layout class for components that don't specify a layout with the @layout directive. The framework's Blazor project templates specify the MainLayout component (MainLayout.razor) as the app's default layout. For more information on layouts, see ASP.NET Core Blazor layouts.

Components support multiple route templates using multiple @page directives. The following example component loads on requests for /blazor-route and /different-blazor-route.

BlazorRoute.razor:

@page "/blazor-route"
@page "/different-blazor-route"

<h1>Routing Example</h1>

<p>
    This page is reached at either <code>/blazor-route</code> or 
    <code>/different-blazor-route</code>.
</p>

The Router doesn't interact with query string values. To work with query strings, see Query strings.

As an alternative to specifying the route template as a string literal with the @page directive, constant-based route templates can be specified with the @attribute directive.

In the following example, the @page directive in a component is replaced with the @attribute directive and the constant-based route template in Constants.CounterRoute, which is set elsewhere in the app to "/counter":

- @page "/counter"
+ @attribute [Route(Constants.CounterRoute)]

Note

With the release of .NET 5.0.1 and for any additional 5.x releases, the Router component includes the PreferExactMatches parameter set to @true. For more information, see Migrate from ASP.NET Core 3.1 to .NET 5.

Focus an element on navigation

The FocusOnNavigate component sets the UI focus to an element based on a CSS selector after navigating from one page to another.

<FocusOnNavigate RouteData="routeData" Selector="h1" />

When the Router component navigates to a new page, the FocusOnNavigate component sets the focus to the page's top-level header (<h1>). This is a common strategy for ensuring that a page navigation is announced when using a screen reader.

Provide custom content when content isn't found

For requests where content isn't found, a Razor component can be assigned to Router.NotFoundPage. The parameter works in concert with NavigationManager.NotFound, a method called in developer code that triggers a Not Found response.

The Blazor project template includes a NotFound.razor page. This page automatically renders whenever NotFound is called, making it possible to handle missing routes with a consistent user experience.

NotFound.razor:

@page "/not-found"
@layout MainLayout

<h3>Not Found</h3>
<p>Sorry, the content you are looking for does not exist.</p>

The NotFound component is assigned to the router's Router.NotFoundPage parameter, which supports routing that can be used across Status Code Pages Re-execution Middleware, including non-Blazor middleware.

In the following example, the preceding NotFound component is present in the app's Pages folder and passed to the Router.NotFoundPage parameter:

<Router AppAssembly="@typeof(Program).Assembly" NotFoundPage="typeof(Pages.NotFound)">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" />
        <FocusOnNavigate RouteData="@routeData" Selector="h1" />
    </Found>
</Router>

For more information, see the next article on ASP.NET Core Blazor navigation.

The Router component allows the app to specify custom content if content isn't found for the requested route.

Set custom content for the Router component's NotFound parameter:

<Router ...>
    ...
    <NotFound>
        ...
    </NotFound>
</Router>

Arbitrary items are supported as content of the NotFound parameter, such as other interactive components. To apply a default layout to NotFound content, see ASP.NET Core Blazor layouts.

Blazor Web Apps don't use the NotFound parameter (<NotFound>...</NotFound> markup), but the parameter is supported† for backward compatibility in .NET 8/9 to avoid a breaking change in the framework. The server-side ASP.NET Core middleware pipeline processes requests on the server. Use server-side techniques to handle bad requests.

†Supported in this context means that placing <NotFound>...</NotFound> markup doesn't result in an exception, but using the markup isn't effective either.

For more information, see the following resources:

Route to components from multiple assemblies

This section applies to Blazor Web Apps.

Use the Router component's AdditionalAssemblies parameter and the endpoint convention builder AddAdditionalAssemblies to discover routable components in additional assemblies. The following subsections explain when and how to use each API.

Static routing

To discover routable components from additional assemblies for static server-side rendering (static SSR), even if the router later becomes interactive for interactive rendering, the assemblies must be disclosed to the Blazor framework. Call the AddAdditionalAssemblies method with the additional assemblies chained to MapRazorComponents in the server project's Program file.

The following example includes the routable components in the BlazorSample.Client project's assembly using the project's _Imports.razor file:

app.MapRazorComponents<App>()
    .AddAdditionalAssemblies(typeof(BlazorSample.Client._Imports).Assembly);

Interactive routing

An interactive render mode can be assigned to the Routes component (Routes.razor) that makes the Blazor router become interactive after static SSR and static routing on the server. For example, <Routes @rendermode="InteractiveServer" /> assigns interactive server-side rendering (interactive SSR) to the Routes component. The Router component inherits interactive server-side rendering (interactive SSR) from the Routes component. The router becomes interactive after static routing on the server.

Internal navigation for interactive routing doesn't involve requesting new page content from the server. Therefore, prerendering doesn't occur for internal page requests. For more information, see ASP.NET Core Blazor prerendered state persistence.

If the Routes component is defined in the server project, the AdditionalAssemblies parameter of the Router component should include the .Client project's assembly. This allows the router to work correctly when rendered interactively.

In the following example, the Routes component is in the server project, and the _Imports.razor file of the BlazorSample.Client project indicates the assembly to search for routable components:

<Router
    AppAssembly="..."
    AdditionalAssemblies="[ typeof(BlazorSample.Client._Imports).Assembly ]">
    ...
</Router>

Additional assemblies are scanned in addition to the assembly specified to AppAssembly.

Alternatively, routable components only exist in the .Client project with global Interactive WebAssembly or Auto rendering applied, and the Routes component is defined in the .Client project, not the server project. In this case, there aren't external assemblies with routable components, so it isn't necessary to specify a value for AdditionalAssemblies.

This section applies to Blazor Server apps.

Use the Router component's AdditionalAssemblies parameter and the endpoint convention builder AddAdditionalAssemblies to discover routable components in additional assemblies.

In the following example, Component1 is a routable component defined in a referenced component class library named ComponentLibrary:

<Router
    AppAssembly="..."
    AdditionalAssemblies="new[] { typeof(ComponentLibrary.Component1).Assembly }">
    ...
</Router>

Additional assemblies are scanned in addition to the assembly specified to AppAssembly.

Route parameters

The router uses route parameters to populate the corresponding component parameters with the same name. Route parameter names are case insensitive. In the following example, the text parameter assigns the value of the route segment to the component's Text property. When a request is made for /route-parameter-1/amazing, the content is rendered as Blazor is amazing!.

RouteParameter1.razor:

@page "/route-parameter-1/{text}"

<h1>Route Parameter Example 1</h1>

<p>Blazor is @Text!</p>

@code {
    [Parameter]
    public string? Text { get; set; }
}

Optional parameters are supported. In the following example, the text optional parameter assigns the value of the route segment to the component's Text property. If the segment isn't present, the value of Text is set to fantastic.

Optional parameters aren't supported. In the following example, two @page directives are applied. The first directive permits navigation to the component without a parameter. The second directive assigns the {text} route parameter value to the component's Text property.

RouteParameter2.razor:

@page "/route-parameter-2/{text?}"

<h1>Route Parameter Example 2</h1>

<p>Blazor is @Text!</p>

@code {
    [Parameter]
    public string? Text { get; set; }

    protected override void OnParametersSet() => Text = Text ?? "fantastic";
}

When the OnInitialized{Async} lifecycle method is used instead of the OnParametersSet{Async} lifecycle method, the default assignment of the Text property to fantastic doesn't occur if the user navigates within the same component. For example, this situation arises when the user navigates from /route-parameter-2/amazing to /route-parameter-2. As the component instance persists and accepts new parameters, the OnInitialized method isn't invoked again.

Note

Route parameters don't work with query string values. To work with query strings, see Query strings.

Route constraints

A route constraint enforces type matching on a route segment to a component.

In the following example, the route to the User component only matches if:

User.razor:

@page "/user/{Id:int}"

<h1>User Example</h1>

<p>User Id: @Id</p>

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

Note

Route constraints don't work with query string values. To work with query strings, see Query strings.

The route constraints shown in the following table are available. For the route constraints that match the invariant culture, see the warning below the table for more information.

Constraint Example Example Matches Invariantculturematching
bool {active:bool} true, FALSE No
datetime {dob:datetime} 2016-12-31, 2016-12-31 7:32pm Yes
decimal {price:decimal} 49.99, -1,000.01 Yes
double {weight:double} 1.234, -1,001.01e8 Yes
float {weight:float} 1.234, -1,001.01e8 Yes
guid {id:guid} 00001111-aaaa-2222-bbbb-3333cccc4444, {00001111-aaaa-2222-bbbb-3333cccc4444} No
int {id:int} 123456789, -123456789 Yes
long {ticks:long} 123456789, -123456789 Yes
nonfile {parameter:nonfile} Not BlazorSample.styles.css, not favicon.ico Yes

Warning

Route constraints that verify the URL and are converted to a CLR type (such as int or DateTime) always use the invariant culture. These constraints assume that the URL is non-localizable.

Route constraints also work with optional parameters. In the following example, Id is required, but Option is an optional boolean route parameter.

User.razor:

@page "/user/{id:int}/{option:bool?}"

<p>
    Id: @Id
</p>

<p>
    Option: @Option
</p>

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

    [Parameter]
    public bool Option { get; set; }
}

Avoid file capture in a route parameter

The following route template inadvertently captures static asset paths in its optional route parameter (Optional). For example, the app's stylesheet (.styles.css) is captured, which breaks the app's styles:

@page "/{optional?}"

...

@code {
    [Parameter]
    public string? Optional { get; set; }
}

To restrict a route parameter to capturing non-file paths, use the :nonfile constraint in the route template:

@page "/{optional:nonfile?}"

Routing with URLs that contain dots

A server-side default route template assumes that if the last segment of a request URL contains a dot (.) that a file is requested. For example, the relative URL /example/some.thing is interpreted by the router as a request for a file named some.thing. Without additional configuration, an app returns a 404 - Not Found response if some.thing was meant to route to a component with an @page directive and some.thing is a route parameter value. To use a route with one or more parameters that contain a dot, the app must configure the route with a custom template.

Consider the following Example component that can receive a route parameter from the last segment of the URL.

Example.razor:

@page "/example/{param?}"

<p>
    Param: @Param
</p>

@code {
    [Parameter]
    public string? Param { get; set; }
}

To permit the Server app of a hosted Blazor WebAssembly solution to route the request with a dot in the param route parameter, add a fallback file route template with the optional parameter in the Program file:

app.MapFallbackToFile("/example/{param?}", "index.html");

To configure a Blazor Server app to route the request with a dot in the param route parameter, add a fallback page route template with the optional parameter in the Program file:

app.MapFallbackToPage("/example/{param?}", "/_Host");

For more information, see Routing in ASP.NET Core.

To permit the Server app of a hosted Blazor WebAssembly solution to route the request with a dot in the param route parameter, add a fallback file route template with the optional parameter in Startup.Configure.

Startup.cs:

endpoints.MapFallbackToFile("/example/{param?}", "index.html");

To configure a Blazor Server app to route the request with a dot in the param route parameter, add a fallback page route template with the optional parameter in Startup.Configure.

Startup.cs:

endpoints.MapFallbackToPage("/example/{param?}", "/_Host");

For more information, see Routing in ASP.NET Core.

Catch-all route parameters

Catch-all route parameters, which capture paths across multiple folder boundaries, are supported in components.

Catch-all route parameters are:

CatchAll.razor:

@page "/catch-all/{*pageRoute}"

<h1>Catch All Parameters Example</h1>

<p>Add some URI segments to the route and request the page again.</p>

<p>
    PageRoute: @PageRoute
</p>

@code {
    [Parameter]
    public string? PageRoute { get; set; }
}

For the URL /catch-all/this/is/a/test with a route template of /catch-all/{*pageRoute}, the value of PageRoute is set to this/is/a/test.

Slashes and segments of the captured path are decoded. For a route template of /catch-all/{*pageRoute}, the URL /catch-all/this/is/a%2Ftest%2A yields this/is/a/test*.

<Router AppAssembly="typeof(App).Assembly" 
    OnNavigateAsync="OnNavigateAsync">
    ...
</Router>

@code {
    private async Task OnNavigateAsync(NavigationContext args)
    {
        ...
    }
}
<Router AppAssembly="typeof(Program).Assembly" 
    OnNavigateAsync="OnNavigateAsync">
    ...
</Router>

@code {
    private async Task OnNavigateAsync(NavigationContext args)
    {
        ...
    }
}

Handle cancellations in OnNavigateAsync

The NavigationContext object passed to the OnNavigateAsync callback contains a CancellationToken that's set when a new navigation event occurs. The OnNavigateAsync callback must throw when this cancellation token is set to avoid continuing to run the OnNavigateAsync callback on an outdated navigation.

If a user navigates to an endpoint but then immediately navigates to a new endpoint, the app shouldn't continue running the OnNavigateAsync callback for the first endpoint.

In the following example:

@inject HttpClient Http
@inject ProductCatalog Products

<Router AppAssembly="typeof(App).Assembly" 
    OnNavigateAsync="OnNavigateAsync">
    ...
</Router>

@code {
    private async Task OnNavigateAsync(NavigationContext context)
    {
        if (context.Path == "/about") 
        {
            var stats = new Stats { Page = "/about" };
            await Http.PostAsJsonAsync("api/visited", stats, 
                context.CancellationToken);
        }
        else if (context.Path == "/store")
        {
            var productIds = new[] { 345, 789, 135, 689 };

            foreach (var productId in productIds) 
            {
                context.CancellationToken.ThrowIfCancellationRequested();
                Products.Prefetch(productId);
            }
        }
    }
}
@inject HttpClient Http
@inject ProductCatalog Products

<Router AppAssembly="typeof(Program).Assembly" 
    OnNavigateAsync="OnNavigateAsync">
    ...
</Router>

@code {
    private async Task OnNavigateAsync(NavigationContext context)
    {
        if (context.Path == "/about") 
        {
            var stats = new Stats { Page = "/about" };
            await Http.PostAsJsonAsync("api/visited", stats, 
                context.CancellationToken);
        }
        else if (context.Path == "/store")
        {
            var productIds = new[] { 345, 789, 135, 689 };

            foreach (var productId in productIds) 
            {
                context.CancellationToken.ThrowIfCancellationRequested();
                Products.Prefetch(productId);
            }
        }
    }
}

Note

Not throwing if the cancellation token in NavigationContext is canceled can result in unintended behavior, such as rendering a component from a previous navigation.