First a very quick recap on how the
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
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 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
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
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
Localization of URLs
Another question came up about custom routing, this time on
requirement in this instance was to allow users to reach the same page e.g. Contact.cshtml,
with a URL in their own language:
/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
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
Now the same page can be reached via multiple URLs:
IPageRouteModelConventions are eminently testable. Here's an example test that ensures that the Index page in the first example has its route modified correctly:
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.