From 597a0c9a11408f0b6b88c40bd8fcfae93229adb7 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Thu, 25 May 2017 14:59:21 -0700 Subject: [PATCH] Add support for friendly routes in pages FIxes #6325 --- .../RazorPagesOptionsExtensions.cs | 86 +++++++++++++++---- .../RazorPagesTest.cs | 13 +++ .../RazorPagesOptionsExtensionsTest.cs | 74 ++++++++++++++++ test/WebSites/RazorPagesWebSite/Startup.cs | 1 + 4 files changed, 155 insertions(+), 19 deletions(-) diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/RazorPagesOptionsExtensions.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/RazorPagesOptionsExtensions.cs index a14df1b6a8..fa8d0f142a 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/RazorPagesOptionsExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/RazorPagesOptionsExtensions.cs @@ -59,30 +59,30 @@ namespace Microsoft.Extensions.DependencyInjection } /// - /// Adds a to the page with the specified path. + /// Adds a to the page with the specified name. /// /// The to configure. - /// The path of the Razor Page. + /// The page name. /// The . - public static RazorPagesOptions AllowAnonymousToPage(this RazorPagesOptions options, string path) + public static RazorPagesOptions AllowAnonymousToPage(this RazorPagesOptions options, string pageName) { if (options == null) { throw new ArgumentNullException(nameof(options)); } - if (string.IsNullOrEmpty(path)) + if (string.IsNullOrEmpty(pageName)) { - throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(path)); + throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(pageName)); } var anonymousFilter = new AllowAnonymousFilter(); - options.Conventions.Add(new PageConvention(path, model => model.Filters.Add(anonymousFilter))); + options.Conventions.Add(new PageConvention(pageName, model => model.Filters.Add(anonymousFilter))); return options; } /// - /// Adds a to all pages under the specified path. + /// Adds a to all pages under the specified folder. /// /// The to configure. /// The folder path. @@ -105,40 +105,40 @@ namespace Microsoft.Extensions.DependencyInjection } /// - /// Adds a with the specified policy to the page with the specified path. + /// Adds a with the specified policy to the page with the specified name. /// /// The to configure. - /// The path of the Razor Page. + /// The page name. /// The authorization policy. /// The . - public static RazorPagesOptions AuthorizePage(this RazorPagesOptions options, string path, string policy) + public static RazorPagesOptions AuthorizePage(this RazorPagesOptions options, string pageName, string policy) { if (options == null) { throw new ArgumentNullException(nameof(options)); } - if (string.IsNullOrEmpty(path)) + if (string.IsNullOrEmpty(pageName)) { - throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(path)); + throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(pageName)); } var authorizeFilter = new AuthorizeFilter(policy); - options.Conventions.Add(new PageConvention(path, model => model.Filters.Add(authorizeFilter))); + options.Conventions.Add(new PageConvention(pageName, model => model.Filters.Add(authorizeFilter))); return options; } /// - /// Adds a to the page with the specified path. + /// Adds a to the page with the specified name. /// /// The to configure. - /// The path of the Razor Page. + /// The page name. /// The . - public static RazorPagesOptions AuthorizePage(this RazorPagesOptions options, string path) => - AuthorizePage(options, path, policy: string.Empty); + public static RazorPagesOptions AuthorizePage(this RazorPagesOptions options, string pageName) => + AuthorizePage(options, pageName, policy: string.Empty); /// - /// Adds a with the specified policy to all pages under the specified path. + /// Adds a with the specified policy to all pages under the specified folder. /// /// The to configure. /// The folder path. @@ -162,7 +162,7 @@ namespace Microsoft.Extensions.DependencyInjection } /// - /// Adds a to all pages under the specified path. + /// Adds a to all pages under the specified folder. /// /// The to configure. /// The folder path. @@ -170,6 +170,54 @@ namespace Microsoft.Extensions.DependencyInjection public static RazorPagesOptions AuthorizeFolder(this RazorPagesOptions options, string folderPath) => AuthorizeFolder(options, folderPath, policy: string.Empty); + /// + /// Adds the specified to the page at the specified . + /// + /// The page can be routed via in addition to the default set of path based routes. + /// All links generated for this page will use the specified route. + /// + /// + /// The . + /// The page name. + /// The route to associate with the page. + /// The . + public static RazorPagesOptions AddPageRoute(this RazorPagesOptions options, string pageName, string route) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + if (string.IsNullOrEmpty(pageName)) + { + throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(pageName)); + } + + if (string.IsNullOrEmpty(route)) + { + throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(route)); + } + + options.Conventions.Add(new PageConvention(pageName, model => + { + // Use the route specified in MapPageRoute for outbound routing. + foreach (var selector in model.Selectors) + { + selector.AttributeRouteModel.SuppressLinkGeneration = true; + } + + model.Selectors.Add(new SelectorModel + { + AttributeRouteModel = new AttributeRouteModel + { + Template = route, + } + }); + })); + + return options; + } + private class PageConvention : IPageApplicationModelConvention { private readonly string _path; diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs index ac3bdc29b1..196563e4cd 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs @@ -1024,6 +1024,19 @@ Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary`1[AspNetCore._InjectedP Assert.StartsWith(expected, response.Trim()); } + [Fact] + public async Task PagesCanByRoutedViaRoute_AddedViaAddPageRoute() + { + // Arrange + var expected = "Hello, test!"; + + // Act + var response = await Client.GetStringAsync("/Different-Route/test"); + + // Assert + Assert.StartsWith(expected, response.Trim()); + } + private async Task AddAntiforgeryHeaders(HttpRequestMessage request) { var getResponse = await Client.GetAsync(request.RequestUri); diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/DependencyInjection/RazorPagesOptionsExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/DependencyInjection/RazorPagesOptionsExtensionsTest.cs index fb09882688..9741a0456f 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/DependencyInjection/RazorPagesOptionsExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/DependencyInjection/RazorPagesOptionsExtensionsTest.cs @@ -242,6 +242,80 @@ namespace Microsoft.Extensions.DependencyInjection }); } + [Fact] + public void AddPageRoute_AddsRouteToSelector() + { + // Arrange + var options = new RazorPagesOptions(); + var models = new[] + { + new PageApplicationModel("/Pages/Index.cshtml", "/Index.cshtml") + { + Selectors = + { + CreateSelectorModel("Index", suppressLinkGeneration: true), + CreateSelectorModel(""), + } + }, + new PageApplicationModel("/Pages/About.cshtml", "/About.cshtml") + { + Selectors = + { + CreateSelectorModel("About"), + } + } + }; + + // Act + options.AddPageRoute("/Index.cshtml", "Different-Route"); + ApplyConventions(options, models); + + // Assert + Assert.Collection(models, + model => + { + Assert.Equal("/Index.cshtml", model.ViewEnginePath); + Assert.Collection(model.Selectors, + selector => + { + Assert.Equal("Index", selector.AttributeRouteModel.Template); + Assert.True(selector.AttributeRouteModel.SuppressLinkGeneration); + }, + selector => + { + Assert.Equal("", selector.AttributeRouteModel.Template); + Assert.True(selector.AttributeRouteModel.SuppressLinkGeneration); + }, + selector => + { + Assert.Equal("Different-Route", selector.AttributeRouteModel.Template); + Assert.False(selector.AttributeRouteModel.SuppressLinkGeneration); + }); + }, + model => + { + Assert.Equal("/About.cshtml", model.ViewEnginePath); + Assert.Collection(model.Selectors, + selector => + { + Assert.Equal("About", selector.AttributeRouteModel.Template); + Assert.False(selector.AttributeRouteModel.SuppressLinkGeneration); + }); + }); + } + + private static SelectorModel CreateSelectorModel(string template, bool suppressLinkGeneration = false) + { + return new SelectorModel + { + AttributeRouteModel = new AttributeRouteModel + { + Template = template, + SuppressLinkGeneration = suppressLinkGeneration + }, + }; + } + private static void ApplyConventions(RazorPagesOptions options, PageApplicationModel[] models) { foreach (var convention in options.Conventions) diff --git a/test/WebSites/RazorPagesWebSite/Startup.cs b/test/WebSites/RazorPagesWebSite/Startup.cs index 8a12746fcc..d4c369bfb1 100644 --- a/test/WebSites/RazorPagesWebSite/Startup.cs +++ b/test/WebSites/RazorPagesWebSite/Startup.cs @@ -19,6 +19,7 @@ namespace RazorPagesWebSite options.AuthorizePage("/HelloWorldWithAuth"); options.AuthorizeFolder("/Pages/Admin"); options.AllowAnonymousToPage("/Pages/Admin/Login"); + options.AddPageRoute("/HelloWorldWithRoute", "Different-Route/{text}"); }) .WithRazorPagesAtContentRoot(); }