diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/RazorViewEngine.cs b/src/Microsoft.AspNetCore.Mvc.Razor/RazorViewEngine.cs index be5c345966..ad0ec5e1cf 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/RazorViewEngine.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/RazorViewEngine.cs @@ -240,7 +240,13 @@ private ViewLocationCacheResult LocatePageFromViewLocations( { var controllerName = GetNormalizedRouteValue(actionContext, ControllerKey); var areaName = GetNormalizedRouteValue(actionContext, AreaKey); - var razorPageName = GetNormalizedRouteValue(actionContext, PageKey); + string razorPageName = null; + if (actionContext.ActionDescriptor.RouteValues.ContainsKey(PageKey)) + { + // Only calculate the Razor Page name if "page" is registered in RouteValues. + razorPageName = GetNormalizedRouteValue(actionContext, PageKey); + } + var expanderContext = new ViewLocationExpanderContext( actionContext, pageName, @@ -270,8 +276,7 @@ private ViewLocationCacheResult LocatePageFromViewLocations( expanderContext.IsMainPage, expanderValues); - ViewLocationCacheResult cacheResult; - if (!ViewLookupCache.TryGetValue(cacheKey, out cacheResult)) + if (!ViewLookupCache.TryGetValue(cacheKey, out ViewLocationCacheResult cacheResult)) { _logger.ViewLookupCacheMiss(cacheKey.ViewName, cacheKey.ControllerName); cacheResult = OnCacheMiss(expanderContext, cacheKey); diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageViewLocationExpander.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageViewLocationExpander.cs index a1af5eea2a..974bb5f99a 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageViewLocationExpander.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageViewLocationExpander.cs @@ -10,13 +10,13 @@ public class PageViewLocationExpander : IViewLocationExpander { public IEnumerable ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable viewLocations) { - if (string.IsNullOrEmpty(context.PageName)) + if ((context.ActionContext.ActionDescriptor is PageActionDescriptor) && !string.IsNullOrEmpty(context.PageName)) { - // Not a page - just act natural. - return viewLocations; + return ExpandPageHierarchy(); } - return ExpandPageHierarchy(); + // Not a page - just act natural. + return viewLocations; IEnumerable ExpandPageHierarchy() { diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/BasicTests.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/BasicTests.cs index b6bd2c39a7..60697caf63 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/BasicTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/BasicTests.cs @@ -376,5 +376,31 @@ public async Task TypesMarkedAsNonAction_AreInaccessible() // Assert Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } + + [Fact] + public async Task UsingPageRouteParameterInConventionalRouteWorks() + { + // Arrange + var expected = "ConventionalRoute - Hello from mypage"; + + // Act + var response = await Client.GetStringAsync("/PageRoute/ConventionalRoute/mypage"); + + // Assert + Assert.Equal(expected, response.Trim()); + } + + [Fact] + public async Task UsingPageRouteParameterInAttributeRouteWorks() + { + // Arrange + var expected = "AttributeRoute - Hello from test-page"; + + // Act + var response = await Client.GetStringAsync("/PageRoute/Attribute/test-page"); + + // Assert + Assert.Equal(expected, response.Trim()); + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewEngineTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewEngineTest.cs index b19fb8002c..f0eee6001b 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewEngineTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewEngineTest.cs @@ -1791,6 +1791,79 @@ public void GetViewLocationFormats_NoRouteValues_ReturnsDefaultSet() Assert.Equal(expected, actual); } + [Fact] + public void ViewEngine_DoesNotSetPageValue_IfItIsNotSpecifiedInRouteValues() + { + // Arrange + var routeValues = new Dictionary + { + { "controller", "MyController" }, + { "action", "MyAction" } + }; + + var expected = new[] { "some-seed" }; + var expander = new Mock(); + expander + .Setup(e => e.PopulateValues(It.IsAny())) + .Callback((ViewLocationExpanderContext c) => + { + Assert.Equal("MyController", c.ControllerName); + Assert.Null(c.PageName); + }) + .Verifiable(); + + expander + .Setup(e => e.ExpandViewLocations( + It.IsAny(), + It.IsAny>())) + .Returns(expected); + + var viewEngine = CreateViewEngine(expanders: new[] { expander.Object }); + var context = GetActionContext(routeValues); + + // Act + viewEngine.FindView(context, viewName: "Test-view", isMainPage: true); + + // Assert + expander.Verify(); + } + + [Fact] + public void ViewEngine_SetsPageValue_IfItIsSpecifiedInRouteValues() + { + // Arrange + var routeValues = new Dictionary + { + { "page", "MyPage" }, + }; + + var expected = new[] { "some-seed" }; + var expander = new Mock(); + expander + .Setup(e => e.PopulateValues(It.IsAny())) + .Callback((ViewLocationExpanderContext c) => + { + Assert.Equal("MyPage", c.PageName); + }) + .Verifiable(); + + expander + .Setup(e => e.ExpandViewLocations( + It.IsAny(), + It.IsAny>())) + .Returns(expected); + + var viewEngine = CreateViewEngine(expanders: new[] { expander.Object }); + var context = GetActionContext(routeValues); + context.ActionDescriptor.RouteValues["page"] = "MyPage"; + + // Act + viewEngine.FindView(context, viewName: "MyView", isMainPage: true); + + // Assert + expander.Verify(); + } + // Return RazorViewEngine with a page factory provider that is always successful. private RazorViewEngine CreateSuccessfulViewEngine() { @@ -1931,8 +2004,8 @@ public TestableRazorViewEngine( optionsAccessor, new FileProviderRazorProject( Mock.Of(a => a.FileProvider == new TestFileProvider()))) - { - } + { + } public TestableRazorViewEngine( IRazorPageFactoryProvider pageFactory, diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/PageViewLocationExpanderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/PageViewLocationExpanderTest.cs index 0a02633d65..a0ad23fdbc 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/PageViewLocationExpanderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/PageViewLocationExpanderTest.cs @@ -1,13 +1,10 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; using System.Collections.Generic; -using System.IO; using System.Linq; -using System.Text; +using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Razor; -using Microsoft.AspNetCore.Razor.Language; using Xunit; namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure @@ -48,6 +45,28 @@ public void ExpandLocations_NoOp_ForNonPage() Assert.Equal(locations, actual); } + [Fact] + public void ExpandLocations_NoOp_ForNonPageWithPageName() + { + // Verifies the fix for https://github.com/aspnet/Mvc/issues/6660. This ensures that when PageViewLocationExpander is called + // from a non-Razor Page with a route value for " + // Arrange + var context = CreateContext(pageName: "test"); + context.ActionContext.ActionDescriptor = new ControllerActionDescriptor(); + var locations = new string[] + { + "/ignore-me", + }; + + var expander = new PageViewLocationExpander(); + + // Act + var actual = expander.ExpandViewLocations(context, locations); + + // Assert + Assert.Equal(locations, actual); + } + [Fact] public void ExpandLocations_NoOp_WhenLocationDoesNotContainPageToken() { @@ -125,8 +144,13 @@ public void ExpandLocations_ExpandsDirectories_MultipleLocations() private ViewLocationExpanderContext CreateContext(string viewName = "_LoginPartial.cshtml", string pageName = null) { + var actionContext = new ActionContext + { + ActionDescriptor = new PageActionDescriptor(), + }; + return new ViewLocationExpanderContext( - new ActionContext(), + actionContext, viewName, controllerName: null, areaName: null, @@ -134,6 +158,7 @@ private ViewLocationExpanderContext CreateContext(string viewName = "_LoginParti isMainPage: true) { Values = new Dictionary(), + }; } } diff --git a/test/WebSites/BasicWebSite/Controllers/PageRouteController.cs b/test/WebSites/BasicWebSite/Controllers/PageRouteController.cs new file mode 100644 index 0000000000..2022d5e221 --- /dev/null +++ b/test/WebSites/BasicWebSite/Controllers/PageRouteController.cs @@ -0,0 +1,25 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Mvc; + +namespace BasicWebSite.Controllers +{ + // Verifies that we can use the "page" token in routing in a controller only (no Razor Pages) application + // without affecting view lookups. + public class PageRouteController : Controller + { + public IActionResult ConventionalRoute(string page) + { + ViewData["page"] = page; + return View(); + } + + [HttpGet("/PageRoute/Attribute/{page}")] + public IActionResult AttributeRoute(string page) + { + ViewData["page"] = page; + return View(); + } + } +} diff --git a/test/WebSites/BasicWebSite/Startup.cs b/test/WebSites/BasicWebSite/Startup.cs index 2ee9f6f816..9cc9d295eb 100644 --- a/test/WebSites/BasicWebSite/Startup.cs +++ b/test/WebSites/BasicWebSite/Startup.cs @@ -44,6 +44,7 @@ public void Configure(IApplicationBuilder app) routes.MapRoute("ActionAsMethod", "{controller}/{action}", defaults: new { controller = "Home", action = "Index" }); + routes.MapRoute("PageRoute", "{controller}/{action}/{page}"); }); } } diff --git a/test/WebSites/BasicWebSite/Views/PageRoute/AttributeRoute.cshtml b/test/WebSites/BasicWebSite/Views/PageRoute/AttributeRoute.cshtml new file mode 100644 index 0000000000..54be40b404 --- /dev/null +++ b/test/WebSites/BasicWebSite/Views/PageRoute/AttributeRoute.cshtml @@ -0,0 +1 @@ +AttributeRoute - Hello from @ViewBag.Page diff --git a/test/WebSites/BasicWebSite/Views/PageRoute/ConventionalRoute.cshtml b/test/WebSites/BasicWebSite/Views/PageRoute/ConventionalRoute.cshtml new file mode 100644 index 0000000000..8a22a765db --- /dev/null +++ b/test/WebSites/BasicWebSite/Views/PageRoute/ConventionalRoute.cshtml @@ -0,0 +1 @@ +ConventionalRoute - Hello from @ViewBag.Page