Allow "page" route parameter to be used in Mvc controllers

Fixes #6660
This commit is contained in:
Pranav K 2017-08-24 09:54:41 -07:00
parent bc87c3e9cb
commit 7b2a4ff465
9 changed files with 171 additions and 14 deletions

View File

@ -240,7 +240,13 @@ namespace Microsoft.AspNetCore.Mvc.Razor
{
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 @@ namespace Microsoft.AspNetCore.Mvc.Razor
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);

View File

@ -10,13 +10,13 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
{
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> 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<string> ExpandPageHierarchy()
{

View File

@ -376,5 +376,31 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
// 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());
}
}
}

View File

@ -1791,6 +1791,79 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
Assert.Equal(expected, actual);
}
[Fact]
public void ViewEngine_DoesNotSetPageValue_IfItIsNotSpecifiedInRouteValues()
{
// Arrange
var routeValues = new Dictionary<string, object>
{
{ "controller", "MyController" },
{ "action", "MyAction" }
};
var expected = new[] { "some-seed" };
var expander = new Mock<IViewLocationExpander>();
expander
.Setup(e => e.PopulateValues(It.IsAny<ViewLocationExpanderContext>()))
.Callback((ViewLocationExpanderContext c) =>
{
Assert.Equal("MyController", c.ControllerName);
Assert.Null(c.PageName);
})
.Verifiable();
expander
.Setup(e => e.ExpandViewLocations(
It.IsAny<ViewLocationExpanderContext>(),
It.IsAny<IEnumerable<string>>()))
.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<string, object>
{
{ "page", "MyPage" },
};
var expected = new[] { "some-seed" };
var expander = new Mock<IViewLocationExpander>();
expander
.Setup(e => e.PopulateValues(It.IsAny<ViewLocationExpanderContext>()))
.Callback((ViewLocationExpanderContext c) =>
{
Assert.Equal("MyPage", c.PageName);
})
.Verifiable();
expander
.Setup(e => e.ExpandViewLocations(
It.IsAny<ViewLocationExpanderContext>(),
It.IsAny<IEnumerable<string>>()))
.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 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
optionsAccessor,
new FileProviderRazorProject(
Mock.Of<IRazorViewEngineFileProviderAccessor>(a => a.FileProvider == new TestFileProvider())))
{
}
{
}
public TestableRazorViewEngine(
IRazorPageFactoryProvider pageFactory,

View File

@ -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 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
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 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
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 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
isMainPage: true)
{
Values = new Dictionary<string, string>(),
};
}
}

View File

@ -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();
}
}
}

View File

@ -44,6 +44,7 @@ namespace BasicWebSite
routes.MapRoute("ActionAsMethod", "{controller}/{action}",
defaults: new { controller = "Home", action = "Index" });
routes.MapRoute("PageRoute", "{controller}/{action}/{page}");
});
}
}

View File

@ -0,0 +1 @@
AttributeRoute - Hello from @ViewBag.Page

View File

@ -0,0 +1 @@
ConventionalRoute - Hello from @ViewBag.Page