Customising Routing Conventions In Razor Pages

At its heart, the Razor Pages routing story is a simple one. It uses a convention of mapping URLs to files on disk. There are ways to customise how pages are found on an individual basis, but what if you want to change the default convention in its entirety? There is also a way to do that. And it's quite simple, as this article will demonstrate, using two real-world examples.

First a very quick recap on how the default convention works (in the simplest of terms). When a Razor Pages application starts up, the framework examines the files located in the Pages folder and generates a set of route templates based on the filepath of each file. If a template has been added to the @page directive, that is also taken into consideration and the generated template is modified accordingly. In this way, it is possible to specify that route data values can or should be part of the URL that matches the file, or to specify that the file is located using a URL that has no relationship at all with its file path. You also have recourse to the AddPageRoute method in Razor Pages Options, but all of these only affect the routing to one file at a time.

Changing the default file

An interesting question came up on GitHub about changing the default file in a folder. Currently it is a file named "Index.cshtml", and this is enforced by the default convention. A lot of applications have "feature folders", folders named after the area of the business that their content is concerned with, such as Contacts, Companies, Administration, Products, Orders etc. Something perhaps like this:

Customising Routing Conventions In Razor Pages

I've actually got an application with 50 or so of these feature folders. In each one there is usually an Index.cshtml file, an Edit.cshtml file, a Create.cshtml file and so on. It is not uncommon for me to have multiple Index.cshtml files open at the same time in Visual Studio, and just like JohnGoldsmith, the author of the issue that was logged, I find navigating from one to another can be a pain, especially if things are playing up and the tooltips take a while to appear when you hover over the file name in the list:

Customising Routing Conventions In Razor Pages

So how about this for a solution? You name each file according to its action and its feature, so the Edit.cshtml file in the Contact folder becomes ContactEdit.cshtml. And you get ContactIndex.cshtml, CompanyIndex.cshtml etc. It is immediately obvious what each file is responsible for. Except that now there are no default documents and users have to get used to a new URL scheme. You can either go in and apply a route template to each file manually, or you can create a new route convention for all the pages in your application.

IPageRouteModelConvention

The IPageRouteModelConvention interface is designed to allow the customisation of the PageRouteModel, an object that lives in the Microsoft.AspNetCore.Mvc.ApplicationModels namespace and represents a Razor Page's routing setup. In other words, you can use this component to override the default conventions.

The interface has one member that needs to be implemented: void Apply(PageRouteModel model). It is in this method that you can access metadata about the current routing set up and modify it as required. The following example solves the problem outlined above so that requests to /contact go to Contact/ContactIndex.cshtml, those that go to /contact/edit reach /Contact/ContactEdit.cshtml etc.

At startup, a PageRouteModel is constructed for all navigable Razor pages. The Apply method takes this object and accesses the collection of SelectorModel objects associated with the PageRouteModel. These contain information about page's route and any constraints. There is usually one SelectorModel in the Selectors collection per page, but there can be any number. The default page, Index.cshtml usually has two selectors - one containing a route template consisting of the relative file path plus "Index" and another that has an empty string in the template where the file name would normally go (which is what makes it the default file for a folder).

In this example, if the template contains a forward slash, it belongs to a file in a folder. The template is divided up into its segments, and if there are two, the template is replaced with one that consists of the folder name followed by the file name with the folder name removed. This means that the original template generated for Contact/ContactEdit.cshtml ("contact/contactedit") becomes "contact/edit". I also replace "Index" with an empty string, and remove any trailing slashes. Therefore the template for Contact/ContactIndex.cshtml becomes "contact".

I have also taken the trouble to enforce a business rule: no nested folders allowed. If the number of segments in the template exceeds two, an exception is raised at application startup. Now, so long as people follow the file naming convention, the new routing convention will be applied. There is no need to remember to specify absolute routes in each page.

The custom convention is registered in Startup where it is added to the RazorPagesOptions.Conventions collection:

Localization of URLs

Another question came up about custom routing, this time on Stackoverflow. The requirement in this instance was to allow users to reach the same page e.g. Contact.cshtml, with a URL in their own language: /contatto, /kontakt, /contacto, /kontakta etc. This can be achieved by adding additional route templates to the Contact page. To illustrate this, here's a simple demo service that gets the translation options for a particular page:

And here is how that service is consumed within a PageRouteModelConvention class:

This time, the Apply method gets any options for the PageRouteModel that's currently being processed, and if there are some, it creates additional templates for the page. This is pretty much what the AddPageRoute method does, but this approach is far more scalable. Just imagine having to use AddPageRoute for 50 pages in 20 languages!

Now the same page can be reached via multiple URLs:

Customising Routing Conventions In Razor Pages

Testability

IPageRouteModelConventions are eminently testable. Here's an example test that ensures that the Index page in the first example has its route modified correctly:

Summary

Chances are that for most Razor Pages applications, the default routing conventions will work just fine. But if you ever need to customise them, the IPageRouteModelConvention interface is what you need. It is scalable, testable and pretty easy to use.