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:
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:
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:
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.