Next up in our routing series is an overview of how to use routing in ASP.NET Core 3.0 Razor Pages. If you're not familiar with Razor Pages, you might want to check out some of my other articles about this tech first. This article assumes you are at least somewhat familiar with Razor Pages.

Photo by Justin Lawrence / Unsplash

Routing in Razor Pages is very similar to routing in MVC apps, though with some changes and an extra set of assumptions. Come along with me as we explore the kinds of things we can do with Routing in ASP.NET Core 3.0 Razor Pages!

Setup

In order to use routing in our Razor Pages application, we need to make two changes to the Startup.cs class. First, we need to inject the Razor Pages services into the service layer:

public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();
}

We also need to call UseEndpoints() and map razor pages endpoints, like so:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    ...

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}

With these two changes, we can now begin to use routing in our Razor Pages apps.

Route Assumptions

Routing in Razor Pages relies on a primary assumption: unless you tell ASP.NET Core 3.0 otherwise, the folder and page name of any given Razor Page will be used to make the route to that page.

For example, say we have a Razor Page located at /Testing/MockTests.cshtml. The routing system will assume that this page matches the following route:

<root>/testing/mocktests

If that's all you need, you don't need to do anything else. If you'd like a bit more explanation on the kinds of things you can still do with routing in Razor Pages, read on.

Changing the Default Page

If you'd like to change the application's "landing" or default page to another folder or page, you need to do that in the Startup.cs file as part of RazorPagesOptions:

public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages()
            .AddRazorPagesOptions(options =>
            {
                //Use the below line to change the default directory
                //for your Razor Pages.
                options.RootDirectory = "/YourFolderName";
                
                //Use the below line to change the default
                //"landing page" of the application.
                options.Conventions.AddPageRoute("/YourFolderName/PageName", "");
            });
}

The @Page Directive

Unlike in either convention-based routing or MVC attribute routing, routes for individual Razor Pages are defined in the CSHTML file of the page itself using the @page attribute.

@page "/index"

The string value in this declaration represents is the "route template" to this page.

Areas

Areas are supported "out-of-the-box" by routing in Razor Pages. The folder structure looks something like this:

We have an Areas folder, under which are named folders (in our case, "Blog"). Each of the named folder have their own Pages folder, containing the pages for that area.

If we have area Blog and a Page called Index, the following URLs will be automatically mapped to that Page:

/blog
/blog/index

The routing system looks for an "Index" page in each area, and if it exists, it becomes the "default" page for that area.

Route Templates and Segments

We went over most of this section and the next one in the Overview post, but it's worth talking about it again here.

Routing in Razor Pages, as with convention-based and attribute routing in MVC, supports route templates. These templates specify the format of URLs that are matched to a given page.  

The templates are broken into segments, each of which can be mapped to page values (more on this later). Say we have the following URL:

/post/2020/1/13/this-is-an-article-title

There are five segments in this URL:

  1. post
  2. 2020
  3. 1
  4. 13
  5. this-is-an-article-title

The Routing system allows us to define what each of these segments mean to our application.

We can deduce from reading the URL that the middle three segments are most likely a date (e.g. Jan 13 2020).  The last segment appears to be a URL-encoded title.  The leave the first segment "post" as the one that tells our application which page to direct to.

On the page that needs to respond to the mapped URLs from this template, we can add the following @page directive:

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

But wait! The year, month, and day are not currently restricted to being integers! So the following URL will also match:

/post/test/wrong/date/this-is-an-article-title

Clearly this isn't what we want. We can restrict what kinds of values are expected in each route segment by using Route Constraints.

Route Constraints

A way to tell routing what kinds of values to expect in each route segment is to use a route constraint on that segment. Let's say we have that year, month, and day route template again:

/post/{year}/{month}/{day}/{title}

We need to tell the routing middleware that year, month, and day are expected to be integers, and that title can be absolutely anything. We can do that by modifying the route template to be this:

/post/{year:int}/{month:int}/{day:int}/{**title}

The ":int" syntax tells the routing system that the specified segment must be an integer. There are several other constraints we can use, including ":bool" for boolean values, ":alpha" for alphanumeric values, ":minlength(10)" to set a minimum length of 10 characters, and more. Check the docs for the full list.

The "**" syntax is a special case termed a catch-all; it means that literally anything in this segment is part of the "title" value, including an empty string.

Using [BindProperty] To Map Route Templates

Razor Pages includes a unique feature for its routing system: the attribute [BindProperty]. This attribute allows the routing system to map values from an incoming URL to properties on a Razor Page.

Let's say we want our newly-improved template to map to a particular page:

/post/{year:int}/{month:int}/{day:int}/{**title}

In our sample project, we have the following definition for the DateArticleModel class:

public class DateArticleModel : PageModel
{
    [BindProperty(SupportsGet = true)]
    public int Year { get; set; }

    [BindProperty(SupportsGet = true)]
    public int Month { get; set; }

    [BindProperty(SupportsGet = true)]
    public int Day { get; set; }

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

    public DateTime PublishDate { get; set; }
    public void OnGet()
    {
        PublishDate = new DateTime(Year, Month, Day);
    }
}

We can add our expected route template to the CSHTML page:

@page "/post/{year:int}/{month:int}/{day:int}/{**title}"
@model RoutingAspNetCoreDemo.RazorPages.Areas.Blog.Pages.DateArticleModel
@{
}

<h1>Date Article</h1>

<p>The publish date is @Model.PublishDate.ToString("yyyy/MM/dd")</p>

Now, when a URL that matches the template is found, the values for Year, Month, Day, and Title will be bound to their corresponding values in the DateArticleModel object.

Summary

Routing in Razor Pages is very similar to convention-based routing or attribute routing. Remember the following things:

  • We need to enable routing by calling services.AddRazorPages() and endpoints.MapRazorPages() in Startup.cs
  • You can also change the default directory or page in Startup.cs
  • Route templates support route constraints, which constrain route segments to being comprised of specified values.
  • We define specific routes on pages using the @page directive.
  • Areas are natively supported
  • We can use [BindProperty] to bind route values to page properties.

Don't forget to check out the sample project over on GitHub!

Happy Routing!