Search code examples
c#.net-coreasp.net-core-mvcrazor-pagesasp.net-mvc-routing

Precedence When Razor View & Razor Page Point to Same Endpoint


Let's say I have an ASP.NET Core 7 MVC project where both pages and views are supported. If a page and a view are pointing to the same endpoint, which one takes precedence and why? So to expand further, I have a Razor Class Library that contains a Razor page at Pages >> Account >> Login.cshtml (and code-behind file Login.cshtml.cs).

This is intended to act as the "default" login behavior. Meanwhile, I set up a controller action inside AccountController to see if I could override the page. But the page seems to always take precedence.

Here's my code:

Program.cs

builder.Services.AddMvc();
builder.Services.AddRazorPages().AddRazorRuntimeCompilation();

// ...

app.MapControllerRoute(name: "area-default", pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}");
app.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}/{id?}");

// I incorrectly assumed that by placing this after the controller routes, the MVC routes would take precedence
app.MapRazorPages();

AccountController.cs

using Microsoft.AspNetCore.Mvc;

namespace RazorClassLibrary.Controllers
{
    public class AccountController : Controller
    {
        [HttpGet]
        public IActionResult Login()
        {
            return Content("This is an override from the RCL.");
        }
    }
}

When I comment out the line app.MapRazorPages(), then the MVC routing kicks in as expected. But if the line is present, the page always wins over the controller action.

My goal is for the controller action to override the page. Is this possible? Any thoughts?


Solution

  • The url matcher in the middle of the request pipeline, builds a state-machine to parse each incoming request and match against the url pattern of each endpoint.

    Where there is ambiguity and multiple routes could match the same url, the routes are ordered using the following precedence rules;

    • RouteEndpoint.Order, lowest first. Which you can set explicitly by calling .WithOrder(int) after any endpoint .MapX rule. Or against individual routes via attributes [Route(Order=X)].
    • number of path segments, longest first. eg a/b/c will match over a/b
    • Segments are then ranked in order; /Literals, /{parameters-with-constraints}, [/{parameters} or /complex-{segments}]
    • catch all rules, eg /{**slug}
    • In case of a tie, an AmbiguousMatchException may be thrown, resulting in a 500 error.

    Note that there is no mention of a default order between controllers and pages. Though controller routes are usually matched against a [controller] route variable, while each page will have an explicit path. Resulting in pages being preferred.