Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Routing HTTP Requests to Razor Pages in ASP.NET Core

Tech 1

Routing maps incoming HTTP requests to the handlers that produce responses. In ASP.NET Core Razor Pages, this mechanism determines which page handler method executes for a given URL. Rather than relying solely on physical file paths, routing decouples the exposed address from the underlying page, enabling cleaner URLs and allowing a single page to respond to multiple address patterns.

ASP.NET Core processes requests through a middleware pipeline. Near the end of this pipeline, endpoint routing resolves which handler should run. Introduced in ASP.NET Core 3.0, the endpoint routing system separates discovery from execution acrosss two components. EndpointRoutingMiddleware, added by calling UseRouting(), inspects the incoming request and selects the best matching endpoint from a shared registry. EndpointMiddleware, added by calling UseEndpoints(), executes the chosen endpoint. Middleware placed between these two stages can inspect the selected endpoint, which is essential for authorization and other cross-cutting concerns that must know the destination before it runs.

Endpoints are registered inside the UseEndpoints delegate. For Razor Pages, calling MapRazorPages() scans the Pages directory and automatically registers every page using attribute routes derived from their file locations. Additional endpoints, such as health checks or minimal APIs, can be registered alongside them:

app.UseRouting();

app.UseEndpoints(endpoints =>
{
    endpoints.MapRazorPages();
    endpoints.MapHealthChecks("/healthz");
    endpoints.MapGet("/status", async context =>
    {
        await context.Response.WriteAsync("Operational");
    });
});

Each endpoint is associated with a route template that defines the URL patterns it matches.

Razor Pages use attribute routing, but the routes are generated automatically by convention. A page located at Pages/Catalog/Item.cshtml receives the route template Catalog/Item. Pages named Index.cshtml create two routes: one that includes the Index segment and one that omits it. For example, Pages/Shop/Index.cshtml registers both Shop and Shop/Index.

This default behavior can be customized with the @page directive placed at the top of the .cshtml file. Appending a relative template adds segments to the conventional route:

@page "{category}/{slug}"

This produces a combined template such as Catalog/Item/{category}/{slug}. To replace the conventional route entirely, prefix the template with a slash:

@page "/{category}/{slug}"

Route templates consist of segments separated by forward slashes. Segments can be literal values that require an exact match, or route parameters enclosed in braces that capture variable data. Parameters can be required, optional, or include default values:

@page "product/{category}/{name=all}/{id?}"

Here, category is mandatory, name defaults to all when omitted, and id is optional. Optional segments must appear at the end of the template. When a request matches, the router extracts these values into a route value dictionary that drives model binding.

To prevent invalid matches, cosntraints restrict the format of captured values. Appending a colon and constraint name to a parameter ensures only matching URLs succeed:

@page "order/{id:int}"

Common constraints include int, guid, decimal, min(value), max(value), and length(min,max). Constraints help disambiguate between similar templates but should not replace proper input validation.

When a URL must capture all remaining path segments, including any slashes, a catch-all parameter is useful:

@page "/docs/{*path}"

A double-asterisk version, {**path}, preserves forward slashes during URL generation.

Routing also supports URL generation, producing addresses from route values rather than hard-coding strings. Inside a PageModel, the IUrlHelper exposed through the Url property constructs links to other pages:

public class BrowseModel : PageModel
{
    public void OnGet()
    {
        var targetUrl = Url.Page("Catalog/Item", new { category = "electronics", slug = "laptop" });
    }
}

If the target route template contains matching parameters, they become part of the path; otherwise, they are appended as query string values. For MVC controllers, the Action method performs the same function using controller and action names.

When returning redirects, RedirectToPage and RedirectToAction results generate URLs automatically:

public IActionResult OnPost()
{
    return RedirectToPage("Checkout/Receipt", new { orderId = 101 });
}

Outside of controllers and page models, the LinkGenerator service creates URLs from anywhere in the application, including background services and custom middleware:

public class NotificationService
{
    private readonly LinkGenerator _linkGenerator;
    
    public NotificationService(LinkGenerator linkGenerator)
    {
        _linkGenerator = linkGenerator;
    }
    
    public string BuildOrderLink(int orderId)
    {
        return _linkGenerator.GetPathByPage(
            "/Orders/Details",
            values: new { id = orderId });
    }
}

After routing selects a Razor Page, the framework must choose which page handler to execute. Handler selection depends on the HTTP verb and an optional handler route value, which can come from a query string or a route parameter. Methods follow the naming pattern On{Verb}[Handler][Async]. For example, OnGet handles GET requests, while OnPostApplyDiscount handles POST requests when handler=ApplyDiscount. If no handler matches, an implicit handler renders the view without custom logic. HEAD requests without an explicit OnHead method fall back to OnGet.

Global conventions can standardize URL formatting across the application. RouteOptions controls trailing slashes and casing:

services.Configure<RouteOptions>(options =>
{
    options.LowercaseUrls = true;
    options.AppendTrailingSlash = true;
    options.LowercaseQueryStrings = true;
});

To convert PascalCase file names to kebab-case URLs, implement IOutboundParameterTransformer:

public class SlugCaseTransformer : IOutboundParameterTransformer
{
    public string TransformOutbound(object value)
    {
        if (value == null) return null;
        return Regex.Replace(value.ToString(), "([a-z])([A-Z])", "$1-$2").ToLowerInvariant();
    }
}

Register it within AddRazorPagesOptions:

services.AddRazorPages().AddRazorPagesOptions(options =>
{
    options.Conventions.Add(
        new PageRouteTransformerConvention(new SlugCaseTransformer()));
    
    options.Conventions.AddPageRoute("/Search/Results", "/find");
});

The AddPageRoute method registers an additional route alias without removing the default template.

Avoid excessive customization that creates overlapping routes. Perfer relative route additions over absolute overrides, rely on folder structure for literals, and use global conventions when broad changes are needed. Overlapping route templates cause runtime ambiguity exceptions because the router cannot determine which endpoint to execute.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.