Reputation: 9490
Update (2)
@poke seems to have figured it out and it looks to be an issue with endpoint routing itself favoring the {*url}
if there's perceived ambiguity with other higher routes.
Update (1)
@poke commented that I had a typo on the {*url}
route where the t in controller was missing. After fixing that the {*url}
route started working and the DefaultController.Gone
action was working.
BUT! Now weird behavior is starting to crop up again. After the {*url}
was fixed, navigating to /settings
which is supposed to match the {controller}/{action}
route fails and falls back to the {*url}
route.
If I remove the {*url}
route from the registrations then /settings
works again. The {action}
route continues to not work.
Original
Please forgive the length of the question, but I am trying to offer as much information as possible.
I am working on an ASP.NET Core 2.2 blogging app for myself, and I'm having inexplicable problems getting routing to work. After spending half the day yelling at my screens, I decided to take a step back and start a new project that was completely isolated. Somehow the problems persisted in the new project. I've stripped it down pretty much to a starved skeleton and I still can't get the routes to work. The routes I’m trying to set up are:
settings/{controller}/{id:int}/{action} - works
settings/{controller}/{action} - works
blog/{*slug} - works
blog/{skip:int?} - works
{controller}/{action} - works
{action} - doesn't work
{*url} - doesn't work
Specifically, I’m having problems with the last two routes.
The {action}
route is not generating for simple actions like DefaultController.About
even though it has no constraints, all it has is defaults for PostsController.List
because I want a list of posts to be shown for the root URL.
The {*url}
just doesn't seem to work at all. I want to use it as my final fallback and it's defaulted to DefaultController.Gone
, but if I just bash on the keyboard for some nonsense URL all I get is a 404 error.
I feel that the issue is with the DefaultController
since both the About
and Gone
actions are in it and neither one seems to be working, but I just can't seem to figure out how. It literally does nothing but renders views, just like the other controllers.
Below is the code of the stripped down project. I'd really appreciate it if someone can spin it up and tell me where I'm failing because I certainly can't seem to figure it.
Program.cs
public sealed class Program {
public static async Task Main(
string[] args) => await WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.Build()
.RunAsync();
}
Startup.cs
public class Startup {
public void ConfigureServices(
IServiceCollection services) {
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Latest);
}
public void Configure(
IApplicationBuilder app) {
app.UseMvc(
r => {
// /settings/{controller}/{id}/{action}
r.MapRoute("600", "settings/{controller}/{id:int}/{action}", null, new {
controller = "Categories|Tags"
});
// /settings/{controller}/{action}
r.MapRoute("500", "settings/{controller}/{action}", null, new {
controller = "Categories|Tags"
});
// /blog/*
r.MapRoute("400", "blog/{*slug}", new {
action = "Show",
controller = "Posts"
});
// /blog/{skip}
r.MapRoute("300", "blog/{skip:int?}", new {
action = "List",
controller = "Posts"
});
// /{controller}/{action}
r.MapRoute("200", "{controller}/{action=Default}", null, new {
controller = "Settings|Tools"
});
// /{action}
r.MapRoute("100", "{action}", new {
action = "List",
controller = "Posts"
});
// /*
r.MapRoute("-1", "{*url}", new {
action = "Gone",
conroller = "Default"
});
});
}
}
CategoriesController.cs
public sealed class CategoriesController :
Controller {
[HttpGet]
public IActionResult Add() => Content("Category added");
[HttpGet]
public IActionResult Remove(
int id) => Content($"Category {id} removed");
}
DefaultController.cs
public sealed class DefaultController :
Controller {
[HttpGet]
public IActionResult About() => View();
[HttpGet]
public IActionResult Gone() => View();
}
About.cshtml (Default)
<h1>DEFAULT.ABOUT</h1>
Gone.cshtml (Default)
<h1>DEFAULT.GONE</h1>
PostsController.cs
public sealed class PostsController :
Controller {
[HttpGet]
public IActionResult List(
int? skip) => View();
[HttpGet]
public IActionResult Show(
string slug) => View();
}
List.cshtml (Posts)
<h1>POSTS.LIST</h1>
<a asp-action="Show" asp-controller="Posts" asp-route-slug="test-test-test">Show a Post</a>
Show.cshtml (Posts)
<h1>POSTS.SHOW</h1>
SettingsController.cs
public sealed class SettingsController :
Controller {
[HttpGet]
public IActionResult Default() => View();
}
Default.cshtml (Settings)
<h1>SETTINGS.DEFAULT</h1>
<a asp-action="Add" asp-controller="Categories">Add a Category</a>
<br />
<a asp-action="Remove" asp-controller="Categories" asp-route-id="1">Remove a Category</a>
<hr />
<a asp-action="Add" asp-controller="Tags">Add a Tag</a>
<br />
<a asp-action="Remove" asp-controller="Tags" asp-route-id="1">Remove a Tag</a>
TagsController.cs
public sealed class TagsController :
Controller {
[HttpGet]
public IActionResult Add() => Content("Tag added");
[HttpGet]
public IActionResult Remove(
int id) => Content($"Tag {id} removed");
}
ToolsController.cs
public sealed class ToolsController :
Controller {
[HttpGet]
public IActionResult Default() => View();
}
Default.cshtml
<h1>TOOLS.DEFAULT</h1>
_Layout.cshtml
<!DOCTYPE html>
<html>
<head>
<title>@ViewBag.Title</title>
</head>
<body>
<a asp-action="List" asp-controller="Posts">Blog</a>
<br />
<a asp-action="Default" asp-controller="Tools">Tools</a>
<br />
<a asp-action="About" asp-controller="Default">About</a>
<br />
<a asp-action="Default" asp-controller="Settings">Settings</a>
<br />
@RenderBody()
</body>
</html>
Upvotes: 0
Views: 2785
Reputation: 387677
{action}
- doesn't work
This one doesn’t work because it has to match an actual action. So it works for /Show
or /List
since you are operating on the PostsController
. It also works for /
since the action
defaults to List
.
{*url}
- doesn't work
This one will work if you set the default controller
, instead of the conroller
:
r.MapRoute("-1", "{*url}", new
{
action = "Gone",
controller = "Default"
});
<a asp-action="About" asp-controller="Default">About</a>
Note that this route will also not match because there is no route to that action. The {controller}/{action}
routes are constrained to the SettingsController
and ToolsController
, so the route won’t match. You will need to adjust the constraint or add another route for this to work.
Btw. as a general suggestion: As you probably noticed, managing this many route mapping gets quite complicated. It’s often easier to just use attribute routing with explicit routes. You could also mix those with template based routing to get the best of both worlds.
Weirdly
/settings
which is supposed to match the{controller}/{action}
route is now failing and falling back to the{*url}
route. If I remove the{*url}
route from the registrations then/settings
works again.
That appears to be a side effect from comining the settings/{controller}/{action}
and the {controller}/{action=Default}
routes.
I’ve been debugging through that now for a bit and it seems that this is a bug with endpoint routing, which favors the catch all route although it is being registered later.
Unfortunately, endpoint routing in ASP.NET Core 2.2 is known to break on a few special cases which is why it is being revamped for 3.0 which will hopefully resolve all issues. That being said, I’ve opened an issue about this particular problem and reported my findings. Maybe there’s a simple solution for this.
One easy workaround would be to change the settings/{controller}/{action}
route template to use a prefix other than settings
, so that there is no longer an ambiguity. That appears to fix the issues.
Upvotes: 1