Mvc: Using "page" route parameter in convention routing fails with Asp.Net Core Mvc 2.0

Created on 15 Aug 2017  ·  40Comments  ·  Source: aspnet/Mvc

I have a website that was working perfectly fine in .NET core 1.1.

Today -- with the release of .NET Core 2.0 -- I decided to update my application. I changed my Target Framework to .NET Core 2.0, and switched over to using the _Microsoft.AspNetCore.All_ package as outlined here: https://blogs.msdn.microsoft.com/webdev/2017/08/14/announcing-asp-net-core-2-0/

I installed the .NET Core 2.0 Runtime on my server. I restarted my server.

The application builds fine, but when I deploy, it seems my routes -- which worked perfectly in .NET Core 1.1 -- no longer work.

When I try using https://example.com/history (desired URL, which the route SHOULD direct to the "History" action of the "Pages" controller) it complains of a missing View.

If I use https://example.com/pages/history (not desired URL) it loads the home page, not the history page.

Again, this worked fine in .NET Core 1.1

This is my _/Program.cs_:

using Microsoft.AspNetCore.Hosting;
using System.IO;

namespace Framework
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var host = new WebHostBuilder()
                .UseKestrel()
                .UseContentRoot(Directory.GetCurrentDirectory())
                .UseIISIntegration()
                .UseStartup<Startup>()
                .Build();

            host.Run();
        }
    }
}

This is the portion of my _/Startup.cs_ with the routes defined at the end:


            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "404",
                    template: "404/{id}",
                    defaults: new { controller = "Pages", action = "Error404"}
                );

                routes.MapRoute(
                    name: "news",
                    template: "news/{page}",
                    defaults: new { controller = "Pages", action = "News", page = 1 }
                );

                routes.MapRoute(
                    name: "history",
                    template: "history/{page=1}/{sortFilter?}/{sortOrder?}",
                    defaults: new { controller = "Pages", action = "History"}
                );

                routes.MapRoute(
                    name: "restoration-resources",
                    template: "restoration-resources/{page=1}/{sortFilter?}/{sortOrder?}",
                    defaults: new { controller = "Pages", action = "RestorationResources"}
                );

                routes.MapRoute(
                    name: "vehicles",
                    template: "vehicles/{vehicle?}/{page=1}/{sortFilter?}/{sortOrder?}",
                    defaults: new { controller = "Pages", action = "Vehicles" }
                );

                routes.MapRoute(
                    name: "gallery",
                    template: "gallery/{gallery?}",
                    defaults: new { controller = "Pages", action = "Gallery" }
                );

                routes.MapRoute(
                    name: "rss",
                    template: "rss/{source?}/{post?}",
                    defaults: new { controller = "Pages", action = "RSS"}
                );

                routes.MapRoute(
                    name: "pages",
                    template: "{action}/{id?}",
                    defaults: new { controller = "Pages", action = "News"}
                );

                routes.MapRoute(
                    name: "default",
                    template: "{controller}/{id?}",
                    defaults: new { controller = "Pages", action = "News"}
                );
            });
        }
    }
}
3 - Done bug

Most helpful comment

Hi @eat-sleep-code success! The following routes worked. Thanks a lot!

routes.MapRoute(
name: "Entries",
template: "Home/Entries/{pageNo}",
defaults: new { controller = "Home", action = "Entries", pageNo = 1 }
);

routes.MapRoute(
name: "Default",
template: "{controller}/{action}/{id?}/{pageNo}",
defaults: new { controller = "Home", action = "Entries", pageNo = 1 }
);

Not sure if this should be treated as a bug since this worked on 1.1 and also on the Full Framework MVC.

All 40 comments

Can you show the exception?

PS: There are a bunch of things that can be cleaned up with your code in general but I'll discuss those after we figure out why your views can't be found.

One more thing, I'd suggest turning up the logging verbosity. That should show you exactly what's happening.

@davidfowl: Here ya go:

System.InvalidOperationException: The view 'History' was not found. The following locations were searched:
/Views/Shared/History.en-US.cshtml
/Views/Shared/History.en.cshtml
/Views/Shared/History.cshtml
at Microsoft.AspNetCore.Mvc.ViewEngines.ViewEngineResult.EnsureSuccessful(IEnumerable`1 originalLocations)
at Microsoft.AspNetCore.Mvc.ViewResult.d__26.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.d__19.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.d__24.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Rethrow(ResultExecutedContext context)
at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.d__22.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Rethrow(ResourceExecutedContext context)
at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.d__17.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.d__15.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at Microsoft.AspNetCore.Builder.RouterMiddleware.d__4.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.d__7.MoveNext()

@eat-sleep-code See my other suggesting about turning up the logging verbosity.

@davidfowl Can that be done through the config? If it requires a rebuild, I will have to wait until later as I am in the office now and the code for this site is at home.

@eat-sleep-code It can be done through configuration in 2.0.0 but you didn't update your code to use any of the new APIs.

I'm assuming you also have views in the right folder? Can you show the layout on disk? Are you running behind IIS?

@davidfowl Do you have an outline of what code changes need to made to use the new APIs?

Yes it is behind IIS. (Windows Server 2016 Datacenter)

When I switched the DLL references in my project, I suddenly get a Framework.PrecompiledViews.dll file instead of the _/Views_ folder.

image

This route, should (and used to) make it work.

routes.MapRoute(
  name: "history",
  template: "history/{page=1}/{sortFilter?}/{sortOrder?}",
  defaults: new { controller = "Pages", action = "History"}
);

@davidfowl Do you have an outline of what code changes need to made to use the new APIs?

@eat-sleep-code https://docs.microsoft.com/en-us/aspnet/core/migration/1x-to-2x/

Just to be clear, I'm not saying this is the reason for any failures. It's more about enabling logging by default (and the ability to change it via configuration).

When I switched the DLL references in my project, I suddenly get a Framework.PrecompiledViews.dll file instead of the /Views folder.

Yes, this is one of the new defaults. Any clues if anything here needs to be changed @pranavkm ?

@davidfowl Thanks for that migration reference. I will have to tackle that stuff tonight.

But you are right, nothing there seems to explain why the silly routes are not working.

@eat-sleep-code could you include a file list tree /f from your app root?

@eat-sleep-code - sorry, I was specifically looking for the apps Views directory which is missing here.

@pranavkm Yeah, doesn't get created. Instead I have the Framework.PrecompiledViews.dll.

I was looking for the views directory in your application root (not the output). Alternatively if you could share your application that would also work.

@pranavkm I will provide a repo tonight when I get home.

@pranavkm Invited you to the private repo I created with all the entire project.

Thanks. I missed this when I was reading your initial issue. The route template you are using template: "history/{page=1}/{sortFilter?}/{sortOrder?}"adds a token named page to RouteValueDictionary which causes the view engine to incorrectly infer the route to be targeted at Razor Pages.

  1. Remove the PageViewLocationExpander that messes up the view lookup. As part of your Startup:

C# services.AddMvc().AddViewLocalization().AddDataAnnotationsLocalization().AddRazorOptions(options => { options.ViewLocationExpanders.Remove(options.ViewLocationExpanders.First(f => f is Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageViewLocationExpander)); });

  1. Modify the name of your route parameter. For instance, change the template from template: "history/{page=1}/{sortFilter?}/{sortOrder?}" to template: "history/{pageId=1}/{sortFilter?}/{sortOrder?}". You'd have to make corresponding changes to your action to either rename the action argument (page -> pageId) or use FromRoute { Name = "page" } to keep the existing name.

@rynowak, am I missing something a more obvious solution? Would we need to do anything additional to specifically address this breaking behavior?

@pranavkm Just confirming, I would need to complete _both_ steps to fix this correct?

@eat-sleep-code doing either one of these should fix your issue.

@pranavkm Thanks!

This appears to have fixed this issue. This probably should be at least documented somewhere to help out other folks who might encounter this scenario.

PS: I took option 1 since I am not using RazorPages. Option 2 seemed to wreak some havoc on my paging module as it seems to expect "page" not "pageId" (but otherwise Option 2 would have worked fine at fixing my issue).

@pranavkm isn't this a breaking change?

I'm having a similar issue, but not using precompiled views. My generic area routes like {area:exists}/{controller=Home}/{action=Index}/{id?} are working fine, but specific routes like:

"Forums/Recent/{page?}", new { controller = "Forum", action = "Recent", page = 1, Area = "Forums" }

are not. The crazy thing is that the actions are being executed, it just can't find the view, and appears to only look in shared locations:

InvalidOperationException: The view 'Recent' was not found. The following locations were searched:
/Areas/Forums/Views/Shared/Recent.cshtml
/Views/Shared/Recent.cshtml

These functioned as expected in v1.1. In the above example, the view is absolutely in /Areas/Forums/Views/Forum/Recent.cshtml and the controller is in the Forums area called ForumController and action Recent(page = 1). The controllers are in a separate project, but I don't imagine that matters.

@jeffputz It appears that having an attribute of "page" in a route causes the issue. It looks as this has now been logged as a bug, but in the meantime -- if you aren't using the new Razor Pages, try using the "Remove the PageViewLocationExpander" option / option 1 that @pranavkm suggested above.

That did it. That's an unfortunate bug. Thank you for your help, everyone!

I have the following routes. This works fine on 1.1, but does not on 2.0.

I also had page as a variable, changed it to pageNo

Startup.cs

routes.MapRoute(
name: "Entries",
template: "{controller=Home}/Entries/{pageNo=1}"
);

routes.MapRoute(
name: "Default",
template: "{controller=Home}/{action=Entries}/{id?}/{pageNo=1}"
);

HomeController.cs

public IActionResult Entries(int pageNo)
public ActionResult Category(string id, int pageNo)

Changing to pageNo fixed this issue but a call to Entries using "Home/Entries/2" continues to pass 1 to the Entries method in HomeController.

If I change the Entries method to Entries(string id, int pageNo), I see 2 coming in to the id variable and 1 into pageNo variable. Am I doing something wrong?

@Code-DJ, Have you tried changing your first route to:

routes.MapRoute(
    name: "Entries",
    template: "{controller=Home}/{action=Entries}/{pageNo=1}"
);

I believe you have to specify both a _Controller_ and an _Action_.

Hi @eat-sleep-code I used the route you gave, it fixes Entries but breaks Category.

For URL = /Home/Category/foo
It reaches the breakpoint in the Category method but with id is null and pageNo is 0

Works as expected for URL = /Home/Category/foo/1

My new routes are:

routes.MapRoute(
name: "Entries",
template: "{controller=Home}/{action=Entries}/{pageNo=1}"
);

routes.MapRoute(
name: "Default",
template: "{controller=Home}/{action=Entries}/{id?}/{pageNo=1}"
);

@Code-DJ Try this.

routes.MapRoute(
    name: "Entries",
    template: "Home/Entries/{pageNo}",
    defaults: new { controller = "Home", action = "Entries", pageNo=1}
);

Hi @eat-sleep-code success! The following routes worked. Thanks a lot!

routes.MapRoute(
name: "Entries",
template: "Home/Entries/{pageNo}",
defaults: new { controller = "Home", action = "Entries", pageNo = 1 }
);

routes.MapRoute(
name: "Default",
template: "{controller}/{action}/{id?}/{pageNo}",
defaults: new { controller = "Home", action = "Entries", pageNo = 1 }
);

Not sure if this should be treated as a bug since this worked on 1.1 and also on the Full Framework MVC.

@Code-DJ please start a new issue if you think there's a regression.

Good job I found this thread:)
I thought I was losing it....
Especially after I had read that upgrading to NetCore 2 was "easy"

I have the same bug but your solution doesn't fix it

routes.MapRoute(
                    name: null,
                    template: "Community/Page{pageID}",
                    defaults: new { Controller = "Forum", action = "Home" });
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Forum}/{action=Home}/{id?}");
                routes.MapRoute(
                    name: null,
                    template: "",
                    defaults: new { controller = "Forum", action = "Home", pageID = 1 });
                routes.MapRoute(name: null, template: "{controller}/{action}/{id?}");

this issue happen because i'm using tag helper

[HtmlAttributeName(DictionaryAttributePrefix = "page-url-")] public Dictionary<string, object> pageUrlValues { set; get; } public override void Process(TagHelperContext context, TagHelperOutput output) { pageUrlValues["**page**"] = i;
if i changed page to pageid the url Community/Page3 display page1
and if i used it as page, now when i navigate between pages they update but i get page1?page=3
note that since it show ?page=3 now my mapping is useless

Hi I have the same issue with routing. The following code worked, but the result looks like this: http://localhost:51409/?page=2, which is not ideal
routes.MapRoute(
name: null,
template: "",
defaults: new { controller = "Product", action = "List" }
);

but when I add {page} variable into Template as following: then the view List is not found.
routes.MapRoute(
name: null,
template: "Page{page:int}",
defaults: new { controller = "Product", action = "List" }
);

Can anyone help me out? Thank you!

Change route name “page” to something else

Hi Davidfowl

Thank you for your response. I have change variable "page" to "pg" as following.

public ViewResult List(string category, int pg = 1)
=> View(new ProductsListViewModel{
Products = repository.Products
.Where(p => category == null || p.Category == category)
.OrderBy(p => p.ProductID)
.Skip((pg - 1) * PageSize)
.Take(PageSize),
PagingInfo = new PagingInfo
{
CurrentPage = pg,
ItemsPerPage = PageSize,
TotalItems = category == null ?
repository.Products.Count() :
repository.Products.Where(e => e.Category == category).Count()
},
CurrentCategory = category
});

routes.MapRoute(
name: null,
template: "{pg}",
defaults: new { controller = "Product", action = "List" }
);

it still didn't work. Do you think it is a bug in ASP Net Core 2.0?

FYI, I started a web app project using empty template. Thank you!

Hi, I have the same problem BIngZL1983...((

use ASP Net Core 2.0,

but i resolved problem change parametr "page" in routes.... =)

@Temoxa, the issue was fixed as part of the 2.0.3 release. Upgrading your package reference - Microsoft.AspNetCore.All to 2.0.3 in CoreCLR projects or Microsoft.AspNetCore.Mvc to 2.0.1 in desktop projects should address this without requiring code changes.

Today upgrade to 2.0.3 , but problem saves....(

I see a similar issue, but related to using page as a query-string parameter inside of a Razor Pages model. e.g.:

public void OnGet(string page)

Using this, page is set to the path for the current page, which is unexpected. I'm seeing this when using Microsoft.AspNetCore.All 2.0.3 too.

The problem first showed up when using int page, which always comes out as 0 due to the difference in data type when binding.

As suggested, changing this to something like pageNumber is a solution, but that's going to be a pain when using a convention of e.g. int page, int pageSize for pagination.

@Temoxa could you file a new issue that details the issue you're running in to?
@serpent5 the term page like the terms controller, action or area is reserved by Mvc for routing and it's attempting to bind the route value. Unfortunately, there isn't a very good solution to this outside of using a different name.

My problem is resolved in version 2.0.3

Was this page helpful?
0 / 5 - 0 ratings

Related issues

michaelbowman1024 picture michaelbowman1024  ·  3Comments

CezaryRynkowski picture CezaryRynkowski  ·  4Comments

MariovanZeist picture MariovanZeist  ·  4Comments

LiangZugeng picture LiangZugeng  ·  3Comments

Lutando picture Lutando  ·  4Comments