An In-depth Look at Routing in Blazor
Tags: blazor, webassembly, dotnet, csharp
Published: 2019-11-10 15:12:11

---
title: An In-depth Look at Routing in Blazor
published: true
date: 2019-10-29 09:00:00 UTC
tags: Blazor,WebAssembly,DotNET,Csharp
canonical_url: https://chrissainty.com/an-in-depth-look-at-routing-in-blazor/
---

 ![An In-depth Look at Routing in Blazor](https://chrissainty.com/content/images/2019/10/an-in-depth-look-at-navigation-in-blazor.jpg)

In this post, I want to build on my last post, [Introduction to Routing in Blazor](https://dev.to/chrissainty/introduction-to-routing-in-blazor-2n3i-temp-slug-2368140), and take a deep dive into the nuts and bolts of routing in Blazor.

We're going to look at each part of Blazor's routing model in detail, starting in the JavaScript world where navigation events are picked up. And following the code over the divide to the C# world, to the point of rendering either the correct page or the not found template.

## Intercepting navigation events with NavigationManager (JavaScript)

We're going to start off looking at the `NavigationManager` service. But this isn't the `NavigationManager` we're used to interacting with in our C# code, this is the JavaScript version.

### Intercepting link clicks

Blazor uses something called an `EventDelegator` to manage the various events produced by DOM elements. This service exposes a function called `notifyAfterClick`, which the `NavigationManager` hooks into in order to intercept navigation link click events. When a navigation link click event occurs the following code is run.

<!--kg-card-begin: markdown-->
```
if (!hasEnabledNavigationInterception) {
  return;
}

if (event.button !== 0 || eventHasSpecialKey(event)) {
  return;
}

if (event.defaultPrevented) {
  return;
}

const anchorTarget = findClosestAncestor(event.target as Element | null, 'A') as HTMLAnchorElement | null;
const hrefAttributeName = 'href';
if (anchorTarget && anchorTarget.hasAttribute(hrefAttributeName)) {
  const targetAttributeValue = anchorTarget.getAttribute('target');
  const opensInSameFrame = !targetAttributeValue || targetAttributeValue === '_self';
  if (!opensInSameFrame) {
    return;
  }

  const href = anchorTarget.getAttribute(hrefAttributeName)!;
  const absoluteHref = toAbsoluteUri(href);

  if (isWithinBaseUriSpace(absoluteHref)) {
    event.preventDefault();
    performInternalNavigation(absoluteHref, true);
  }
}

```
<!--kg-card-end: markdown-->

We're going to break this code down a piece at a time so we can understand it.

First there are some checks being made before anything more invasive is done.

<!--kg-card-begin: markdown-->
```
if (!hasEnabledNavigationInterception) {
  return;
}

if (event.button !== 0 || eventHasSpecialKey(event)) {
  // Don't stop ctrl/meta-click (etc) from opening links in new tabs/windows
  return;
}

if (event.defaultPrevented) {
  return;
}

```
<!--kg-card-end: markdown-->

The first check is to see if navigation interception has been enabled - this gets enabled by Blazor's router component during it's `OnAfterRender` life-cycle method.

Then there's a check to see if the link was clicked with a modifier key being held - for example, holding `ctrl` when clicking a link will open the link in a new tab. If a modifier was being held, then the event is allowed to continue normally and open in a new tab. Finally, a check is made to see if the event has had its default behaviour prevented already.

### Determining internal navigation
<!--kg-card-begin: markdown-->
```
const anchorTarget = findClosestAncestor(event.target as Element | null, 'A') as HTMLAnchorElement | null;
const hrefAttributeName = 'href';

if (anchorTarget && anchorTarget.hasAttribute(hrefAttributeName)) {
  const targetAttributeValue = anchorTarget.getAttribute('target');
  const opensInSameFrame = !targetAttributeValue || targetAttributeValue === '_self';
  
  if (!opensInSameFrame) {
    return;
  }

  const href = anchorTarget.getAttribute(hrefAttributeName)!;
  const absoluteHref = toAbsoluteUri(href);

  if (isWithinBaseUriSpace(absoluteHref)) {
    event.preventDefault();
    performInternalNavigation(absoluteHref, true);
  }
}

```
<!--kg-card-end: markdown-->

The next section of code checks if the target of the click was an `<a>` tag, and if it was, that it has an `href` attribute. If either of these checks fail then the event will be allowed to continue as normal.

Next, a check happens to decide if the link should be opened in the same frame (tab) or not. If not, then again, the event is allowed to continue as normal.

Finally, the value of the `href` attribute is converted to an absolute URI - if it isn't one already.  It's then checked to see if it falls within the scope of the base URI. This is set in the `<head>` tag of either the `index.html` (Blazor WebAssembly) or `_Hosts.cshtml` (Blazor Server) using the `<base>` element.

If the link falls within the scope of the `base` element, then it's considered internal navigation. The `performInternalNavigation` function is called, passing the absolute URI and a boolean value to indicate it was intercepted.

### Simulating browser navigation
<!--kg-card-begin: markdown-->
```
function performInternalNavigation(absoluteInternalHref: string, interceptedLink: boolean) {
  resetScrollAfterNextBatch();

  history.pushState(null, /* ignored title */ '', absoluteInternalHref);
  notifyLocationChanged(interceptedLink);
}

```
<!--kg-card-end: markdown-->

The first call, `resetScrollAfterNextBatch` isn't of much interest to us. It stops unwanted flickering when resetting the scroll position during navigation. But the next part is more interesting.

The new location is pushed into the browsers history. This is what allows the forward and back buttons to function as they would in a traditional web app. By adding the new location to the browsers history it's simulating traditional app navigation. Another important function this action performs is updating the URL in the browsers address bar.

At the end, the `notifyLocationChanged` function is called.

### The gateway to C#
<!--kg-card-begin: markdown-->
```
async function notifyLocationChanged(interceptedLink: boolean) {
  if (notifyLocationChangedCallback) {
    await notifyLocationChangedCallback(location.href, interceptedLink);
  }
}

```
<!--kg-card-end: markdown-->

The final step before we head into the C# world is the `notifyLocationChanged` function above. This function checks if there is a `notifyLocationChangedCallback` and then invokes it, passing the location and whether the link was intercepted.

But where does the `notifyLocationChangedCallback` come from? Well, that depends.

If we're running on WebAssembly then the callback is registered during the application startup in `Boot.WebAssembly.ts`.

<!--kg-card-begin: markdown-->
```
// Configure navigation via JS Interop
window['Blazor']._internal.navigationManager.listenForNavigationEvents(async (uri: string, intercepted: boolean): Promise<void> => {
    await DotNet.invokeMethodAsync(
      'Microsoft.AspNetCore.Blazor',
      'NotifyLocationChanged',
      uri,
      intercepted
    );
});

```
<!--kg-card-end: markdown-->

If we're running on .NET Core (Blazor Server) then the callback is registered in `Boot.Server.ts`.

<!--kg-card-begin: markdown-->
```
// Configure navigation via SignalR
window['Blazor']._internal.navigationManager.listenForNavigationEvents((uri: string, intercepted: boolean): Promise<void> => {
    return connection.send('OnLocationChanged', uri, intercepted);
});

```
<!--kg-card-end: markdown-->
## Navigation Manager (C#)

This leads us into the C# side of things and what responds to the location changed event.

The C# version of `NavigationManager` listens for the location changed event. But the `NavigationManager` class is abstract. There are actually two implementations, one for Blazor Server called `RemoteNavigationManager`. And one for Blazor WebAssembly called `WebAssemblyNavigationManager`.

The `NavigationManager` class performs lots of useful operations, but right now, we're only interested in the `LocationChanged` event. This event gets invoked from different places depending on if we're in a Blazor WebAssembly or Blazor Server application.

### Blazor WebAssembly

When the `NotifyLocationChanged` event is invoked from the JS world it enters the C# world via a class called `JSInteropMethods`.

<!--kg-card-begin: markdown-->
```
public static class JSInteropMethods
{
    /// <summary>
    /// For framework use only.
    /// </summary>
    [JSInvokable(nameof(NotifyLocationChanged))]
    public static void NotifyLocationChanged(string uri, bool isInterceptedLink)
    {
        WebAssemblyNavigationManager.Instance.SetLocation(uri, isInterceptedLink);
    }
}

```
<!--kg-card-end: markdown-->

The `NotifyLocationChanged` method calls the `SetLocation` method on the `WebAssemblyNavigationManager` which looks like this.

<!--kg-card-begin: markdown-->
```
public void SetLocation(string uri, bool isInterceptedLink)
{
    Uri = uri;
    NotifyLocationChanged(isInterceptedLink);
}

```
<!--kg-card-end: markdown-->

This method records the new URI and calls the `NotifyLocationChanged` method on the base `NavigationManager` - this method invokes an event called `LocationChanged`.

### Blazor Server

In this version the `NotifyLocationChanged` event enters the C# world via the `ComponentHub`'s `OnLocationChanged` method.

<!--kg-card-begin: markdown-->
```
public async ValueTask OnLocationChanged(string uri, bool intercepted)
{
    var circuitHost = await GetActiveCircuitAsync();
    if (circuitHost == null)
    {
        return;
    }

    _ = circuitHost.OnLocationChangedAsync(uri, intercepted);
}

```
<!--kg-card-end: markdown-->

This method calls the `CircuitHost`'s `OnLocationChangedAsync` method.

<!--kg-card-begin: markdown-->
```
public async Task OnLocationChangedAsync(string uri, bool intercepted)
{
    AssertInitialized();
    AssertNotDisposed();

    try
    {
        await Renderer.Dispatcher.InvokeAsync(() =>
        {
            Log.LocationChange(_logger, uri, CircuitId);
            var navigationManager = (RemoteNavigationManager)Services.GetRequiredService<NavigationManager>();
            navigationManager.NotifyLocationChanged(uri, intercepted);
            Log.LocationChangeSucceeded(_logger, uri, CircuitId);
        });
    }
    
    // Remaining code omitted for brevity
}

```
<!--kg-card-end: markdown-->

The interesting part for us is in the `try` block. Essentially, an instance of the `RemoteNavigationManager` is being retrieved from the DI container and then it's `NotifyLocationChanged` method is called.

<!--kg-card-begin: markdown-->
```
public void NotifyLocationChanged(string uri, bool intercepted)
{
    Log.ReceivedLocationChangedNotification(_logger, uri, intercepted);

    Uri = uri;
    NotifyLocationChanged(intercepted);
}

```
<!--kg-card-end: markdown-->

In much the same way as the `WebAssemblyNavigationManager`, the new URI is recorded and the `NotifyLocationChanged` method on the base `NavigationManager` is called.

But what's listening?

Technically, it could be a few things. The `NavigationManager`'s `LocationChanged` event is public for anyone to handle after all. But what we're interested in is Blazor's `Router` component.

## The Router Component

When the router is initialised it registers a handler for the `LocationChanged` event. Which looks like this.

<!--kg-card-begin: markdown-->
```
private void OnLocationChanged(object sender, LocationChangedEventArgs args)
{
    _locationAbsolute = args.Location;
    if (_renderHandle.IsInitialized && Routes != null)
    {
        Refresh(args.IsNavigationIntercepted);
    }
}

```
<!--kg-card-end: markdown-->

But in order for the router to function it needs to know what components to load for a particular URI, or route. How does it do this?

### Finding Page Components

We looked at the parameters the router accepts [in the last post](https://dev.to/chrissainty/introduction-to-routing-in-blazor-2n3i-temp-slug-2368140). The router accepts a parameter called `AppAssembly`, which is required. It also accepts another optional parameter, `AdditionalAssemblies`. The router passes these assemblies to a class called `RouteTableFactory` via it's `Create` method.

<!--kg-card-begin: markdown-->
```
public static RouteTable Create(IEnumerable<Assembly> assemblies)
{
    var key = new Key(assemblies.OrderBy(a => a.FullName).ToArray());
    if (Cache.TryGetValue(key, out var resolvedComponents))
    {
        return resolvedComponents;
    }

    var componentTypes = key.Assemblies.SelectMany(a => a.ExportedTypes.Where(t => typeof(IComponent).IsAssignableFrom(t)));
    var routeTable = Create(componentTypes);
    Cache.TryAdd(key, routeTable);
    return routeTable;
}

```
<!--kg-card-end: markdown-->

This method loops over each assembly and pulls out any types which implement `IComponent`. It then passes them to an internal version of `Create` for further processing.

<!--kg-card-begin: markdown-->
```
internal static RouteTable Create(IEnumerable<Type> componentTypes)
{
    var templatesByHandler = new Dictionary<Type, string[]>();
    foreach (var componentType in componentTypes)
    {
        var routeAttributes = componentType.GetCustomAttributes<RouteAttribute>(inherit: false);

        var templates = routeAttributes.Select(t => t.Template).ToArray();
        templatesByHandler.Add(componentType, templates);
    }
    return Create(templatesByHandler);
}

```
<!--kg-card-end: markdown-->

This next method loops over each component and extracts any `RouteAttributes`. It then selects the template for each route. A template being what's in the quotes when using a `@page` directive, `@page "/my/route/template"` for example.

It then adds the component type and it's templates (there can be more than one `@page` directive on a component) to a dictionary which is passed to the final overload of `Create`.

<!--kg-card-begin: markdown-->
```
internal static RouteTable Create(Dictionary<Type, string[]> templatesByHandler)
{
    var routes = new List<RouteEntry>();
    foreach (var keyValuePair in templatesByHandler)
    {
        var parsedTemplates = keyValuePair.Value.Select(v => TemplateParser.ParseTemplate(v)).ToArray();
        var allRouteParameterNames = parsedTemplates
            .SelectMany(GetParameterNames)
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .ToArray();

        foreach (var parsedTemplate in parsedTemplates)
        {
            var unusedRouteParameterNames = allRouteParameterNames
                .Except(GetParameterNames(parsedTemplate), StringComparer.OrdinalIgnoreCase)
                .ToArray();
            var entry = new RouteEntry(parsedTemplate, keyValuePair.Key, unusedRouteParameterNames);
            routes.Add(entry);
        }
    }

    return new RouteTable(routes.OrderBy(id => id, RoutePrecedence).ToArray());
}

```
<!--kg-card-end: markdown-->

In this last method, a `RouteTable` is constructed, which is what will be used later by the `Router` to know which components to load for a given URI.

Essentially, this method does some house keeping to remove any duplication, checks that templates are valid, etc... Before constructing a `RouteEntry`, which holds the route template, component type and any unused route parameters. Finally, a new `RouteTable` is returned.

The router then stores the returned `RouteTable` so it can use it for route lookups during `NavigationChanged` events.

### Loading Page Components

We now understand how the `Router` knows where to find the correct components for a given route. So let's get back to the `OnLocationChanged` method.

<!--kg-card-begin: markdown-->
```
private void OnLocationChanged(object sender, LocationChangedEventArgs args)
{
    _locationAbsolute = args.Location;
    if (_renderHandle.IsInitialized && Routes != null)
    {
        Refresh(args.IsNavigationIntercepted);
    }
}

```
<!--kg-card-end: markdown-->

In the code above the router stores the new URI and then performs some checks. One of which is checking that it has a `RouteTable`. If everything is present and correct the `Refresh` method is called.

<!--kg-card-begin: markdown-->
```
private void Refresh(bool isNavigationIntercepted)
{
    var locationPath = NavigationManager.ToBaseRelativePath(_locationAbsolute);
    locationPath = StringUntilAny(locationPath, _queryOrHashStartChar);
    var context = new RouteContext(locationPath);
    Routes.Route(context);

    if (context.Handler != null)
    {
        if (!typeof(IComponent).IsAssignableFrom(context.Handler))
        {
            throw new InvalidOperationException($"The type {context.Handler.FullName} " +
                $"does not implement {typeof(IComponent).FullName}.");
        }

        Log.NavigatingToComponent(_logger, context.Handler, locationPath, _baseUri);

        var routeData = new RouteData(
            context.Handler,
            context.Parameters ?? _emptyParametersDictionary);
        _renderHandle.Render(Found(routeData));
    }
    else
    {
        if (!isNavigationIntercepted)
        {
            Log.DisplayingNotFound(_logger, locationPath, _baseUri);
            _renderHandle.Render(NotFound);
        }
        else
        {
            Log.NavigatingToExternalUri(_logger, _locationAbsolute, locationPath, _baseUri);
            NavigationManager.NavigateTo(_locationAbsolute, forceLoad: true);
        }
    }
}

```
<!--kg-card-end: markdown-->

We'll work through the code a piece at a time to understand what's going on.

<!--kg-card-begin: markdown-->
```
var locationPath = NavigationManager.ToBaseRelativePath(_locationAbsolute);
locationPath = StringUntilAny(locationPath, _queryOrHashStartChar);
var context = new RouteContext(locationPath);
Routes.Route(context);

```
<!--kg-card-end: markdown-->

The code above is converting the current URL to a relative URL, then stripping off any querystrings (_?name=chris_) or hash strings (_#my-div_). Then a new `RouteContext` is created using the remaining path.  

A `RouteContext` takes the string provided and splits it on each `/` into segments. Finally, the `Route` method is called on the routing table.

Inside the `Route` method, each route in the routing table is checked to see if it matches the route in the `RouteContext` being passed in. This is done by calling the `Match` method on each `RouteEntry`.

<!--kg-card-begin: markdown-->
```
internal void Match(RouteContext context)
{
    if (Template.Segments.Length != context.Segments.Length)
    {
        return;
    }

    // Parameters will be lazily initialized.
    IDictionary<string, object> parameters = null;
    for (int i = 0; i < Template.Segments.Length; i++)
    {
        var segment = Template.Segments[i];
        var pathSegment = context.Segments[i];
        if (!segment.Match(pathSegment, out var matchedParameterValue))
        {
            return;
        }
        else
        {
            if (segment.IsParameter)
            {
                GetParameters()[segment.Value] = matchedParameterValue;
            }
        }
    }

    context.Parameters = parameters;
    context.Handler = Handler;

    IDictionary<string, object> GetParameters()
    {
        if (parameters == null)
        {
            parameters = new Dictionary<string, object>();
        }

        return parameters;
    }
}

```
<!--kg-card-end: markdown-->

The `Match` method first checks to see if the number of segments in the routes are the same. If that succeeds, then each route segment is checked individually to ensure a match.

If the segment on the `RouteEntry` is marked as a parameter, then the value for that segment on the `RouteContext` is added to a parameters collection. Once each segment has been checked, any parameters are added to the `RouteContext` along with the `Handler` for that route, which is the component type.

### A match was found - load the page!
<!--kg-card-begin: markdown-->
```
if (context.Handler != null)
{
    if (!typeof(IComponent).IsAssignableFrom(context.Handler))
    {
        throw new InvalidOperationException($"The type {context.Handler.FullName} " +
            $"does not implement {typeof(IComponent).FullName}.");
    }

    Log.NavigatingToComponent(_logger, context.Handler, locationPath, _baseUri);

    var routeData = new RouteData(
        context.Handler,
        context.Parameters ?? _emptyParametersDictionary);
    _renderHandle.Render(Found(routeData));
}

```
<!--kg-card-end: markdown-->

This executes if a handler was assigned i.e. a match was found for the route. A final check is made to make sure the handler component is definitely implementing `IComponent`. If that passes then the a `RouteData` object is constructed with the handler and any parameters which need to be passed to the handler.

A render is then queued which will use the `Found` template with the route data. This will then render the correct page component and supply it with any necessary parameters.

### No match found - Load not found template
<!--kg-card-begin: markdown-->
```
else
{
    if (!isNavigationIntercepted)
    {
        Log.DisplayingNotFound(_logger, locationPath, _baseUri);
        _renderHandle.Render(NotFound);
    }
    else
    {
        Log.NavigatingToExternalUri(_logger, _locationAbsolute, locationPath, _baseUri);
        NavigationManager.NavigateTo(_locationAbsolute, forceLoad: true);
    }
}

```
<!--kg-card-end: markdown-->

First a check is made to see if the navigation was intercepted. If it wasn't intercepted, this can only occur programmatically, so the `NotFound` template is queued to be rendered.

If it was intercepted then a browser reload is forced to the new location, the main scenario for this would be linking to another page on the same domain which isn't a Blazor component, for example, a standard HTML page or a Razor Page or MVC view.

## Summary

That's it! We've reached the end of the journey. We've followed the flow of navigation events from the source, starting in the JavaScript world all the way to the point of rendering either the correct page component or the not found template.

I hope you've found this post interesting, I've certainly learned a lot about how the mechanics of client-side routers work writing this post. Next time, we'll have a go at writing our own router and replacing the default implementation.