-
Notifications
You must be signed in to change notification settings - Fork 10.4k
Description
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/Default
→SettingsController.Default
/Settings/Categories/Add
→CategoriesController.Add
/Foo
→DefaultController.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).