Building a Custom Router for Blazor
Tags: blazor, routing, aspnet, csharp
Published: 2019-11-10 15:12:19

---
title: Building a Custom Router for Blazor
published: true
date: 2019-11-05 09:00:00 UTC
tags: Blazor,Routing,ASP.NET,C#
canonical_url: https://chrissainty.com/building-a-custom-router-for-blazor/
---

 ![Building a Custom Router for Blazor](https://chrissainty.com/content/images/2019/11/building-a-custom-router-for-blazor.jpg)

In this post we are going to build a simple custom router component which will replace the default router Blazor ships with. I just want to say from the start that this isn't meant to be an all singing all dancing replacement for the default router. The default router is quite sophisticated and replicating all that functionality is a bit much for a blog post. Hopefully though, this will give you an idea of what's possible and maybe provide some inspiration.

To help guide things a bit, I want to set a few requirement for our new router, they are:

- Convention based routing - no `@page` directive to be used
- Should be able to handle nested routes e.g. `/nested/mycomponent`
- Parameters to be passed via query string e.g. `/counter?startingCount=4`
- Only support string parameters
- Still allow external links to work
- Re-use code from the default router where possible

> All code from this post is [available on GitHub](https://github.com/chrissainty/BuildingACustomRouterForBlazor)

## The Plan

With the requirements set, let's start by creating a plan for how our new router will work.

The first requirement is that is should be convention based and not use the `@page` directive. In order to achieve this we are going to use namespaces to define a page component. Taking the default project as a base, we'll assume any components in the `ProjectName.Pages.*` namespace are page components.

Taking this approach should also allow us to achieve the second requirement, handling nested routes. If a user requests `https://coolblazorapp.com/admin/settings`, we will look for a component called `Settings.razor` in the following namespace `ProjectName.Pages.Admin`.

As we'll be passing parameters via the query string and only be supporting strings, we'll have to deal with type conversions somehow. We can do this by using the getter and setter on the target parameters to convert incoming values to the correct type. Not very pretty, but it should work for our scenario.

Allowing external links to work should come for free. If you read [last weeks blog post](https://dev.to/chrissainty/an-in-depth-look-at-routing-in-blazor-26hb-temp-slug-3792692) you'll know why. Blazor's JavaScript `NavigationManager` should handle this requirement for us.

We shouldn't have to reinvent the wheel when it comes to rendering page components. Once we have located the correct page component, based on our convention, we should be able to render it using the same `Found` and `NotFound` template approach which is used in the default router. We should also be able to use the existing `RouteView` and `LayoutView` components as well. That takes care of our final requirement to reuse any existing code, if possible.

I think that's everything covered, so lets get into the code.

## Creating The New Router

We're going to start by creating the new router component, named `ConventionRouter`. This is going to be defined as a C# class, the same as the default router is. Here is the full code.

<!--kg-card-begin: markdown-->
```
public class ConventionRouter : IComponent, IHandleAfterRender, IDisposable
{
    RenderHandle _renderHandle;
    bool _navigationInterceptionEnabled;
    string _location;

    [Inject] private NavigationManager NavigationManager { get; set; }
    [Inject] private INavigationInterception NavigationInterception { get; set; }
    [Inject] RouteManager RouteManager { get; set; }

    [Parameter] public RenderFragment NotFound { get; set; }
    [Parameter] public RenderFragment<RouteData> Found { get; set; }

    public void Attach(RenderHandle renderHandle)
    {
        _renderHandle = renderHandle;
        _location = NavigationManager.Uri;
        NavigationManager.LocationChanged += HandleLocationChanged;
    }

    public Task SetParametersAsync(ParameterView parameters)
    {
        parameters.SetParameterProperties(this);

        if (Found == null)
        {
            throw new InvalidOperationException($"The {nameof(ConventionRouter)} component requires a value for the parameter {nameof(Found)}.");
        }

        if (NotFound == null)
        {
            throw new InvalidOperationException($"The {nameof(ConventionRouter)} component requires a value for the parameter {nameof(NotFound)}.");
        }

        RouteManager.Initialise();
        Refresh();

        return Task.CompletedTask;
    }

    public Task OnAfterRenderAsync()
    {
        if (!_navigationInterceptionEnabled)
        {
            _navigationInterceptionEnabled = true;
            return NavigationInterception.EnableNavigationInterceptionAsync();
        }

        return Task.CompletedTask;
    }

    public void Dispose()
    {
        NavigationManager.LocationChanged -= HandleLocationChanged;
    }

    private void HandleLocationChanged(object sender, LocationChangedEventArgs args)
    {
        _location = args.Location;
        Refresh();
    }

    private void Refresh()
    {
        var relativeUri = NavigationManager.ToBaseRelativePath(_location);
        var parameters = ParseQueryString(relativeUri);

        if (relativeUri.IndexOf('?') > -1)
        {
            relativeUri = relativeUri.Substring(0, relativeUri.IndexOf('?'));
        }

        var segments = relativeUri.Trim().Split('/', StringSplitOptions.RemoveEmptyEntries);
        var matchResult = RouteManager.Match(segments);

        if (matchResult.IsMatch)
        {
            var routeData = new RouteData(
                matchResult.MatchedRoute.Handler,
                parameters);

            _renderHandle.Render(Found(routeData));
        }
        else
        {
            _renderHandle.Render(NotFound);
        }
    }

    private Dictionary<string, object> ParseQueryString(string uri)
    {
        var querystring = new Dictionary<string, object>();

        foreach (string kvp in uri.Substring(uri.IndexOf("?") + 1).Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries))
        {
            if (kvp != "" && kvp.Contains("="))
            {
                var pair = kvp.Split('=');
                querystring.Add(pair[0], pair[1]);
            }
        }

        return querystring;
    }
}

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

Let's work through this see how it works. We won't cover every single method, as some of it's self explanatory.

<!--kg-card-begin: markdown-->
```
RenderHandle _renderHandle;
bool _navigationInterceptionEnabled;
string _location;

[Inject] private NavigationManager NavigationManager { get; set; }
[Inject] private INavigationInterception NavigationInterception { get; set; }
[Inject] RouteManager RouteManager { get; set; }

[Parameter] public RenderFragment NotFound { get; set; }
[Parameter] public RenderFragment<RouteData> Found { get; set; }

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

We start by defining some local members and injecting some services - we'll talk about these later. We also define two parameters `Found` and `NotFound`, these are lifted straight from the default router.

<!--kg-card-begin: markdown-->
```
public void Attach(RenderHandle renderHandle)
{
    _renderHandle = renderHandle;
    _location = NavigationManager.Uri;
    NavigationManager.LocationChanged += HandleLocationChanged;
}

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

Next we have the `Attach` method. We have to implement this as we're implementing the `IComponent` interface. Normally this is not something which we'd need to care about as it's dealt with in the `ComponentBase` class which most components inherit from.

This is a low level method which attaches the component to a `RenderHandle`. The `RenderHandle` provides a link between the component and it's renderer, allowing the component to be rendered.

Here we're saving a reference to the `RenderHandle` as well as recording the current URI. We're also registering a handler for the `NavigationManager`'s `LocationChanged` event. This handler updates the routers `_location` field with the new location. Then calls the `Refresh` method to update the view with the new page, if one is found.

<!--kg-card-begin: markdown-->
```
public Task OnAfterRenderAsync()
{
    if (!_navigationInterceptionEnabled)
    {
        _navigationInterceptionEnabled = true;
        return NavigationInterception.EnableNavigationInterceptionAsync();
    }

    return Task.CompletedTask;
}

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

In the `OnAfterRender` method, we're setting up navigation interception. This instructs Blazor to intercept any link click events within the application. If you want to understand how all this works, I suggest [reading my last post](https://dev.to/chrissainty/an-in-depth-look-at-routing-in-blazor-26hb-temp-slug-3792692) which covered this in detail.

<!--kg-card-begin: markdown-->
```
public Task SetParametersAsync(ParameterView parameters)
{
    parameters.SetParameterProperties(this);

    if (Found == null)
    {
        throw new InvalidOperationException($"The {nameof(ConventionRouter)} component requires a value for the parameter {nameof(Found)}.");
    }

    if (NotFound == null)
    {
        throw new InvalidOperationException($"The {nameof(ConventionRouter)} component requires a value for the parameter {nameof(NotFound)}.");
    }

    RouteManager.Initialise();
    Refresh();

    return Task.CompletedTask;
}

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

The `SetParametersAsync` method is also part of the `IComponent` interface. We're doing some basic checks to make sure we have values for the `Found` and `NotFound` parameters.

We then call `RouteManager.Initialise`. We'll look at the `RouteManager` class in detail in the next section but essentially, it's going to go off and find all of the page components in our project and store them.

Finally, we call the `Refresh` method. Let's check that out now.

<!--kg-card-begin: markdown-->
```
private void Refresh()
{
    var relativeUri = NavigationManager.ToBaseRelativePath(_location);
    var parameters = ParseQueryString(relativeUri);

    if (relativeUri.IndexOf('?') > -1)
    {
        relativeUri = relativeUri.Substring(0, relativeUri.IndexOf('?'));
    }

    var segments = relativeUri.Trim().Split('/', StringSplitOptions.RemoveEmptyEntries);
    var matchResult = RouteManager.Match(segments);

    if (matchResult.IsMatch)
    {
        var routeData = new RouteData(
            matchResult.MatchedRoute.Handler,
            parameters);

        _renderHandle.Render(Found(routeData));
    }
    else
    {
        _renderHandle.Render(NotFound);
    }
}

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

Similar to the `Refresh` method on the default router, our version is going to look at the current URI and try and load the correct page component for it. If it can't find a matching page component, then it will render the `NotFound` template.

We start by getting the relative URI and extracting any query string parameters. We store these so they can be passed to the matching page component, if one is found. Once this is complete, we remove the query string from the relative URI, if present. Then split the URI into segments removing any empty ones. The array of segments is then passed to the `RouteManager`'s `Match` method which will attempt to find a page component for that route.

A `MatchResult` is returned which shows if a match was found. If a match was found then the matching `Route` will be included. The `Route` and any parameters found in the query string are then used to construct a `RouteData` object. This is the same `RouteData` object from the default router implementation. The renderer is then instructed to render the `Found` template using the `RouteData` object. This results in the page component being displayed to the user.

If a match isn't found then the renderer is instructed to render the `NotFound` template.

## Finding Page Components With RouteManager

The `RouteManager` class is used to find page components when the application first starts up. It is also responsible for finding page components which match the requested route.

<!--kg-card-begin: markdown-->
```
public class RouteManager
{
    public Route[] Routes { get; private set; }

    public void Initialise()
    {
        var pageComponentTypes = Assembly.GetExecutingAssembly()
                                         .ExportedTypes
                                         .Where(t => t.IsSubclassOf(typeof(ComponentBase))
                                                     && t.Namespace.Contains(".Pages"));

        var routesList = new List<Route>();
        foreach (var pageType in pageComponentTypes)
        {
            var newRoute = new Route
            {
                UriSegments = pageType.FullName.Substring(pageType.FullName.IndexOf("Pages") + 6).Split('.'),
                Handler = pageType
            };

            routesList.Add(newRoute);
        }

        Routes = routesList.ToArray();
    }

    public MatchResult Match(string[] segments)
    {
        if (segments.Length == 0)
        {
            var indexRoute = Routes.SingleOrDefault(x => x.Handler.FullName.ToLower().EndsWith("index"));
            return MatchResult.Match(indexRoute);
        }

        foreach (var route in Routes)
        {
            var matchResult = route.Match(segments);

            if (matchResult.IsMatch)
            {
                return matchResult;
            }
        }

        return MatchResult.NoMatch();
    }
}

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

The `Initialise` method is called in the router's `SetParametersAsync`, we saw that earlier. It uses some reflection to scan the current assembly and find any components with `.Pages` in their namespace, as per our convention we stated at the start.

Once we have the page components we create each one as a `Route`. We break the full name into segments which we will use to compare to the requested route. We also store the handler for the route, which is the type of the component. Once all of the `Routes` are created they're stored as an array on the `RouteManager`.

The `Route` class looks like this.

<!--kg-card-begin: markdown-->
```
public class Route
{
    public string[] UriSegments { get; set; }
    public Type Handler { get; set; }

    public MatchResult Match(string[] segments)
    {
        if (segments.Length != UriSegments.Length)
        {
            return MatchResult.NoMatch();
        }

        for (var i = 0; i < UriSegments.Length; i++)
        {
            if (string.Compare(segments[i], UriSegments[i], StringComparison.OrdinalIgnoreCase) != 0)
            {
                return MatchResult.NoMatch();
            }
        }

        return MatchResult.Match(this);
    }
}

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

It's `Match` method is the most interesting part. It starts by checking if the number of segments in the requested route matches the number of segments it has. If that's not the case then a `NotMatch` result is returned. It then loops over each of it's segments and compares them to the segments passed in. If they all match then a `Match` result is returned, if they don't, then a `NoMatch` result is returned.

Back to the `RouteManager` and it's `Match` method.

<!--kg-card-begin: markdown-->
```
public MatchResult Match(string[] segments)
{
    if (segments.Length == 0)
    {
        var indexRoute = Routes.SingleOrDefault(x => x.Handler.FullName.ToLower().EndsWith("index"));
        return MatchResult.Match(indexRoute);
    }

    foreach (var route in Routes)
    {
        var matchResult = route.Match(segments);

        if (matchResult.IsMatch)
        {
            return matchResult;
        }
    }

    return MatchResult.NoMatch();
}

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

`Match` is called by the router's `Refresh` method. It's job is to find a page component which matches the requested route. It starts by checking if the `segments` length is zero. If it is, we'll assume the request is for the root page, so `https://mycoolblazorapp.com/` for example. By convention, we'll look for a page component called `Index.razor` and return the `MatchResult`.

Otherwise, we'll loop over each route we have stored on the `RouteManager`, calling it's `Match` method. If a match is found, then we'll return it. If we get through all the routes and a match isn't found then we return a `NoMatch` result.

This is what the `MatchResult` class looks like.

<!--kg-card-begin: markdown-->
```
public class MatchResult
{
    public bool IsMatch { get; set; }
    public Route MatchedRoute { get; set; }

    public MatchResult(bool isMatch, Route matchedRoute)
    {
        IsMatch = isMatch;
        MatchedRoute = matchedRoute;
    }

    public static MatchResult Match(Route matchedRoute)
    {
        return new MatchResult(true, matchedRoute);
    }

    public static MatchResult NoMatch()
    {
        return new MatchResult(false, null);
    }
}

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

This is a simple class which gives us a consistent way of returning the result of a route match.

## Summary

I think that about wraps things up. We've built a new router to replace the existing default router. It works on a convention basis and while it is nowhere near as feature rich and flexible as the default one, we've manged to hit all of the requirements set out at the start of the post.

I think it's really cool that Blazor has been built in such as way that we can easily replace parts as we choose. If you would like to see another, and far more sophisticated, example of a custom router for Blazor. I would recommend checking out [this post](https://www.oqtane.org/Resources/Blog/PostId/521/custom-routing-in-blazor) by Shaun Walker who's building an open source CMS using Blazor, called [Oqtane](https://www.oqtane.org/).