May 2019

Volume 34 Number 5

[Cutting Edge]

Routing and Route Templates in Blazor

By Dino Esposito | May 2019

Dino EspositoA remarkable difference between the old days of ASP.NET Web Forms and the modern Web is the presence of a routing component at the gate of the Web server. In Web Forms, the vast majority of Web endpoints were physical file resources, invoked directly through their page path.

With ASP.NET MVC, a routing component kicks in whenever the requested URL can’t be mapped to a physical server file. The router takes the requested URL as an instruction to execute that has a client response as its output, whether an HTML view, a JSON payload, a binary stream or other output. The URL can also include optional parameters that help the router determine the specific content to present.

All Web development frameworks today have a routing component, and Blazor is no exception. In this article, I’ll explore the implementation and programming interface of the Blazor routing engine.

The Routing Engine

The Blazor routing engine is a component that runs on the client side. It’s implementation, however, is made of C# code found in one of the assemblies downloaded in the browser and run through the WebAssembly processor. In Blazor applications, the router is currently configured in the app.cshtml file, like so:

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

The following code shows the current content of the program.cs file. As you can see, at the moment it doesn’t include anything related to the router engine, but something is expected to happen later. Or at least, this is what the comment found in the Visual Studio autogenerated app.cshtml files suggests:

public static void Main(string[] args)
{
  BrowserHttpMessageHandler.DefaultCredentials =
    FetchCredentialsOption.Include;
  CreateHostBuilder(args).Build().Run();
}
public static IWebAssemblyHostBuilder CreateHostBuilder(
  string[] args) =>
  BlazorWebAssemblyHost
    .CreateDefaultBuilder()
    .UseBlazorStartup<Startup>();

The router class gets the provided assembly name and searches it, along with all referenced assemblies, for Blazor components that match the URL of the ongoing request. Note that this particular aspect of the router class behavior may evolve in the future toward a model where you have to explicitly specify the assemblies that you want the router to consider. That way you don’t accidentally end up with endpoints that you didn’t expect.

Internally, the router builds up a table of routes and sorts them in a given order. The list of candidate routes results from the list of classes in the explored assemblies that implement the IComponent interface and, more importantly, are decorated with the Route attribute. All collected routes are stored in a dictionary sorted from the most specific to the least specific.

The evaluation algorithm is based on segments discovered in the URL and their position in the string. For example, a literal segment is more specific than a parameter segment, and in turn a parameter segment with more route constraints is considered more specific than others that present fewer constraints. Furthermore, as it happens in ASP.NET MVC, when it comes to resolving a URL, routes in the table are evaluated from most to least specific and the search stops at first match.

On the client-side, the router engages in a number of situations, most commonly when the user clicks a link, a submit button on a form, or an item in a dropdown list that triggers a server call. The router binds to an internal location-changed event and handles—from the client side—the whole process of navigating to the newly requested path. Needless to say, the router kicks in also when the location of the application is changed programmatically. Last, but not least, the router logs in the browser history any location change it’s responsible for, so the back and forward buttons can work as users expect.

A War of Routers: Blazor vs. Angular

For a long time, the implementation of routing logic was buried in the folds of Web servers or server-side frameworks such as ASP.NET. It was with SPA frameworks—of which Angular is the king—that router implementations moved to the client. Let’s take a moment to run a brief comparison of the features in the consolidated Angular router and the still in-the-works router of Blazor.

The bottom line is that the Blazor router at the moment only provides basic functionality as a client-side router. For instance, it lacks the ability to check authorizations on a route and create links that perform view transitions when the location changes. And unlike the Angular router, it cannot work asynchronously to run the resolve step after obtaining route parameters. Finally, the Blazor router lacks support for conditional redirection to alternate routes—something, again, that the Angular router can do.

It’s reasonable to expect that part of this delta will be reduced by the time Blazor ships as version 1.0.

Route Templates

Routing is the process that binds together a URL with a list of known URL patterns. In Blazor, URL patterns, or route templates, are collected in a route table. The table is populated by looking at the components of the Blazor application decorated with the Route attribute. The path to each of those components becomes a supported route template.

At the moment, there’s just one way for developers to control the route path of reachable components: the @page directive. In ASP.NET Core, for example, a developer can define routes explic­itly by adding them to the table programmatically, letting the system figure out candidates using the default route conventions or using attributes on controller methods. If you use Razor pages in ASP.NET Core applications, then you go through exactly the same experience as a Blazor developer—the @page directive.

In summary, each and every Blazor component must specify its route template through the @page directive to be reachable. A Blazor component is made of a .cshtml file that’s compiled into a C# class that implements the IComponent interface. The same dynamically compiled class is also decorated with the Route attribute if the Razor source contains the @page directive.

It’s interesting to note that Blazor supports multiple route directives in the same view. In other words, the following code is well supported:

@page “/”
@page “/home”
<h1>My Home Page</h1>

All routes discovered are put in the same route table container and sorted according to the aforementioned rules. In the previous sample, both route directives are made of literals, so they both go in the top area of the final container and are sorted in order of (relative) appearance.

Routes do support parameters, and a parametric route is recognized at a lower priority in the final table than literal routes, as it’s considered less specific. Here’s an example of a parametric route:

@page “/user/view/{Id}”

The URL pattern-matching algorithm triggers for this route when the URL comprises the server name followed by /user/view/. Any content in the URL that trails /user/view/ is associated with the named parameter {Id}. 

If you’re familiar with ASP.NET MVC (and to a good extent even Web Forms), this model-binding pattern should be old hat. In ASP.NET, route parameters are assigned to the formal parameters of the matched controller method. In Blazor, things are slightly different but comparable.

In Blazor, router parameters are automatically assigned to the properties of the component annotated with the [Parameter] attribute. The matching occurs on the names of parameters and properties. Here’s a quick example:

@page “/user/view/{Id}”
<h1>Hello @Id!</h1>
@functions {
  [Parameter]
  public string Id { get; set; }
  protected override void OnInit()
  {
    // Some code here
  }
}

At the moment, Blazor doesn’t support optional parameters, so if {Id} in the sample URL is missing, the entire URL isn’t matched. To avoid this, the best current workaround is to have two @page directives, with and without the parameter, as shown in the following code:

@page “/user/view/{Id}”
@page “/user/view/”
<h1>Hello @Id!</h1>
@functions {
  [Parameter]
  public string Id { get; set; } = “0”;
  protected override void OnInit()
  {
    // Some code here
  }
}

At the same time, you might also want to give the bound page parameter a default that’s overridden if a value is passed via the URL.

Type matching is a common problem with parametric routes and automatic binding to variables. If the segment of the URL contains a literal string, but the bound variable is declared of type int, what happens? In normal conditions, without any precaution, it probably yields an exception, as a literal value is stuffed into an integer container. If you need to make sure that only values of a given type are specified where a parameter is expected, you should opt for route constraints.

A route constraint is nothing new if you’re familiar with any flavor of ASP.NET MVC. It consists of adding a type attribute to each URL parameter, as shown here:

@page “/user/view/{Id:int}”

The name of the parameter is followed by a colon symbol and a literal that refers to a .NET type. Supported literals match one-to-one most of the .NET primitive types: int, bool, double, float, datetime, long and decimal. For routes with constraints, any parameter values that can’t be converted successfully to the specified type invalidate the match and the route isn’t recognized.

In a Blazor application, you’re welcome to use anchor tags to create links to external content. However, when anchor tags are used to render a menu or a navigation bar, some additional work may be necessary to adjust CSS styles and to reflect the state of the link.

The built-in Blazor NavLink component can be used wherever an anchor element is required, but especially in a menu. The difference between a canonical HTML anchor element and the NavLink component is in the automatic assignment of the “active” style when the current address matches the link. The “active” CSS class is automatically added to the anchor tag rendered by the NavLink component if the current page URL matches the referenced URL. The implementation of the “active” CSS class remains the responsibility of the page developer. The component also includes a property that controls how matching is done. You can do a strict match or a prefix match.

The Blazor router can also be triggered programmatically. To navigate via code from within a Blazor page, you should first inject the configured dependency for the IUriHelper abstract type. The @inject directive does the job, as shown here:

@inject Microsoft.AspNetCore.Blazor.Services.IUriHelper Navigator

The injected object can be commanded via the method NavigateTo. The method takes the URL as an argument:

Navigator.NavigateTo(“/user/view/1”);

The method is conceptually equivalent to setting the href property of the DOM location object in pure JavaScript. In Blazor, though, the router makes the magic of navigating without leaving the client and without fully reloading the content from the server.

What’s Missing

The Blazor framework is an attractive piece of software, but one that remains very much in the works. There are a number of missing routing features—for example, the ability to attach roles or user identities to a route—and authentication and authorization is still incomplete. Any consideration around security-related facilities in routes must wait until those APIs are finalized.

Another important piece of the routing puzzle that’s missing: The ability to fully customize the logic of the router that decides about the target URL. This feature would help developers gain control over invalid link requests. While the Blazor router is far from finished, work continues toward a mature, shipping framework. You can view enhancements to the Blazor routing system tracked by the team at bit.ly/2TtY0DP.


Dino Esposito has authored more than 20 books and 1,000-plus articles in his 25-year career. Author of “The Sabbatical Break,” a theatrical-style show, Esposito is busy writing software for a greener world as the digital strategist at BaxEnergy. Follow him on Twitter: @despos.

Thanks to the following Microsoft technical expert for reviewing this article: Daniel Roth


Discuss this article in the MSDN Magazine forum