diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageActionDescriptorProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageActionDescriptorProvider.cs index 04be8a1803..816020e60b 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageActionDescriptorProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageActionDescriptorProvider.cs @@ -88,6 +88,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure Name = selector.AttributeRouteModel.Name, Order = selector.AttributeRouteModel.Order ?? 0, Template = selector.AttributeRouteModel.Template, + SuppressLinkGeneration = selector.AttributeRouteModel.SuppressLinkGeneration, + SuppressPathMatching = selector.AttributeRouteModel.SuppressPathMatching, }, DisplayName = $"Page: {model.ViewEnginePath}", FilterDescriptors = filters, @@ -95,7 +97,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure RelativePath = model.RelativePath, RouteValues = new Dictionary(StringComparer.OrdinalIgnoreCase) { - { "page", model.ViewEnginePath}, + { "page", model.ViewEnginePath }, }, ViewEnginePath = model.ViewEnginePath, }); diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageSelectorModel.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageSelectorModel.cs index 4690d2e434..35e2bb4bd0 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageSelectorModel.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageSelectorModel.cs @@ -20,11 +20,16 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal model.RelativePath)); } - model.Selectors.Add(CreateSelectorModel(model.ViewEnginePath, routeTemplate)); + var selectorModel = CreateSelectorModel(model.ViewEnginePath, routeTemplate); + model.Selectors.Add(selectorModel); var fileName = Path.GetFileName(model.RelativePath); if (string.Equals(IndexFileName, fileName, StringComparison.OrdinalIgnoreCase)) { + // For pages ending in /Index.cshtml, we want to allow incoming routing, but + // force outgoing routes to match to the path sans /Index. + selectorModel.AttributeRouteModel.SuppressLinkGeneration = true; + var parentDirectoryPath = model.ViewEnginePath; var index = parentDirectoryPath.LastIndexOf('/'); if (index == -1) diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs index 2fd32e6d4d..dd0728b6ff 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs @@ -142,5 +142,33 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests // Assert Assert.Equal(expected, response.Trim()); } + + [Fact] + public async Task RedirectFromPage_RedirectsToPathWithoutIndexSegment() + { + //Arrange + var expected = "/Redirects"; + + // Act + var response = await Client.GetAsync("/Redirects/Index"); + + // Assert + Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); + Assert.Equal(expected, response.Headers.Location.ToString()); + } + + [Fact] + public async Task RedirectFromPage_ToIndex_RedirectsToPathWithoutIndexSegment() + { + //Arrange + var expected = "/Redirects"; + + // Act + var response = await Client.GetAsync("/Redirects/RedirectToIndex"); + + // Assert + Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); + Assert.Equal(expected, response.Headers.Location.ToString()); + } } } diff --git a/test/WebSites/RazorPagesWebSite/Pages/Redirects/Index.cshtml b/test/WebSites/RazorPagesWebSite/Pages/Redirects/Index.cshtml new file mode 100644 index 0000000000..ec1558997c --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Pages/Redirects/Index.cshtml @@ -0,0 +1,7 @@ +@page "{formaction?}" +@functions +{ + public IActionResult OnGet() => RedirectToPage(); + + public IActionResult OnGetRedirectToIndex() => RedirectToPage("/Redirects/Index"); +}