ASP.NET Core in Action 14 Mapping URLs to Razor Pages using routing

14 Mapping URLs to Razor Pages using routing

This chapter covers
• Routing requests to Razor Pages
• Customizing Razor Page route templates
• Generating URLs for Razor Pages

In chapter 13 you learned about the Model-View-Controller (MVC) design pattern and how ASP.NET Core uses it to generate the UI for an application using Razor Pages. Razor Pages contain page handlers that act as mini controllers for a request. The page handler calls the application model to retrieve or save data. Then the handler passes data from the application model to the Razor view, which generates an HTML response.

Although not part of the MVC design pattern per se, one crucial part of Razor Pages is selecting which Razor Page to invoke in response to a given request. Razor Pages use the same routing system as minimal APIs (introduced in chapter 6); this chapter focuses on how routing works with Razor Pages.

I start this chapter with a brief reminder about how routing works in ASP.NET Core. I’ll touch on the two pieces of middleware that are crucial to endpoint routing in .NET 7 and the approach Razor Pages uses of mixing conventions with explicit route templates.

In section 14.3 we look at the default routing behavior of Razor Pages, and in section 14.4 you’ll learn how to customize the behavior by adding or changing route templates. Razor Pages have access to the same route template features that you learned about in chapter 6, and in section 14.4 you’ll learn how to them.

In section 14.5 I describe how to use the routing system to generate URLs for Razor Pages. Razor Pages provide some helper methods to simplify URL generation compared with minimal APIs, so I compare the two approaches and discuss the benefits of each.

Finally, in section 14.6 I describe how to customize the conventions Razor Pages uses, giving you complete control of the URLs in your application. You’ll see how to change the built-in conventions, such as using lowercase for your URLs, as well as how to write your own convention and apply it globally to your application.

By the end of this chapter you should have a much clearer understanding of how an ASP.NET Core application works. You can think of routing as the glue that ties the middleware pipeline to Razor Pages and the MVC framework. With middleware, Razor Pages, and routing under your belt, you’ll be writing web apps in no time!

14.1 Routing in ASP.NET Core

In chapter 6 we looked in detail at routing and some of the benefits it brings, such as the ability to have multiple URLs pointing to the same endpoint and extracting segments from the URL. You also learned how it’s implemented in ASP.NET Core apps, using two pieces of middleware:

EndpointMiddleware—You use this middleware to register the endpoints in the routing system when you start your application. The middleware executes one of the endpoints at runtime.
RoutingMiddleware—This middleware chooses which of the endpoints registered by the EndpointMiddleware should execute for a given request at runtime.
The EndpointMiddleware is where you register all the endpoints in your app, including minimal APIs, Razor Pages, and MVC controllers. It’s easy to register all the Razor Pages in your application using the MapRazorPages() extension method, as shown in the following listing.

Listing 14.1 Registering Razor Pages in Startup.Configure

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages(); ❶
var app = builder.Build();
app.UseStaticFiles();
app.UseRouting(); ❷
app.UseAuthorization();
app.MapRazorPages(); ❸
app.Run();

❶ Adds the required Razor Pages services to dependency injection
❷ Adds the RoutingMiddleware to the middleware pipeline
❸ Registers all the Razor Pages in the application with the EndpointMiddleware

Each endpoint, whether it’s a Razor Page or a minimal API, has an associated route template that defines which URLs the endpoint should match. The EndpointMiddleware stores these route templates and endpoints in a dictionary, which it shares with the RoutingMiddleware. At runtime the RoutingMiddleware compares the incoming request with the routes in the dictionary and selects the matching endpoint. When the request reaches the EndpointMiddleware, the middleware checks to see which endpoint was selected and executes it, as shown in figure 14.1.

alt text

Figure 14.1 Endpoint routing uses a two-step process. The RoutingMiddleware selects which endpoint to execute, and the EndpointMiddleware executes it. If the request URL doesn’t match a route template, the endpoint middleware will not generate a response.

As discussed in chapter 6, the advantage of having two separate pieces of middleware to handle this process is that any middleware placed after the RoutingMiddleware can see which endpoint is going to be executed before it is. You’ll see this benefit in action when we look at authorization in chapter 24.

Routing in ASP.NET Core uses the same infrastructure and middleware whether you’re building minimal APIs, Razor Pages, or MVC controllers, but there are some differences in how you define the mapping between your route templates and your handlers in each case. In section 14.2 you’ll learn the different approaches each paradigm takes.

14.2 Convention-based routing vs. explicit routing

Routing is a key part of ASP.NET Core, as it maps the incoming request’s URL to a specific endpoint to execute. You have two ways to define these URL-endpoint mappings in your application:

• Using global, convention-based routing
• Using explicit routing, where each endpoint is mapped with a single route template

Which approach you use typically depends on whether you’re using minimal APIs, Razor Pages, or MVC controllers and whether you’re building an API or a website (using HTML). These days I lean heavily toward explicit routing, as you’ll see.

Convention-based routing is defined globally for your application. You can use convention-based routes to map endpoints (MVC controller actions specifically) to URLs, but those MVC controllers must adhere strictly to the conventions you define. Traditionally, applications using MVC controllers to generate HTML tend to use this approach to routing. The downside of this approach is that customizing URLs for a subset of controllers and actions is tricky.

Alternatively, you can use explicit routing to tie a given URL to a specific endpoint. You’ve seen this approach with minimal APIs, where each endpoint is directly associated with a route template. You can also use explicit routing with MVC controllers by placing [Route] attributes on the action methods themselves, hence explicit-routing is also often called attribute-routing.

Explicit routing provides more flexibility than convention-based based routing, as you can explicitly define the route template for every action method. Explicit routing is generally more verbose than the convention-based approach, as it requires applying attributes to every action method in your application. Despite this, the extra flexibility can be useful, especially when building APIs.

Somewhat confusingly, Razor Pages use conventions to generate explicit routes! In many ways this combination gives you the best of both worlds: the predictability and terseness of convention-based routing with the easy customization of explicit routing. There are tradeoffs to each of the approaches, as shown in table 14.1.

Table 14.1 The advantages and disadvantages of the routing styles available in ASP.NET Core

Routing style Typical use Advantages Disadvantages
Convention-based routes HTML-generating MVC controllers Terse definition in one location in your application. Forces a consistent layout of MVC controllers. Routes are defined in a different place from your controllers. Overriding the route conventions can be tricky and error-prone. Adds an extra layer of indirection when routing a request.
Explicit routes Minimal API endpoints, Web API MVC controllers Gives complete control of route templates for every endpoint.Routes are defined next to the endpoint they execute. Verbose compared with convention-based routing.Can be easy to overcustomize route templates.Route templates may be scattered throughout your application rather than defined in one location.
Convention-based generation of explicit routes Razor Pages Encourages consistent set of exposed URLs. Terse when you stick to the conventions. Easily override the route template for a single page. Customize conventions globally to change exposed URLs. Possible to overcustomize route templates. You must calculate what the route template for a page is, rather than its being explicitly defined in your app.

So which approach should you use? I believe that convention-based routing is not worth the effort in 99 percent of cases and that you should stick to explicit routing. If you’re following my advice to use Razor Pages for server-rendered applications, you’re already using explicit routing under the covers. Also, if you’re creating APIs using minimal APIs or MVC controllers, explicit routing is the best option and the recommended approach.

The only scenario where convention-based routing is used traditionally is if you’re using MVC controllers to generate HTML. But if you’re following my advice from chapter 13, you’ll be using Razor Pages for HTML-generating applications and falling back to MVC controllers only when necessary, as I discuss in more detail in chapter 19. For consistency, I would often stick with explicit routing with attributes in that scenario too.

NOTE For the reasons above, this book focuses on explicit/attribute routing. For details on convention-based routing, see Microsoft’s “Routing to controller actions in ASP.NET Core” documentation at http://mng.bz/ZP0O.

You learned about routing and route templates in chapter 6 in the context of minimal APIs. The good news is that exactly the same patterns and features are available with Razor Pages. The main difference with minimal APIs is that Razor Pages use conventions to generate the route template for a page, though you can easily change the template on a page-by-page basis. In section 14.3 we look at the default conventions and how routing maps a request’s URL to a Razor Page in detail.

14.3 Routing requests to Razor Pages

As I mentioned in section 14.2, Razor Pages use explicit routing by creating route templates based on conventions. ASP.NET Core creates a route template for every Razor Page in your app during app startup, when you call MapRazorPages() in Program.cs:

app.endpoints.MapRazorPages();

For every Razor Page in your application, the framework uses the path of the Razor Page file relative to the Razor Pages root directory (Pages/), excluding the file extension (.cshtml). If you have a Razor Page located at the path Pages/Products/View.cshtml, the framework creates a route template with the value "Products/View", as shown in figure 14.2.

alt text

Figure 14.2 By default, route templates are generated for Razor Pages based on the path of the file relative to the root directory, Pages.

Requests to the URL /products/view match the route template "Products/View", which in turn corresponds to the View.cshtml Razor Page in the Pages/Products folder. The RoutingMiddleware selects the View.cshtml Razor Page as the endpoint for the request, and the EndpointMiddleware executes the page’s handler when the request reaches it in the middleware pipeline.

NOTE Remember that routing is not case-sensitive, so the request URL will match even if it has a different URL casing from the route template.

In chapter 13 you learned that Razor Page handlers are the methods that are invoked on a Razor Page, such as OnGet. When we say “a Razor Page is executed,” we really mean “an instance of the Razor Page’s PageModel is created, and a page handler on the model is invoked.” Razor Pages can have multiple page handlers, so once the RoutingMiddleware selects a Razor Page, the EndpointMiddleware still needs to choose which handler to execute. You’ll learn how the framework selects which page handler to invoke in chapter 15.

By default, each Razor Page creates a single route template based on its file path. The exception to this rule is for Razor Pages that are called Index.cshtml. Index.cshtml pages create two route templates, one ending with "Index" and the other without this suffix. If you have a Razor Page at the path Pages/ToDo/Index.cshtml, you have two route templates that point to the same page:

• "ToDo"
• "ToDo/Index"

When either of these routes is matched, the same Index.cshtml Razor Page is selected. If your application is running at the URL https://example.org, you can view the page by executing https://example.org/ToDo or https://example.org/ToDo/Index.

Warning You must watch out for overlapping routes when using Index.cshtml pages. For example, if you add the Pages/ToDo/Index.cshtml page in the above example you must not add a Pages/ToDo.cshtml page, as you’ll get an exception at runtime when you navigate to /todo, as you’ll see in section 14.6.

As a final example, consider the Razor Pages created by default when you create a Razor Pages application by using Visual Studio or running dotnet new razor using the .NET command-line interface (CLI), as we did in chapter 13. The standard template includes three Razor Pages in the Pages directory:

• Pages/Error.cshtml
• Pages/Index.cshtml
• Pages/Privacy.cshtml

That creates a collection of four routes for the application, defined by the following templates:

• "" maps to Index.cshtml.
• "Index" maps to Index.cshtml.
• "Error" maps to Error.cshtml.
• "Privacy" maps to Privacy.cshtml.

At this point, Razor Page routing probably feels laughably trivial, but this is the basics that you get for free with the default Razor Pages conventions, which are often sufficient for a large portion of any website. At some point, though, you’ll find you need something more dynamic, such as using route parameters to include an ID in the URL. This is where the ability to customize your Razor Page route templates becomes useful.

14.4 Customizing Razor Page route templates

The route templates for a Razor Page are based on the file path by default, but you’re also able to customize the final template for each page or even replace it. In this section I show how to customize the route templates for individual pages so you can customize your application’s URLs and map multiple URLs to a single Razor Page.

You may remember from chapter 6 that route templates consist of both literal segments and route parameters, as shown in figure 14.3. By default, Razor Pages have URLs consisting of a series of literal segments, such as "ToDo/Index".

alt text

Figure 14.3 A simple route template showing a literal segment and two required route parameters

Literal segments and route parameters are the two cornerstones of ASP.NET Core route templates, but how can you customize a Razor Page to use one of these patterns? In section 14.4.1 you’ll see how to add a segment to the end of a Razor Page’s route template, and in section 14.4.2 you’ll see how to replace the route template completely.

14.4.1 Adding a segment to a Razor Page route template

To customize the Razor Page route template, you update the @page directive at the top of the Razor Page’s .cshtml file. This directive must be the first thing in the Razor Page file for the page to be registered correctly.

To add an extra segment to a Razor Page’s route template, add a space followed by the extra route template segment, after the @page statement. To add "Extra" to a Razor Page’s route template, for example, use

@page "Extra"

This appends the provided route template to the default template generated for the Razor Page. The default route template for the Razor Page at Pages/Privacy.html, for example, is "Privacy". With the preceding directive, the new route template for the page would be "Privacy/Extra".

The most common reason for customizing a Razor Page’s route template like this is to add a route parameter. You could have a single Razor Page for displaying the products in an e-commerce site at the path Pages/Products.cshtml and use a route parameter in the @page directive

@page "{category}/{name}"

This would give a final route template of Products/{category}/{name}, which would match all the following URLs:

• /products/bags/white-rucksack
• /products/shoes/black-size9
• /Products/phones/iPhoneX

NOTE You can use the same routing features you learned about in chapter 6 with Razor Pages, including optional parameters, default parameters, and constraints.

It’s common to add route segments to the Razor Page template like this, but what if that’s not enough? Maybe you don’t want to have the /products segment at the start of the preceding URLs, or you want to use a completely custom URL for a page. Luckily, that’s just as easy to achieve.

14.4.2 Replacing a Razor Page route template completely

You’ll be most productive working with Razor Pages if you can stick to the default routing conventions where possible, adding extra segments for route parameters where necessary. But sometimes you need more control. That’s often the case for important pages in your application, such as the checkout page for an e-commerce application or even product pages, as you saw in the previous section.

To specify a custom route for a Razor Page, prefix the route with / in the @page directive. To remove the "product/" prefix from the route templates in section 14.4.1, use this directive:

@page "/{category}/{name}"

Note that this directive includes the "/" at the start of the route, indicating that this is a custom route template, instead of an addition. The route template for this page will be "{category}/{name}" no matter which Razor Page it is applied to.

Similarly, you can create a static custom template for a page by starting the template with a "/" and using only literal segments:

@page "/checkout"

Wherever you place your checkout Razor Page within the Pages folder, using this directive ensures that it always has the route template "checkout", so it always matches the request URL /checkout.

Tip You can also think of custom route templates that start with “/” as absolute route templates, whereas other route templates are relative to their location in the file hierarchy.

It’s important to note that when you customize the route template for a Razor Page, both when appending to the default and when replacing it with a custom route, the default template is no longer valid. If you use the "checkout" route template above on a Razor Page located at Pages/Payment.cshtml, you can access it only by using the URL /checkout; the URL /Payment is no longer valid and won’t execute the Razor Page.

Tip Customizing the route template for a Razor Page using the @page directive replaces the default route template for the page. In section 14.6 I show how you can add extra routes while preserving the default route template.

In this section you learned how to customize the route template for a Razor Page. For the most part, routing to Razor Pages works like minimal APIs, the main difference being that the route templates are created using conventions. When it comes to the other half of routing—generating URLs—Razor Pages and minimal APIs are also similar, but Razor Pages gives you some nice helpers.

14.5 Generating URLs for Razor Pages

In this section you’ll learn how to generate URLs for your Razor Pages using the IUrlHelper that’s part of the Razor Pages PageModel type. You’ll also learn to use the LinkGenerator service you saw in chapter 6 for generating URLs with minimal APIs.

One of the benefits of using convention-based routing in Razor Pages is that your URLs can be somewhat fluid. If you rename a Razor Page, the URL associated with that page also changes. Renaming the Pages/Cart.cshtml page to Pages/Basket/View.cshtml, for example, causes the URL you use to access the page to change from /Cart to /Basket/View.

To track these changes (and to avoid broken links), you can use the routing infrastructure to generate the URLs that you output in your Razor Page HTML and that you include in your HTTP responses. In chapter 6 you saw how to generate URLs for your minimal API endpoints, and in this section, you’ll see how to do the same for your Razor Pages. I also describe how to generate URLs for MVC controllers, as the mechanism is virtually identical to that used by Razor Pages.

14.5.1 Generating URLs for a Razor Page

You’ll need to generate URLs in various places in your application, and one common location is in your Razor Pages and MVC controllers. The following listing shows how you could generate a link to the Pages/Currency/View.cshtml Razor Page, using the Url helper from the PageModel base class.

Listing 14.2 Generating a URL using IUrlHelper and the Razor Page name

public class IndexModel : PageModel ❶
{
public void OnGet()
{
var url = Url.Page("Currency/View", new { code = "USD" }); ❷
}
}

❶ Deriving from PageModel gives access to the Url property.
❷ You provide the relative path to the Razor Page, along with any additional route
values.

The Url property is an instance of IUrlHelper that allows you to easily generate URLs for your application by referencing other Razor Pages by their file path.

NOTE IUrlHelper is a wrapper around the LinkGenerator class you learned about in chapter 6. IUrlHelper adds some shortcuts for generating URLs based on the current request.

IUrlHelper exposes a Page() method to which you pass the name of the Razor Page and any additional route data as an anonymous object. Then the helper generates a URL based on the referenced page’s route template.

Tip You can provide the relative file path to the Razor Page, as shown in listing 14.2. Alternatively, you can provide the absolute file path (relative to the Pages folder) by starting the path with a "/", such as "/Currency/View".

IUrlHelper has several different overloads of the Page() method. Some of these methods allow you to specify a specific page handler, others let you generate an absolute URL instead of a relative URL, and some let you pass in additional route values.

In listing 14.2, as well as providing the file path I passed in an anonymous object, new { code = "USD" }. This object provides additional route values when generating the URL, in this case setting the code parameter to "USD", as you did when generating URLs for minimal APIs with LinkGenerator in chapter 6. As before, the code value is used in the URL directly if it corresponds to a route parameter. Otherwise, it’s appended as additional data in the query string.

Generating URLs based on the page you want to execute is convenient, and it’s the usual approach taken in most cases. If you’re using MVC controllers for your APIs, the process is much the same as for Razor Pages, though the methods are slightly different.

14.5.2 Generating URLs for an MVC controller

Generating URLs for MVC controllers is similar to Razor Pages. The main difference is that you use the Action method on the IUrlHelper, and you provide an MVC controller name and action name instead of a page path.

NOTE I’ve covered MVC controllers only in passing, as I generally don’t recommend them over Razor Pages or minimal APIs, so don’t worry too much about them. We’ll come back to MVC controllers in chapters 19 and 20; the main reason for mentioning them here is to point out how similar MVC controllers are to Razor Pages.

The following listing shows an MVC controller generating a link from one action method to another, using the Url helper from the Controller base class.

Listing 14.3 Generating a URL using IUrlHelper and the action name

public class CurrencyController : Controller ❶
{
[HttpGet("currency/index")] ❷
public IActionResult Index()
{
var url = Url.Action("View", "Currency", ❸
new { code = "USD" }); ❸
return Content($"The URL is {url}"); ❹
}
[HttpGet("currency/view/{code}")]
public IActionResult View(string code) ❺
{
    /* method implementation*/
}
}

❶ Deriving from Controller gives access to the Url property.
❷ Explicit route templates using attributes
❸ You provide the action and controller name to generate, along with any
additional route values.
❹ Returns “The URL is /Currency/View/USD”
❺ The URL generated a route to this action method.

You can call the Action and Page methods on IUrlHelper from both Razor Pages and MVC controllers, so you can generate links back and forth between them if you need to. The important question is, what is the destination of the URL? If the URL you need refers to a Razor Page, use the Page() method. If the destination is an MVC action, use the Action() method.

Tip Instead of using strings for the name of the action method, use the C# 6 nameof operator to make the value refactor-safe, such as nameof(View).

If you’re routing to an action in the same controller, you can use a different overload of Action() that omits the controller name when generating the URL. The IUrlHelper uses ambient values from the current request and overrides them with any specific values you provide.

DEFINITION Ambient values are the route values for the current request. They include Controller and Action when called from an MVC controller and Page when called from a Razor Page. Ambient values can also include additional route values that were set when the action or Razor Page was initially located using routing. See Microsoft’s “Routing in ASP.NET Core” documentation for further details: http://mng.bz/OxoE.

IUrlHelper can make it simpler to generate URLs by reusing ambient values from the current request, though it also adds a layer of complexity, as the same method arguments can give a different generated URL depending on the page the method is called from.

If you need to generate URLs from parts of your application outside the Razor Page or MVC infrastructure, you won’t be able to use the IUrlHelper helper. Instead, you can use the LinkGenerator class.

14.5.3 Generating URLs with LinkGenerator

In chapter 6 I described how to generate links to minimal API endpoints using the LinkGenerator class. By contrast with IUrlHelper, LinkGenerator requires that you always provide sufficient arguments to uniquely define the URL to generate. This makes it more verbose but also more consistent and has the advantage that it can be used anywhere in your application. This differs from IUrlHelper, which should be used only inside the context of a request.

If you’re writing your Razor Pages and MVC controllers following the advice from chapter 13, you should be trying to keep your Razor Pages relatively simple. That requires you to execute your application’s business and domain logic in separate classes and services.

For the most part, the URLs your application uses shouldn’t be part of your domain logic. That makes it easier for your application to evolve over time or even to change completely. You may want to create a mobile application that reuses the business logic from an ASP.NET Core app, for example. In that case, using URLs in the business logic wouldn’t make sense, as they wouldn’t be correct when the logic is called from the mobile app!

Tip Where possible, try to keep knowledge of the frontend application design out of your business logic. This pattern is known generally as the Dependency Inversion principle.

Unfortunately, sometimes that separation is not possible, or it makes things significantly more complicated. One example might be when you’re creating emails in a background service; it’s likely you’ll need to include a link to your application in the email. The LinkGenerator class lets you generate that URL so that it updates automatically if the routes in your application change.

As you saw in chapter 6, the LinkGenerator class is available everywhere in your application, so you can use it inside middleware, minimal API endpoints, or any other services. You can use it from Razor Pages and MVC too, if you want, though the IUrlHelper is often more convenient and hides some details of using the LinkGenerator.

You’ve already seen how to generate links to minimal API endpoints with LinkGenerator using methods like GetPathByName() and GetUriByName(). LinkGenerator has various analogous methods for generating URLs for Razor Pages and MVC actions, such as GetPathByPage(), GetPathByAction(), and GetUriByPage(), as shown in the following listing.

Listing 14.4 Generating URLs using the LinkGeneratorClass

public class CurrencyModel : PageModel
{
private readonly LinkGenerator _link; ❶
public CurrencyModel(LinkGenerator linkGenerator) ❶
{ ❶
_link = linkGenerator; ❶
} ❶
public void OnGet ()
{
var url1 = Url.Page("Currency/View", new { id = 5 }); ❷
var url3 = _link.GetPathByPage( ❸
HttpContext, ❸
"/Currency/View", ❸
values: new { id = 5 }); ❸
var url2 = _link.GetPathByPage( ❹
"/Currency/View", ❹
values: new { id = 5 }); ❹
var url4 = _link.GetUriByPage( ❺
page: "/Currency/View", ❺
handler: null, ❺
values: new { id = 5 }, ❺
scheme: "https", ❺
host: new HostString("example.com")); ❺
}
}

❶ LinkGenerator can be accessed using dependency injection.
❷ You can generate relative paths using Url.Page. You can use relative or
absolute Page paths.
❸ GetPathByPage is equivalent to Url.Page and generates a relative URL.
❹ Other overloads don’t require an HttpContext.
❺ GetUriByPage generates an absolute URL instead of a relative URL.

Warning As always, you need to be careful when generating URLs, whether you’re using IUrlHelper or LinkGenerator. If you get anything wrong—use the wrong path or don’t provide a required route parameter—the URL generated will be null.

At this point we’ve covered mapping request URLs to Razor Pages and generating URLs, but most of the URLs we’ve used have been kind of ugly. If seeing capital letters in your URLs bothers you, the next section is for you. In section 14.6 we customize the conventions your application uses to calculate route templates.

14.6 Customizing conventions with Razor Pages

Razor Pages is built on a series of conventions that are designed to reduce the amount of boilerplate code you need to write. In this section you’ll see some of the ways you can customize those conventions. By customizing the conventions Razor Pages uses in your application, you get full control of your application’s URLs without having to customize every Razor Page’s route template manually.

By default, ASP.NET Core generates URLs that match the filenames of your Razor Pages very closely. The Razor Page located at the path Pages/Products/ProductDetails.cshtml, for example, corresponds to the route template Products/ProductDetails.

These days, it’s not common to see capital letters in URLs. Similarly, words in URLs are usually separated using kebab-case rather than PascalCase—product-details instead of ProductDetails. Finally, it’s also common to ensure that your URLs always end with a slash, for example—/product-details/ instead of /product-details. Razor Pages gives you complete control of the conventions your application uses to generate route templates, but these are some of the common changes I often make.

You saw how to make some of these changes in chapter 6, by customizing the RouteOptions for your application. You can make your URLs lowercase and ensure that they already have a trailing slash as shown in the following listing.

Listing 14.5 Configuring routing conventions using RouteOptions in Program.cs

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.Configure<RouteOptions>(o => ❶
{ ❶
o.LowercaseUrls = true; ❶
o.LowercaseQueryStrings = true; ❶
o.AppendTrailingSlash = true; ❶
});
WebApplication app = builder.Build();
app.MapRazorPages();
app.Run();

❶ Changes the conventions used to generate URLs. By default, these properties
are false.

To use kebab-case for your application, annoyingly you must create a custom parameter transformer. This is a somewhat advanced topic, but it’s relatively simple to implement in this case. The following listing shows how you can create a parameter transformer that uses a regular expression to replace PascalCase values in a generated URL with kebab-case.

Listing 14.6 Creating a kebab-case parameter transformer

public class KebabCaseParameterTransformer ❶
: IOutboundParameterTransformer ❶
{
public string TransformOutbound(object? value)
{
if (value is null) return null; ❷
return Regex.Replace(value.ToString(), ❸
"([a-z])([A-Z])", "$1-$2").ToLower(); ❸
}
}

❶ Creates a class that implements the parameter transformer interface
❷ Guards against null values to prevent runtime exceptions
❸ The regular expression replaces PascalCase patterns with kebab-case.

Source generators in .NET 7

One of the exciting features introduced in C# 9 was source generators. Source generators are a compiler feature that let you inspect code as it’s compiled and generate new C# files on the fly, which are included in the compilation. Source generators have the potential to dramatically reduce the boilerplate required for some features and to improve performance by relying on compile-time analysis instead of runtime reflection.

.NET 6 introduced several source generator implementations, such as a high-performance logging API, which I discuss in this post: http://mng.bz/Y1GA. Even the Razor compiler used to compile .cshtml files was rewritten to use source generators!

In .NET 7, many new source generators were added. One such generator is the regular-expression generator, which can improve performance of your Regex instances, such as the one in listing 14.6. In fact, if you’re using an IDE like Visual Studio, you should see a code fix suggesting that you use the new pattern. After you apply the code fix, listing 14.6 should look like the following instead, which is functionally identical but will likely be faster:

partial class KebabCaseParameterTransformer : IOutboundParameterTransformer
{
    public string? TransformOutbound(object? value)
    {
        if (value is null) return null;
        return MyRegex().Replace(value.ToString(), "$1-$2").ToLower();
    }
    [GeneratedRegex("([a-z])([A-Z])")]
    private static partial Regex MyRegex();
}

If you’d like to know more about how this source generator works and how it can improve performance, see this post at http://mng.bz/GyEO. If you’d like to learn more about source generators or even write your own, see my series on the process at http://mng.bz/zX4Q.

You can register the parameter transformer in your application with the AddRazorPagesOptions() extension method in Program.cs. This method is chained after the AddRazorPages() method and can be used to customize the conventions used by Razor Pages. The following listing shows how to register the kebab-case transformer. It also shows how to add an extra page route convention for a given Razor Page.

Listing 14.7 Registering a parameter transformer using RazorPagesOptions

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages()
.AddRazorPagesOptions(opts => ❶
{
opts.Conventions.Add( ❷
new PageRouteTransformerConvention( ❷
new KebabCaseParameterTransformer())); ❷
opts.Conventions.AddPageRoute( ❸
"/Search/Products/StartSearch", "/search-products"); ❸
});
WebApplication app = builder.Build();
app.MapRazorPages();
app.Run();

❶ AddRazorPagesOptions can be used to customize the conventions used by Razor Pages
❷ Registers the parameter transformer as a convention used by all Razor Pages
❸ AddPageRoute adds a route template to Pages/Search/Products/StartSearch.cshtml.

The AddPageRoute() convention adds an alternative way to execute a single Razor Page. Unlike when you customize the route template for a Razor Page using the @page directive, using AddPageRoute() adds an extra route template to the page instead of replacing the default. That means there are two route templates that can access the page.

Tip Even the name of the Pages root folder is a convention that you can customize! You can customize it by setting the RootDirectory property inside the AddRazorPageOptions() configuration lambda.

If you want even more control of your Razor Pages route templates, you can implement a custom convention by implementing the IPageRouteModelConvention interface and registering it as a custom convention. IPageRouteModelConvention is one of three powerful Razor Pages interfaces which let you customize how your Razor Pages app works:

• IPageRouteModelConvention—Used to customize the route templates for all the Razor Pages in your app.
• IPageApplicationModelConvention—Used to customize how the Razor Page is processed, such as to add filters to your Razor Page automatically. You’ll learn about filters in Razor Pages in chapters 21 and 22.
• IPageHandlerModelConvention—Used to customize how page handlers are discovered and selected.

These interfaces are powerful, as they give you access to all the internals of your Razor Page conventions and configuration. You can use the IPageRouteModelConvention, for example, to rewrite all the route templates for your Razor Pages or to add routes automatically. This is particularly useful if you need to localize an application so that you can use URLs in multiple languages, all of which map to the same Razor Page.

Listing 14.8 shows a simple example of an IPageRouteModelConvention that adds a fixed prefix, "page", to all the routes in your application. If you have a Razor Page at Pages/Privacy.cshtml, with a default route template of "Privacy", after adding the following convention it would also have the route template "page/Privacy”.

Listing 14.8 Creating a custom IPageRouteModelConvention

public class PrefixingPageRouteModelConvention
: IpageRouteModelConvention ❶
{
public void Apply(PageRouteModel model) ❷
{
var selectors = model.Selectors
.Select(selector => new SelectorModel ❸
{ ❸
AttributeRouteModel = new AttributeRouteModel ❸
{ ❸
Template = AttributeRouteModel.CombineTemplates( ❸
"page", ❸
selector.AttributeRouteModel!.Template), ❸
} ❸
}) ❸
.ToList();
foreach(var newSelector in selectors) ❹
{
model.Selectors.Add(newSelector);
}
}
}

❶ The convention implements IPageRouteModelConvention.
❷ ASP.NET Core calls Apply on app startup.
❸ Creates a new SelectorModel, defining a new route template for the page
❹ Adds the new selector to the page’s route template collection

You can add the convention to your application inside the call to AddRazorPagesOptions(). The following applies the contention to all pages:

builder.Services.AddRazorPages().AddRazorPagesOptions(opts =>
{
    opts.Conventions.Add(new PrefixingPageRouteModelConvention());
});

There are many ways you can customize the conventions in your Razor Page applications, but a lot of the time that’s not necessary. If you do find you need to customize all the pages in your application in some way, Microsoft’s “Razor Pages route and app conventions in ASP.NET Core” documentation contains further details on everything that’s available: http://mng.bz/A0BK.

Conventions are a key feature of Razor Pages, and you should lean on them whenever you can. Although you can override the route templates for individual Razor Pages manually, as you’ve seen in previous sections, I advise against it where possible. In particular,

• Avoid replacing the route template with an absolute path in a page’s @page directive.
• Avoid adding literal segments to the @page directive. Rely on the file hierarchy instead.
• Avoid adding additional route templates to a Razor Page with the AddPageRoute() convention. Having multiple URLs to access a page can often be confusing.
• Do add route parameters to the @page directive to make your routes dynamic, as in @page “{name}".
• Do consider using global conventions when you want to change the route templates for all your Razor Pages, such as using kebab-case, as you saw earlier.

In a nutshell, these rules say “Stick to the conventions.” The danger, if you don’t, is that you may accidentally create two Razor Pages that have overlapping route templates. Unfortunately, if you end up in that situation, you won’t get an error at compile time. Instead, you’ll get an exception at runtime when your application receives a request that matches multiple route templates, as shown in figure 14.4.

alt text

Figure 14.4 If multiple Razor Pages are registered with overlapping route templates, you’ll get an exception at runtime when the router can’t work out which one to select.

We’ve covered pretty much everything about routing to Razor Pages now. For the most part, routing to Razor Pages works like minimal APIs, the main difference being that the route templates are created using conventions. When it comes to the other half of routing—generating URLs—Razor Pages and minimal APIs are also similar, but Razor Pages gives you some nice helpers.

Congratulations—you’ve made it all the way through this detailed discussion on Razor Page routing! I hope you weren’t too fazed by the differences from minimal API routing. We’ll revisit routing again when I describe how to create Web APIs in chapter 20, but rest assured that we’ve already covered all the tricky details in this chapter!

Routing controls how incoming requests are bound to your Razor Page, but we haven’t seen where page handlers come into it. In chapter 15 you’ll learn all about page handlers—how they’re selected, how they generate responses, and how to handle error responses gracefully.

14.7 Summary

Routing is the process of mapping an incoming request URL to an endpoint that will execute to generate a response. Each Razor Page is an endpoint, and a single page handler executes for each request.

You can define the mapping between URLs and endpoint in your application using either convention-based routing or explicit routing. Minimal APIs use explicit routing, where each endpoint has a corresponding route template. MVC controllers often use conventional routing in which a single pattern matches multiple controllers but may also use explicit/attribute routing. Razor Pages lies in between; it uses conventions to generate explicit route templates for each page.

By default, each Razor Page has a single route template that matches its path inside the Pages folder, so the Razor Page Pages/Products/View.cshtml has route template Products/View. These file-based defaults make it easy to visualize the URLs your application exposes.

Index.cshtml Razor Pages have two route templates, one with an /Index suffix and one without. Pages/Products/Index.cshtml, for example, has two route templates: Products/Index and Products. This is in keeping with the common behavior of index.html files in traditional HTML applications.

You can add segments to a Razor Page’s template by appending it to the @page directive, as in @page "{id}". Any extra segments are appended to the Razor Page’s default route template. You can include both literal and route template segments, which can be used to make your Razor Pages dynamic. You can replace the route template for a Razor Page by starting the template with a "/", as in @page "/contact".

You can use IUrlHelper to generate URLs as a string based on an action name or Razor Page. IUrlHelper can be used only in the context of a request and uses ambient routing values from the current request. This makes it easier to generate links for Razor Pages in the same folder as the currently executing request but also adds inconsistency, as the same method call generates different URLs depending on where it’s called.

The LinkGenerator can be used to generate URLs from other services in your application, where you don’t have access to an HttpContext object. The LinkGenerator methods are more verbose than the equivalents on IUrlHelper, but they are unambiguous as they don’t use ambient values from the current request.

You can control the routing conventions used by ASP.NET Core by configuring the RouteOptions object, such as to force all URLs to be lowercase or to always append a trailing slash.

You can add extra routing conventions for Razor Pages by calling AddRazorPagesOptions() after AddRazorPages() in Program.cs. These conventions can control how route parameters are displayed and can add extra route templates for specific Razor Pages.

Leave a Reply

Your email address will not be published. Required fields are marked *