diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageRouteModelFactory.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageRouteModelFactory.cs index 0b75e6a22d..682d55141f 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageRouteModelFactory.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageRouteModelFactory.cs @@ -60,21 +60,15 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { model.RouteValues.Add("page", model.ViewEnginePath); - if (AttributeRouteModel.IsOverridePattern(routeTemplate)) - { - throw new InvalidOperationException(string.Format( - Resources.PageActionDescriptorProvider_RouteTemplateCannotBeOverrideable, - model.RelativePath)); - } - var selectorModel = CreateSelectorModel(pageRoute, routeTemplate); model.Selectors.Add(selectorModel); var fileName = Path.GetFileName(model.RelativePath); - if (string.Equals(IndexFileName, fileName, StringComparison.OrdinalIgnoreCase)) + if (!AttributeRouteModel.IsOverridePattern(routeTemplate) && + 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. + // For pages without an override route, and 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 index = pageRoute.LastIndexOf('/'); diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Resources.resx b/src/Microsoft.AspNetCore.Mvc.RazorPages/Resources.resx index be0f94f186..90e0ea9300 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Resources.resx +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Resources.resx @@ -117,9 +117,6 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - The route for the page at '{0}' cannot start with / or ~/. Pages do not support overriding the file path of the page. - The '{0}' property of '{1}' must not be null. diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs index 856e88cb94..ea36e1bb7c 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs @@ -1233,6 +1233,16 @@ Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary`1[AspNetCore.InjectedPa Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); } + [Fact] + public async Task Page_CanOverrideRouteTemplate() + { + // Arrange & Act + var content = await Client.GetStringAsync("like-totally-custom"); + + // Assert + Assert.Equal("

Hey, it's Mr. totally custom here!

", content.Trim()); + } + private async Task AddAntiforgeryHeaders(HttpRequestMessage request) { var getResponse = await Client.GetAsync(request.RequestUri); diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/CompiledPageRouteModelProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/CompiledPageRouteModelProviderTest.cs index 011681f152..b270ee3389 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/CompiledPageRouteModelProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/CompiledPageRouteModelProviderTest.cs @@ -479,23 +479,40 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal } [Fact] - public void OnProvidersExecuting_ThrowsIfRouteTemplateHasOverridePattern() + public void OnProvidersExecuting_AllowsRouteTemplatesWithOverridePattern() { // Arrange var descriptors = new[] { - CreateVersion_2_0_Descriptor("/Pages/Index.cshtml"), + CreateVersion_2_0_Descriptor("/Pages/Index.cshtml", "~/some-other-prefix"), CreateVersion_2_0_Descriptor("/Pages/Home.cshtml", "/some-prefix"), }; var provider = CreateProvider(descriptors: descriptors); var context = new PageRouteModelProviderContext(); - // Act & Assert - var exception = Assert.Throws(() => provider.OnProvidersExecuting(context)); - Assert.Equal( - "The route for the page at '/Pages/Home.cshtml' cannot start with / or ~/. Pages do not support overriding the file path of the page.", - exception.Message); + // Act + provider.OnProvidersExecuting(context); + + // Assert + Assert.Collection( + context.RouteModels, + result => + { + Assert.Equal("/Pages/Index.cshtml", result.RelativePath); + Assert.Equal("/Index", result.ViewEnginePath); + Assert.Collection( + result.Selectors, + selector => Assert.Equal("some-other-prefix", selector.AttributeRouteModel.Template)); + }, + result => + { + Assert.Equal("/Pages/Home.cshtml", result.RelativePath); + Assert.Equal("/Home", result.ViewEnginePath); + Assert.Collection( + result.Selectors, + selector => Assert.Equal("some-prefix", selector.AttributeRouteModel.Template)); + }); } private TestCompiledPageRouteModelProvider CreateProvider( diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageRouteModelFactoryTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageRouteModelFactoryTest.cs index 7e8aeeb734..67bb226d06 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageRouteModelFactoryTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageRouteModelFactoryTest.cs @@ -67,6 +67,34 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal }); } + [Fact] + public void CreateRouteModel_DoesNotAddMultipleSelectorsWhenRouteTemplateIsAbsolute() + { + // Arrange + var relativePath = "/Pages/Users/Profile/Index.cshtml"; + var options = new RazorPagesOptions(); + var routeModelFactory = new PageRouteModelFactory(options, NullLogger.Instance); + + // Act + var routeModel = routeModelFactory.CreateRouteModel(relativePath, "/my-override"); + + // Assert + Assert.Equal(relativePath, routeModel.RelativePath); + Assert.Equal("/Users/Profile/Index", routeModel.ViewEnginePath); + + Assert.Collection( + routeModel.Selectors, + selector => Assert.Equal("my-override", selector.AttributeRouteModel.Template)); + + Assert.Collection( + routeModel.RouteValues, + kvp => + { + Assert.Equal("page", kvp.Key); + Assert.Equal("/Users/Profile/Index", kvp.Value); + }); + } + [Fact] public void CreateAreaRouteModel_AddsSelector() { diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/RazorProjectPageRouteModelProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/RazorProjectPageRouteModelProviderTest.cs index 37fd8c6ac6..8054e21a4f 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/RazorProjectPageRouteModelProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/RazorProjectPageRouteModelProviderTest.cs @@ -282,7 +282,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal } [Fact] - public void OnProvidersExecuting_ThrowsIfRouteTemplateHasOverridePattern() + public void OnProvidersExecuting_AllowsRouteTemplateWithOverridePattern() { // Arrange var fileProvider = new TestFileProvider(); @@ -296,10 +296,20 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal var provider = new RazorProjectPageRouteModelProvider(project, optionsManager, NullLoggerFactory.Instance); var context = new PageRouteModelProviderContext(); - // Act & Assert - var ex = Assert.Throws(() => provider.OnProvidersExecuting(context)); - Assert.Equal("The route for the page at '/Index.cshtml' cannot start with / or ~/. Pages do not support overriding the file path of the page.", - ex.Message); + // Act + provider.OnProvidersExecuting(context); + + // Assert + Assert.Collection( + context.RouteModels, + model => + { + Assert.Equal("/Index.cshtml", model.RelativePath); + Assert.Equal("/Index", model.ViewEnginePath); + Assert.Collection( + model.Selectors, + selector => Assert.Equal("custom-route", selector.AttributeRouteModel.Template)); + }); } [Fact] diff --git a/test/WebSites/RazorPagesWebSite/Pages/CustomPrefix/Custom.cshtml b/test/WebSites/RazorPagesWebSite/Pages/CustomPrefix/Custom.cshtml new file mode 100644 index 0000000000..ace20dc097 --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Pages/CustomPrefix/Custom.cshtml @@ -0,0 +1,3 @@ +@page "~/like-totally-custom" + +

Hey, it's Mr. totally custom here!