Razor Pages Routing

Routing is the system that matches URLs to Razor pages. Like most page-centric frameworks, the primary routing system in ASP.NET Razor Pages is based on matching URLs to file paths, starting from the root Razor Pages folder, which is named Pages by default.

How URLs are matched

When a Razor Pages application starts up, a collection of Attribute Routes (familiar to anyone who has used them in ASP.NET MVC 5 or MVC Core) is constructed, using the file and folder paths rooted in the Pages folder as the basis for each route's template.

The standard Razor Pages site template includes three pages in the root folder:

Error.cshtml
Index.cshtml
Privacy.cshtml

A collection of four routes are defined with the following route templates:

""
"Error"
"Index"
"Privacy"

By default, the route templates are generated by taking the virtual path of each content page and then removing the root folder name from the start and the file extension from the end.

Index.cshtml is considered the default document in any folder, so it has two routes defined - one for the file name without the extension, and one with an empty string representing the file. Therefore, you can access Index.cshtml by browsing to both http://yourdomain.com/ and http://yourdomain.com/index.

If you create a folder named Test and add a file named Index.cshtml to it, a further two routes will be defined with the following templates:

"Test"
"Test/Index"

Both of these routes will be mapped to the same virtual path: /<root>/Test/Index.cshtml.

However, if you now add a file called Test.cshtml to the root pages folder and attempt to browse to it, an exception will be raised:

AmbiguousActionException: Multiple actions matched.
The following actions matched route data and had all constraints satisfied:

Page: /Test/Index
Page: /Test

As the exception message says, it is an error to have a single URL mapped to multiple actions or routes. The framework has no way of knowing which page to call. You can disambiguate between routes by adding route parameters and/or constraints to the template.

Areas

Areas were introduced to Razor Pages in ASP.NET Core 2.1. Routes to resources in areas have the name of the area as the first segment of the URL. In the following example, the area is named Administration.

Areas
    Adminstration
        Pages
            Index.cshtml
            Reports.cshtml
    Production
        Pages
            Index.cshtml
Pages
    Error.cshtml
    Index.cshtml
    Privacy.cshtml

The additional routes created for the content in the areas is as follows:

"Adminstration"
"Administration/Index"
"Administration/Reports"
"Production"
"Production/Index"

The names of the Areas folder and the Pages folder do not feature as part of the route template.

Changing the default Razor Pages root folder

You can use configuration to change the root folder for Razor pages. The following example changes the root folder from the default Pages to Content:

builder.Services.AddRazorPages()
    .AddRazorPagesOptions(options => {
        options.RootDirectory = "/Content";
    });

Or you can use the WithRazorPagesRoot extension method:

builder.Services.AddRazorPages().WithRazorPagesRoot("/Content");
public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages()
    .AddRazorPagesOptions(options => {
        options.RootDirectory = "/Content";
    });
}

Or you can use the WithRazorPagesRoot extension method:

public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages().WithRazorPagesRoot("/Content");
}
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc()
    .AddRazorPagesOptions(options => {
        options.RootDirectory = "/Content";
    });
}

Or you can use the WithRazorPagesRoot extension method:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().WithRazorPagesRoot("/Content");
}

Note that you cannot change the root folder for pages located in areas.

Route Data

Let's say you have created a blog. You may have a page called Post.cshtml in your root pages folder in which you display the content of specific posts. You provide a series of links on your home page to individual posts, and each one includes a value in the URL to identify the specific post to retrieve from the database. You could supply this value as a query string value (www.myblog.com/post?title=my-latest-post), or you could add it as Route Data - a segment in the URL that plays no part in matching files on disk e.g. /my-latest-post in www.myblog.com/post/my-latest-post. The last segment, or parameter is an arbitrary piece of data passed in the URL. The Route Data approach is preferred for a number of reasons, among which it is more readable - especially if you have a number of parameter values - and it is more search engine-friendly.

Route Templates

Route Data parameters are defined in a Route Template as part of the @page directive in the .cshtml file. To cater for the title value in the example above, the declaration at the top of the Post.cshtml file will look like this:

@page "{title}"

The template created for this route is "Post/{title}". The {title} part of the template is a placeholder that represents any value added to the URL after post/. The template definition must appear in double quotes, and the parameter must be enclosed in curly brackets or braces.

In this example, the value is required, so you cannot just browse to /post. You must provide a value in the URL to match the "title" segment, otherwise you will get a status code of 404 - Not Found. However, you can make the parameter optional by adding a ? after it:

@page "{title?}"

Or you can provide a default value for the parameter:

@page "{title=first post}"

There is no limit to the number of parameters you can add to a route, although there is a limit to the data types that you can use as route parameters. Only simple types, such as string, datetime, boolean and numeric types are supported. It is common to see blog post urls include the year, month and day of publication as well as the title. A route definition that accomplishes this might appear as follows:

@page "{year}/{month}/{day}/{title}"

Warning

The following words are reserved for ASP.NET Core routing and should not be used as names for route or handler parameters or query string keys as they are unlikely to be bound correctly:

  • action
  • area
  • controller
  • handler
  • page

Accessing route parameter values

Route parameter values are stored in a RouteValueDictionary accessible via the RouteData.Values property. You reference values by their string-based key:

@RouteData.Values["title"]

The potential problem with this approach is that it relies on referencing values by strings, which are prone to typographical errors, resulting in runtime errors. The recommended alternative is to bind the values to properties on a PageModel. To do this, you can add a public property of a suitable data type to the page model class and a parameter to the OnGet() method with the same name and data type as the route parameter:

public class PostModel : PageModel
{
    public string Title { get; set; }

    public void OnGet(string title)
    {
        Title = title;
    }
}

You assign the parameter value to the public property, which makes it available on the Model property in the content page:

@page "{title?}"
@model PostModel
@{

}
<h2>@Model.Title</h2>

The key reason for recommending this approach is that you benefit from strong typing and therefore IntelliSense support in IDE's that support it:

strong typing

Alternatively, you can use the [BindProperty] attribute on the PageModel property with SupportsGet set to true:

public class PostModel : PageModel
{
    [BindProperty(SupportsGet = true)]
    public string Title { get; set; }

    public void OnGet()
    {
        // the Title property is automatically bound
    }
}

Adding Constraints

Constraints are an additional means of disambiguating between routes. So far, the only constraint placed on a route parameter value is its presence. You can also constrain route parameters values by data type and range. The following example shows how to constrain a parameter value to an integer data type:

@page "{id:int}"

The id value is both required, and must be an integer. The next example illustrates an optional parameter, which must be a double if a value is provided:

@page "{latitude:double?}"

The next example shows use of the min constraint, that ensures that the value supplied is an int and that it meets a minimum value of 10000. The minimum value is supplied in parentheses:

@page "{id:min(10000)}"

The final example shows how to specify multiple constraints using colons:

@page "{username:alpha:minlength(5):maxlength(8)}"

This template specifies that the username values is required (i.e. is not optional), must be composed of a mixture of upper case and lowercase letters (no numbers or other symbols), has a minimum length of 5 characters and a maximum length of 8 characters.

The range of constraints available are extensive, but you can also create your own custom route constraints.

Override Routes

From ASP.NET Core 2.1 onward, you can use the template to specify an alternative route for a page that has no relationship with the file name. The override route template should start with / or ~/. For example, you may have a page located deep in the folder structure somewhere e.g. Pages/Projects/Building/SOP/Schools/Intro.cshtml that you want to surface at an much easier to remember URL: schools/sop. You do this by specifying the URL pattern in the template:

@page "/schools/sop"

This replaces the file-path-based URL.

You can use a similar approach to add segments to a route. This is achieved by omitting the / or ~/ from the start of the template. The following template will require the user to add /schools to the default route that is generated for the page:

@page "schools"

Register Additional Routes

The final piece in the Razor Pages routing jigsaw is based on the "Friendly URLs" feature found in ASP.NET Web Forms (another page-centric development model) which enables you to bypass the tight relationship between URL and the file path and name of the page that's being requested and create additional routes to those pages.

Additional route mappings to pages can be configured by adding options to the RazorPagesOptions.PageConventions collection in the ConfigureServices method in Startup via the AddPageRoute method. In this example, a physical file named Post exists in /Pages/Archive/. You want to enable users to reach it without prepending Archive to the URL, and you want to specify some route parameters. You do that as follows:

builder.Services
    .AddRazorPages()
    .AddRazorPagesOptions(options =>
    {
        options.Conventions.AddPageRoute("/Archive/Post", "Post/{year}/{month}/{day}/{title}");
    });
services
    .AddRazorPages()
    .AddRazorPagesOptions(options =>
    {
        options.Conventions.AddPageRoute("/Archive/Post", "Post/{year}/{month}/{day}/{title}");
    });
services
    .AddMvc()
    .AddRazorPagesOptions(options =>
    {
        options.Conventions.AddPageRoute("/Archive/Post", "Post/{year}/{month}/{day}/{title}");
    });

The AddPageRoute method takes two parameters. The first is the relative path to the Razor page file without the extension and the second is the route template that maps to it.

Unlike Absolute Routes, friendly routes are additive, that is they do not replace existing routes. They act in a similar way to method overloads in programming. It will still be possible to reach the resource above by navigating to /archive/post. Consequently it is possible to add a "catchall" friendly route without affecting routes generated from physical files. The following example illustrates a route that catches any URL that doesn't map to a physical file and gets the Index.cshtml file to process the request:

builder.Services
    .AddRazorPages()
    .AddRazorPagesOptions(options =>
    {
        options.Conventions.AddPageRoute("/index", "{*url}");
    }
services
    .AddRazorPages()
    .AddRazorPagesOptions(options =>
    {
        options.Conventions.AddPageRoute("/index", "{*url}");
    }
services
    .AddMvc()
    .AddRazorPagesOptions(options =>
    {
        options.Conventions.AddPageRoute("/index", "{*url}");
    }

You might do this, for example, if your Index.cshtml file is responsible for locating and processing Markdown files based on the URL, as is the case with this site.

There is also a method for overloading routes to pages in areas: AddAreaPageRoute. This takes the name of the area, the name of the page, and the route template e.g.

builder.Services
    .AddRazorPages()
    .AddRazorPagesOptions(options =>
    {
        options.Conventions.AddAreaPageRoute("Administration", "/index", "admin");
    }
services
    .AddRazorPages()
    .AddRazorPagesOptions(options =>
    {
        options.Conventions.AddAreaPageRoute("Administration", "/index", "admin");
    }
services
    .AddMvc()
    .AddRazorPagesOptions(options =>
    {
        options.Conventions.AddAreaPageRoute("Administration", "/index", "admin");
    }

Other Routing Options

The routing system provides some additional configuration options via properties of the RouteOptions object which can be accessed in the ConfigureServices method. The properties are as follows:

Property Type Description
AppendTrailingSlash bool Appends a trailing slash to URLs generated by the anchor tag helper or UrlHelper. Default is false
ConstraintMap IDictionary<string, Type> Enables the registration of custom constraints via the Add method
LowercaseUrls bool URLs are generated all in lower case. The default is false
LowercaseQueryStrings bool Query strings are generated all in lower case. The default is false. Will only take effect if LowercaseUrls is also true

Examples:

builder.Services.Configure<RouteOptions>(options =>
{
    options.LowercaseUrls = true;
    options.LowercaseQueryStrings = true;
    options.AppendTrailingSlash = true;
    options.ContraintMap.Add("Custom", typeof(CustomConstraint));
});

Generating Urls

Razor Pages provides two main mechanisms for generating URLs to pages within the application, depending on where they are needed.

Anchor Tag Helper

The anchor tag helper is designed to be used to render anchor elements within content pages:

<a asp-page="/Supplier" asp-route-id="2">Click</a>

You can read more about the anchor tag helper here.

The LinkGenerator

The LinkGenerator service is available in Razor Pages 3 onwards. Registered by default with the dependency injection system, you can use the LinkGenerator within classes (such as PageModels) to generate URLs based on the route information provided.

There are two Razor Pages specific methods for generating URLs: GetPathByPage and GetUriByPage. The GetPathByPage method generates a relative URL, and the GetUriByPage method generates an absolute URL:

public class LinkGeneratorDemoModel : PageModel
{
    private LinkGenerator linkGenerator;

    public LinkGeneratorDemoModel(LinkGenerator linkGenerator) => this.linkGenerator = linkGenerator;

    public string PathByPage { get; set; }
    public string UriByPage{ get; set; }

    public void OnGet()
    {
        PathByPage = linkGenerator.GetPathByPage("/Supplier", null, new { id = 2 });
        UriByPage = linkGenerator.GetUriByPage(this.HttpContext, "/Supplier", null, new { id = 2 });
    }
}

Output:

Path By Page: /supplier/2
Uri By Page: https://localhost:5001/supplier/2

Note: this output assumes that the option to use lower case URLS is set to true.

The LinkGenerator service also provides a number of methods for working with URLs related to MVC controllers.

Last updated: 17/03/2024 08:28:08

© 2018 - 2024 - Mike Brind.
All rights reserved.
Contact me at Outlook.com