Skip to content

Endpoint routing chooses fallback route due to conflict #9758

@poke

Description

@poke

This is a follow-up from a Stack Overflow question, reduced to a minimum repro, so you don’t actually have to go through the question there.

Let’s take the following route configuration on an otherwise empty ASP.NET Core 2.2 project:

app.UseMvc(routes =>
{
    routes.MapRoute("SettingsRoute", "{controller}/{action=Default}", null, new
    {
        controller = "Settings"
    });

    routes.MapRoute("NestedRoute", "Settings/{controller}/{action}", null, new
    {
        controller = "Categories"
    });

    routes.MapRoute("FallbackRoute", "{*url}", new
    {
        action = "Gone",
        controller = "Default"
    });
});

Along with three very simple controllers:

public class CategoriesController : Controller
{
    public IActionResult Add() => Content("Category added");
}

public class DefaultController : Controller
{
    public IActionResult Gone() => Content("Gone");
}

public class SettingsController : Controller
{
    public IActionResult Default() => Content("Settings");
}

As you can see, there are three route templates which should match the following routes. These work:

  • /Settings/DefaultSettingsController.Default
  • /Settings/Categories/AddCategoriesController.Add
  • /FooDefaultController.Gone

However, since the SettingsRoute has a default action of Default specified, /Settings should also trigger the SettingsController.Default action. Instead, endpoint routing picks the FallbackRoute here and goes to DefaultController.Gone.

If you remove the FallbackRoute or the NestedRoute, the /Settings path does work again correctly, so it appears to be a side effect of these three route definitions.

I have been debugging through the route matcher now to figure out why this happens and for some reason the DfaMatcher finds two candidates for the /Settings path, in this order:

  • Endpoint: DefaultController.Gone, Score: 0
  • Endpoint: SettingsController.Default, Score: 1

Since the matcher then iterates through these candidates and appears to pick the first one that matches the route, it decides to use the DefaultController.Gone route, although the SettingsController.Default one would be the better choice.

During initialization, the MvcEndpointDataSource calculates the following five endpoints:

  • CategoriesController.Add – Order 1, RoutePattern: Settings/Categories/Add
  • DefaultController.Gone – Order: 1, RoutePattern: {*url}
  • SettingsController.Default – Order: 1, RoutePattern: Settings/{action=Default}
  • SettingsController.Default – Order: 2, RoutePattern: Settings
  • SettingsController.Default – Order: 3, RoutePattern: Settings/Default

I was thinking that maybe this order was determining which candidate came first, so I moved the DefaultController declaration after the SettingsController (in a usual situation where each class is in its own type, the name would have to be changed since the file name order decides this). Sure enough, that moved the DefaultController.Gone endpoint to the end, but that didn’t actually change the candidate order.

So I am a bit at loss now what else could determine the candidate order. I haven’t digged deep enough into the matcher to tell.

I do realize that the endpoint routing is known to be broken in certain situations in ASP.NET Core 2.2. And for what it’s worth, this issue does not appear to exist in the current 3.0 preview (phew! 😅). So it’s maybe not worth to dig too deep into this, but I would still like to understand and see if we can’t figure out a workaround (one that does not change the route templates).

Metadata

Metadata

Assignees

Labels

area-mvcIncludes: MVC, Actions and Controllers, Localization, CORS, most templatesinvestigate

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions