Fix #6140 - Allow linking between controller and page
The issue here is that route values used for action selection are 'global'. That means that pages need to have a 'null' route value for 'action' and controllers need to have a 'null' route value for pages. This is the same way that areas work. The fix is to move the 'merge' of route values up to a level where pages and controllers can work together. Since ADPs use the russian-doll pattern, the fix is to run this 'merge' in the controller ADP, but after all of the ADs have been created.
This commit is contained in:
parent
6cd487e15f
commit
4bf518b09b
|
|
@ -34,8 +34,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
{
|
{
|
||||||
var actions = new List<ControllerActionDescriptor>();
|
var actions = new List<ControllerActionDescriptor>();
|
||||||
|
|
||||||
var routeValueKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
var methodInfoMap = new MethodToActionMap();
|
var methodInfoMap = new MethodToActionMap();
|
||||||
|
|
||||||
var routeTemplateErrors = new List<string>();
|
var routeTemplateErrors = new List<string>();
|
||||||
|
|
@ -64,7 +62,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
actionDescriptor.ControllerTypeInfo = controller.ControllerType;
|
actionDescriptor.ControllerTypeInfo = controller.ControllerType;
|
||||||
|
|
||||||
AddApiExplorerInfo(actionDescriptor, application, controller, action);
|
AddApiExplorerInfo(actionDescriptor, application, controller, action);
|
||||||
AddRouteValues(routeValueKeys, actionDescriptor, controller, action);
|
AddRouteValues(actionDescriptor, controller, action);
|
||||||
AddProperties(actionDescriptor, action, controller, application);
|
AddProperties(actionDescriptor, action, controller, application);
|
||||||
|
|
||||||
actionDescriptor.BoundProperties = controllerPropertyDescriptors;
|
actionDescriptor.BoundProperties = controllerPropertyDescriptors;
|
||||||
|
|
@ -108,12 +106,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
// attribute routes with a given name have the same template.
|
// attribute routes with a given name have the same template.
|
||||||
AddActionToNamedGroup(actionsByRouteName, attributeRouteInfo.Name, actionDescriptor);
|
AddActionToNamedGroup(actionsByRouteName, attributeRouteInfo.Name, actionDescriptor);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a route value with 'null' for each user-defined route value in the set to all the
|
|
||||||
// actions that don't have that value. For example, if a controller defines
|
|
||||||
// an area, all actions that don't belong to an area must have a route
|
|
||||||
// value that prevents them from matching an incoming request when area is specified.
|
|
||||||
AddGlobalRouteValues(actionDescriptor, routeValueKeys);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attributeRoutingConfigurationErrors.Any())
|
if (attributeRoutingConfigurationErrors.Any())
|
||||||
|
|
@ -409,7 +401,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void AddRouteValues(
|
public static void AddRouteValues(
|
||||||
ISet<string> keys,
|
|
||||||
ControllerActionDescriptor actionDescriptor,
|
ControllerActionDescriptor actionDescriptor,
|
||||||
ControllerModel controller,
|
ControllerModel controller,
|
||||||
ActionModel action)
|
ActionModel action)
|
||||||
|
|
@ -421,8 +412,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
// generating a link.
|
// generating a link.
|
||||||
foreach (var kvp in action.RouteValues)
|
foreach (var kvp in action.RouteValues)
|
||||||
{
|
{
|
||||||
keys.Add(kvp.Key);
|
|
||||||
|
|
||||||
// Skip duplicates
|
// Skip duplicates
|
||||||
if (!actionDescriptor.RouteValues.ContainsKey(kvp.Key))
|
if (!actionDescriptor.RouteValues.ContainsKey(kvp.Key))
|
||||||
{
|
{
|
||||||
|
|
@ -432,8 +421,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
|
|
||||||
foreach (var kvp in controller.RouteValues)
|
foreach (var kvp in controller.RouteValues)
|
||||||
{
|
{
|
||||||
keys.Add(kvp.Key);
|
|
||||||
|
|
||||||
// Skip duplicates - this also means that a value on the action will take precedence
|
// Skip duplicates - this also means that a value on the action will take precedence
|
||||||
if (!actionDescriptor.RouteValues.ContainsKey(kvp.Key))
|
if (!actionDescriptor.RouteValues.ContainsKey(kvp.Key))
|
||||||
{
|
{
|
||||||
|
|
@ -483,19 +470,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AddGlobalRouteValues(
|
|
||||||
ControllerActionDescriptor actionDescriptor,
|
|
||||||
ISet<string> removalConstraints)
|
|
||||||
{
|
|
||||||
foreach (var key in removalConstraints)
|
|
||||||
{
|
|
||||||
if (!actionDescriptor.RouteValues.ContainsKey(key))
|
|
||||||
{
|
|
||||||
actionDescriptor.RouteValues.Add(key, string.Empty);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AddActionToNamedGroup(
|
private static void AddActionToNamedGroup(
|
||||||
IDictionary<string, IList<ActionDescriptor>> actionsByRouteName,
|
IDictionary<string, IList<ActionDescriptor>> actionsByRouteName,
|
||||||
string routeName,
|
string routeName,
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,36 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void OnProvidersExecuted(ActionDescriptorProviderContext context)
|
public void OnProvidersExecuted(ActionDescriptorProviderContext context)
|
||||||
{
|
{
|
||||||
|
// After all of the providers have run, we need to provide a 'null' for each all of route values that
|
||||||
|
// participate in action selection.
|
||||||
|
//
|
||||||
|
// This is important for scenarios like Razor Pages, that use the 'page' route value. An action that
|
||||||
|
// uses 'page' shouldn't match when 'action' is set, and an action that uses 'action' shouldn't match when
|
||||||
|
// 'page is specified.
|
||||||
|
//
|
||||||
|
// Or for another example, consider areas. A controller that's not in an area needs a 'null' value for
|
||||||
|
// area so it can't match when the route produces an 'area' value.
|
||||||
|
var keys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
for (var i = 0; i < context.Results.Count; i++)
|
||||||
|
{
|
||||||
|
var action = context.Results[i];
|
||||||
|
foreach (var key in action.RouteValues.Keys)
|
||||||
|
{
|
||||||
|
keys.Add(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < context.Results.Count; i++)
|
||||||
|
{
|
||||||
|
var action = context.Results[i];
|
||||||
|
foreach (var key in keys)
|
||||||
|
{
|
||||||
|
if (!action.RouteValues.ContainsKey(key))
|
||||||
|
{
|
||||||
|
action.RouteValues.Add(key, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal protected IEnumerable<ControllerActionDescriptor> GetDescriptors()
|
internal protected IEnumerable<ControllerActionDescriptor> GetDescriptors()
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
||||||
_pagesOptions = pagesOptionsAccessor.Value;
|
_pagesOptions = pagesOptionsAccessor.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int Order { get; set; }
|
public int Order { get; set; } = -900; // Run after the default MVC provider, but before others.
|
||||||
|
|
||||||
public void OnProvidersExecuting(ActionDescriptorProviderContext context)
|
public void OnProvidersExecuting(ActionDescriptorProviderContext context)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -277,11 +277,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
|
|
||||||
var descriptorWithoutValue = Assert.Single(
|
var descriptorWithoutValue = Assert.Single(
|
||||||
descriptors,
|
descriptors,
|
||||||
ad => ad.RouteValues.Any(kvp => kvp.Key == "key" && string.IsNullOrEmpty(kvp.Value)));
|
ad => !ad.RouteValues.ContainsKey("key"));
|
||||||
|
|
||||||
var descriptorWithValue = Assert.Single(
|
var descriptorWithValue = Assert.Single(
|
||||||
descriptors,
|
descriptors,
|
||||||
ad => ad.RouteValues.Any(kvp => kvp.Key == "key" && kvp.Value == "value"));
|
ad => ad.RouteValues.ContainsKey("key"));
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal(2, descriptors.Length);
|
Assert.Equal(2, descriptors.Length);
|
||||||
|
|
@ -303,7 +303,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
c.Key == "key" &&
|
c.Key == "key" &&
|
||||||
c.Value == "value");
|
c.Value == "value");
|
||||||
|
|
||||||
Assert.Equal(3, descriptorWithoutValue.RouteValues.Count);
|
Assert.Equal(2, descriptorWithoutValue.RouteValues.Count);
|
||||||
Assert.Single(
|
Assert.Single(
|
||||||
descriptorWithoutValue.RouteValues,
|
descriptorWithoutValue.RouteValues,
|
||||||
c =>
|
c =>
|
||||||
|
|
@ -314,11 +314,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
c =>
|
c =>
|
||||||
c.Key == "action" &&
|
c.Key == "action" &&
|
||||||
c.Value == "OnlyPost");
|
c.Value == "OnlyPost");
|
||||||
Assert.Single(
|
|
||||||
descriptorWithoutValue.RouteValues,
|
|
||||||
c =>
|
|
||||||
c.Key == "key" &&
|
|
||||||
c.Value == string.Empty);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -932,7 +927,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
|
|
||||||
var indexAction = Assert.Single(actionDescriptors, ad => ad.ActionName.Equals("Index"));
|
var indexAction = Assert.Single(actionDescriptors, ad => ad.ActionName.Equals("Index"));
|
||||||
|
|
||||||
Assert.Equal(5, indexAction.RouteValues.Count);
|
Assert.Equal(3, indexAction.RouteValues.Count);
|
||||||
|
|
||||||
var controllerDefault = Assert.Single(indexAction.RouteValues, rd => rd.Key.Equals("controller", StringComparison.OrdinalIgnoreCase));
|
var controllerDefault = Assert.Single(indexAction.RouteValues, rd => rd.Key.Equals("controller", StringComparison.OrdinalIgnoreCase));
|
||||||
Assert.Equal("ConventionalAndAttributeRoutedActionsWithArea", controllerDefault.Value);
|
Assert.Equal("ConventionalAndAttributeRoutedActionsWithArea", controllerDefault.Value);
|
||||||
|
|
@ -942,12 +937,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
|
|
||||||
var areaDefault = Assert.Single(indexAction.RouteValues, rd => rd.Key.Equals("area", StringComparison.OrdinalIgnoreCase));
|
var areaDefault = Assert.Single(indexAction.RouteValues, rd => rd.Key.Equals("area", StringComparison.OrdinalIgnoreCase));
|
||||||
Assert.Equal("Home", areaDefault.Value);
|
Assert.Equal("Home", areaDefault.Value);
|
||||||
|
|
||||||
var mvRouteValueDefault = Assert.Single(indexAction.RouteValues, rd => rd.Key.Equals("key", StringComparison.OrdinalIgnoreCase));
|
|
||||||
Assert.Equal(string.Empty, mvRouteValueDefault.Value);
|
|
||||||
|
|
||||||
var anotherRouteValue = Assert.Single(indexAction.RouteValues, rd => rd.Key.Equals("second", StringComparison.OrdinalIgnoreCase));
|
|
||||||
Assert.Equal(string.Empty, anotherRouteValue.Value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -1355,6 +1344,42 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
Assert.Single(action.ActionConstraints, a => a is ConstraintAttribute);
|
Assert.Single(action.ActionConstraints, a => a is ConstraintAttribute);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OnProviderExecuted_AddsGlobalRouteValues()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var context = new ActionDescriptorProviderContext();
|
||||||
|
context.Results.Add(new ActionDescriptor()
|
||||||
|
{
|
||||||
|
RouteValues = new Dictionary<string, string>()
|
||||||
|
{
|
||||||
|
{ "controller", "Home" },
|
||||||
|
{ "action", "Index" },
|
||||||
|
}
|
||||||
|
});
|
||||||
|
context.Results.Add(new ActionDescriptor()
|
||||||
|
{
|
||||||
|
RouteValues = new Dictionary<string, string>()
|
||||||
|
{
|
||||||
|
{ "page", "/Some/Page" }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var provider = GetProvider();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
provider.OnProvidersExecuted(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(context.Results[0].RouteValues.ContainsKey("page"));
|
||||||
|
Assert.Null(context.Results[0].RouteValues["page"]);
|
||||||
|
|
||||||
|
Assert.True(context.Results[1].RouteValues.ContainsKey("controller"));
|
||||||
|
Assert.Null(context.Results[1].RouteValues["controller"]);
|
||||||
|
Assert.True(context.Results[1].RouteValues.ContainsKey("action"));
|
||||||
|
Assert.Null(context.Results[1].RouteValues["action"]);
|
||||||
|
}
|
||||||
|
|
||||||
private ControllerActionDescriptorProvider GetProvider(
|
private ControllerActionDescriptorProvider GetProvider(
|
||||||
TypeInfo controllerTypeInfo,
|
TypeInfo controllerTypeInfo,
|
||||||
IEnumerable<IFilterMetadata> filters = null)
|
IEnumerable<IFilterMetadata> filters = null)
|
||||||
|
|
|
||||||
|
|
@ -791,6 +791,34 @@ Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary`1[AspNetCore._InjectedP
|
||||||
Assert.Equal(expected, response.Headers.Location.ToString());
|
Assert.Equal(expected, response.Headers.Location.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Controller_RedirectToPage()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expected = "/RedirectToController?param=17";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await Client.GetAsync("/RedirectToPage");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
|
||||||
|
Assert.Equal(expected, response.Headers.Location.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Page_RedirectToController()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expected = "/RedirectToPage?param=92";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await Client.GetAsync("/RedirectToController");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
|
||||||
|
Assert.Equal(expected, response.Headers.Location.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
private async Task AddAntiforgeryHeaders(HttpRequestMessage request)
|
private async Task AddAntiforgeryHeaders(HttpRequestMessage request)
|
||||||
{
|
{
|
||||||
var getResponse = await Client.GetAsync(request.RequestUri);
|
var getResponse = await Client.GetAsync(request.RequestUri);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
// 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 RazorPagesWebSite
|
||||||
|
{
|
||||||
|
public class RedirectController : Controller
|
||||||
|
{
|
||||||
|
[HttpGet("/RedirectToPage")]
|
||||||
|
public IActionResult RedirectToPage()
|
||||||
|
{
|
||||||
|
return RedirectToRoute(new { page = "/RedirectToController", param = 17 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
@page
|
||||||
|
|
||||||
|
@functions {
|
||||||
|
public IActionResult OnGet()
|
||||||
|
{
|
||||||
|
return new RedirectToRouteResult(new { controller = "Redirect", action = "RedirectToPage", param = 92 });
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue