From 925ad75cdfe677c819c0b8d1c7ee83122dbab5c4 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Fri, 31 Mar 2017 14:34:29 -0700 Subject: [PATCH] Add convenience methods for redirecting to another Razor Page Fixes #5956 --- .../MvcRazorPagesMvcCoreBuilderExtensions.cs | 11 +- .../Internal/PageLoggerExtensions.cs | 10 +- .../Internal/RedirectToPageResultExecutor.cs | 50 ++++ .../Page.cs | 78 ++++++ .../PageModel.cs | 78 ++++++ .../PageUrlHelperExtensions.cs | 114 ++++++++ .../Properties/Resources.Designer.cs | 14 + .../RedirectToPageResult.cs | 143 ++++++++++ .../Resources.resx | 3 + .../RazorPagesTest.cs | 28 ++ .../PageUrlHelperExtensionsTest.cs | 250 ++++++++++++++++++ .../RedirectToPageResultTest.cs | 151 +++++++++++ .../Pages/Redirects/Redirect.cshtml | 1 + .../Pages/Redirects/RedirectFromModel.cs | 10 + .../Pages/Redirects/RedirectFromModel.cshtml | 2 + .../Pages/Redirects/RedirectFromPage.cshtml | 5 + 16 files changed, 945 insertions(+), 3 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/RedirectToPageResultExecutor.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.RazorPages/PageUrlHelperExtensions.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.RazorPages/RedirectToPageResult.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.RazorPages.Test/PageUrlHelperExtensionsTest.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.RazorPages.Test/RedirectToPageResultTest.cs create mode 100644 test/WebSites/RazorPagesWebSite/Pages/Redirects/Redirect.cshtml create mode 100644 test/WebSites/RazorPagesWebSite/Pages/Redirects/RedirectFromModel.cs create mode 100644 test/WebSites/RazorPagesWebSite/Pages/Redirects/RedirectFromModel.cshtml create mode 100644 test/WebSites/RazorPagesWebSite/Pages/Redirects/RedirectFromPage.cshtml diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs index ac90544772..495d137bcc 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs @@ -53,15 +53,19 @@ namespace Microsoft.Extensions.DependencyInjection // Internal for testing. internal static void AddServices(IServiceCollection services) { + // Options services.TryAddEnumerable( ServiceDescriptor.Transient, RazorPagesOptionsSetup>()); + // Action Invoker services.TryAddEnumerable( ServiceDescriptor.Singleton()); + services.TryAddSingleton(); services.TryAddEnumerable( ServiceDescriptor.Singleton()); + // Page and Page model creation and activation services.TryAddSingleton(); services.TryAddSingleton(); @@ -70,12 +74,15 @@ namespace Microsoft.Extensions.DependencyInjection services.TryAddSingleton(); services.TryAddSingleton(); - services.TryAddSingleton(); + // Page model binding services.TryAddSingleton(); - services.TryAddSingleton(); + // Action executors + services.TryAddSingleton(); + services.TryAddSingleton(); + // Random infrastructure services.TryAddSingleton(); } } diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageLoggerExtensions.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageLoggerExtensions.cs index 63e4f4f0a5..ad8589227b 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageLoggerExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageLoggerExtensions.cs @@ -18,6 +18,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal private static readonly Action _pageExecuted; private static readonly Action _exceptionFilterShortCircuit; private static readonly Action _pageFilterShortCircuit; + private static readonly Action _redirectToPageResultExecuting; static PageLoggerExtensions() { @@ -41,6 +42,10 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal 3, "Request was short circuited at page filter '{PageFilter}'."); + _redirectToPageResultExecuting = LoggerMessage.Define( + LogLevel.Information, + 5, + "Executing RedirectToPageResult, redirecting to {Page}."); } public static IDisposable PageScope(this ILogger logger, ActionDescriptor actionDescriptor) @@ -82,6 +87,9 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal _pageFilterShortCircuit(logger, filter, null); } + public static void RedirectToPageResultExecuting(this ILogger logger, string page) + => _redirectToPageResultExecuting(logger, page, null); + private class PageLogScope : IReadOnlyList> { private readonly ActionDescriptor _action; @@ -111,7 +119,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal public IEnumerator> GetEnumerator() { - for (int i = 0; i < Count; ++i) + for (var i = 0; i < Count; ++i) { yield return this[i]; } diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/RedirectToPageResultExecutor.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/RedirectToPageResultExecutor.cs new file mode 100644 index 0000000000..8a7494cdb9 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/RedirectToPageResultExecutor.cs @@ -0,0 +1,50 @@ +// 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 Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal +{ + public class RedirectToPageResultExecutor + { + private readonly ILogger _logger; + private readonly IUrlHelperFactory _urlHelperFactory; + + public RedirectToPageResultExecutor(ILoggerFactory loggerFactory, IUrlHelperFactory urlHelperFactory) + { + if (loggerFactory == null) + { + throw new ArgumentNullException(nameof(loggerFactory)); + } + + if (urlHelperFactory == null) + { + throw new ArgumentNullException(nameof(urlHelperFactory)); + } + + _logger = loggerFactory.CreateLogger(); + _urlHelperFactory = urlHelperFactory; + } + + public void Execute(ActionContext context, RedirectToPageResult result) + { + var urlHelper = result.UrlHelper ?? _urlHelperFactory.GetUrlHelper(context); + var destinationUrl = urlHelper.Page( + result.PageName, + result.RouteValues, + result.Protocol, + result.Host, + fragment: result.Fragment); + + if (string.IsNullOrEmpty(destinationUrl)) + { + throw new InvalidOperationException(Resources.FormatNoRoutesMatched(result.PageName)); + } + + _logger.RedirectToPageResultExecuting(result.PageName); + context.HttpContext.Response.Redirect(destinationUrl, result.Permanent); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Page.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Page.cs index 32e73ad0b3..8d39d29d36 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Page.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Page.cs @@ -138,6 +138,84 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages return new RedirectResult(url); } + /// + /// Redirects () to the specified . + /// + /// The name of the page. + /// The . + protected RedirectToPageResult RedirectToPage(string pageName) + => RedirectToPage(pageName, routeValues: null); + + /// + /// Redirects () to the specified + /// using the specified . + /// + /// The name of the page. + /// The parameters for a route. + /// The . + protected RedirectToPageResult RedirectToPage(string pageName, object routeValues) + => RedirectToPage(pageName, routeValues, fragment: null); + + /// + /// Redirects () to the specified + /// using the specified . + /// + /// The name of the page. + /// The fragment to add to the URL. + /// The . + protected RedirectToPageResult RedirectToPage(string pageName, string fragment) + => RedirectToPage(pageName, routeValues: null, fragment: fragment); + + /// + /// Redirects () to the specified + /// using the specified and . + /// + /// The name of the page. + /// The parameters for a route. + /// The fragment to add to the URL. + /// The . + protected RedirectToPageResult RedirectToPage(string pageName, object routeValues, string fragment) + => new RedirectToPageResult(pageName, routeValues, fragment); + + /// + /// Redirects () to the specified . + /// + /// The name of the page. + /// The with set. + protected RedirectToPageResult RedirectToPagePermanent(string pageName) + => RedirectToPagePermanent(pageName, routeValues: null); + + /// + /// Redirects () to the specified + /// using the specified . + /// + /// The name of the page. + /// The parameters for a route. + /// The with set. + protected RedirectToPageResult RedirectToPagePermanent(string pageName, object routeValues) + => RedirectToPagePermanent(pageName, routeValues, fragment: null); + + /// + /// Redirects () to the specified + /// using the specified . + /// + /// The name of the page. + /// The fragment to add to the URL. + /// The with set. + protected RedirectToPageResult RedirectToPagePermanent(string pageName, string fragment) + => RedirectToPagePermanent(pageName, routeValues: null, fragment: fragment); + + /// + /// Redirects () to the specified + /// using the specified and . + /// + /// The name of the page. + /// The parameters for a route. + /// The fragment to add to the URL. + /// The with set. + protected RedirectToPageResult RedirectToPagePermanent(string pageName, object routeValues, string fragment) + => new RedirectToPageResult(pageName, routeValues, permanent: true, fragment: fragment); + /// /// Creates a object that renders this page as a view to the response. /// diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/PageModel.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/PageModel.cs index 292b1e4d0a..c5ca5d259e 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/PageModel.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/PageModel.cs @@ -176,6 +176,84 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages return new RedirectResult(url); } + /// + /// Redirects () to the specified . + /// + /// The name of the page. + /// The . + protected internal RedirectToPageResult RedirectToPage(string pageName) + => RedirectToPage(pageName, routeValues: null); + + /// + /// Redirects () to the specified + /// using the specified . + /// + /// The name of the page. + /// The parameters for a route. + /// The . + protected internal RedirectToPageResult RedirectToPage(string pageName, object routeValues) + => RedirectToPage(pageName, routeValues, fragment: null); + + /// + /// Redirects () to the specified + /// using the specified . + /// + /// The name of the page. + /// The fragment to add to the URL. + /// The . + protected internal RedirectToPageResult RedirectToPage(string pageName, string fragment) + => RedirectToPage(pageName, routeValues: null, fragment: fragment); + + /// + /// Redirects () to the specified + /// using the specified and . + /// + /// The name of the page. + /// The parameters for a route. + /// The fragment to add to the URL. + /// The . + protected internal RedirectToPageResult RedirectToPage(string pageName, object routeValues, string fragment) + => new RedirectToPageResult(pageName, routeValues, fragment); + + /// + /// Redirects () to the specified . + /// + /// The name of the page. + /// The with set. + protected internal RedirectToPageResult RedirectToPagePermanent(string pageName) + => RedirectToPagePermanent(pageName, routeValues: null); + + /// + /// Redirects () to the specified + /// using the specified . + /// + /// The name of the page. + /// The parameters for a route. + /// The with set. + protected internal RedirectToPageResult RedirectToPagePermanent(string pageName, object routeValues) + => RedirectToPagePermanent(pageName, routeValues, fragment: null); + + /// + /// Redirects () to the specified + /// using the specified . + /// + /// The name of the page. + /// The fragment to add to the URL. + /// The with set. + protected internal RedirectToPageResult RedirectToPagePermanent(string pageName, string fragment) + => RedirectToPagePermanent(pageName, routeValues: null, fragment: fragment); + + /// + /// Redirects () to the specified + /// using the specified and . + /// + /// The name of the page. + /// The parameters for a route. + /// The fragment to add to the URL. + /// The with set. + protected internal RedirectToPageResult RedirectToPagePermanent(string pageName, object routeValues, string fragment) + => new RedirectToPageResult(pageName, routeValues, permanent: true, fragment: fragment); + /// /// Creates a object that renders the page. /// diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/PageUrlHelperExtensions.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/PageUrlHelperExtensions.cs new file mode 100644 index 0000000000..f8927dd29d --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/PageUrlHelperExtensions.cs @@ -0,0 +1,114 @@ +// 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 Microsoft.AspNetCore.Routing; + +namespace Microsoft.AspNetCore.Mvc +{ + /// + /// Razor page specific extensions for . + /// + public static class PageNameUrlHelperExtensions + { + /// + /// Generates a URL with an absolute path for the specified . + /// + /// The . + /// The page name to generate the url for. + /// The generated URL. + public static string Page(this IUrlHelper urlHelper, string pageName) + => Page(urlHelper, pageName, values: null); + + /// + /// Generates a URL with an absolute path for the specified . + /// + /// The . + /// The page name to generate the url for. + /// An object that contains route values. + /// The generated URL. + public static string Page( + this IUrlHelper urlHelper, + string pageName, + object values) + => Page(urlHelper, pageName, values, protocol: null); + + /// + /// Generates a URL with an absolute path for the specified . + /// + /// The . + /// The page name to generate the url for. + /// An object that contains route values. + /// The protocol for the URL, such as "http" or "https". + /// The generated URL. + public static string Page( + this IUrlHelper urlHelper, + string pageName, + object values, + string protocol) + => Page(urlHelper, pageName, values, protocol, host: null, fragment: null); + + /// + /// Generates a URL with an absolute path for the specified . + /// + /// The . + /// The page name to generate the url for. + /// An object that contains route values. + /// The protocol for the URL, such as "http" or "https". + /// The host name for the URL. + /// The generated URL. + public static string Page( + this IUrlHelper urlHelper, + string pageName, + object values, + string protocol, + string host) + => Page(urlHelper, pageName, values, protocol, host, fragment: null); + + /// + /// Generates a URL with an absolute path for the specified . + /// + /// The . + /// The page name to generate the url for. + /// An object that contains route values. + /// The protocol for the URL, such as "http" or "https". + /// The host name for the URL. + /// The fragment for the URL. + /// The generated URL. + public static string Page( + this IUrlHelper urlHelper, + string pageName, + object values, + string protocol, + string host, + string fragment) + { + if (urlHelper == null) + { + throw new ArgumentNullException(nameof(urlHelper)); + } + + var routeValues = new RouteValueDictionary(values); + if (pageName == null) + { + var ambientValues = urlHelper.ActionContext.RouteData.Values; + if (!routeValues.ContainsKey("page") && + ambientValues.TryGetValue("page", out var value)) + { + routeValues["page"] = value; + } + } + else + { + routeValues["page"] = pageName; + } + + return urlHelper.RouteUrl( + routeName: null, + values: routeValues, + protocol: protocol, + host: host, + fragment: fragment); + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Properties/Resources.Designer.cs index cd6c6a8aa1..433b9d9b37 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Properties/Resources.Designer.cs @@ -122,6 +122,20 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages internal static string FormatPathMustBeAnAppRelativePath() => GetString("PathMustBeAnAppRelativePath"); + /// + /// No page named '{0}' matches the supplied values. + /// + internal static string NoRoutesMatched + { + get => GetString("NoRoutesMatched"); + } + + /// + /// No page named '{0}' matches the supplied values. + /// + internal static string FormatNoRoutesMatched(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("NoRoutesMatched"), p0); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/RedirectToPageResult.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/RedirectToPageResult.cs new file mode 100644 index 0000000000..33311dbfd2 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/RedirectToPageResult.cs @@ -0,0 +1,143 @@ +// 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 Microsoft.AspNetCore.Mvc.RazorPages.Internal; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Mvc.RazorPages +{ + /// + /// An that returns a Found (302) + /// or Moved Permanently (301) response with a Location header. + /// Targets a registered route. + /// + public class RedirectToPageResult : ActionResult, IKeepTempDataResult + { + /// + /// Initializes a new instance of the with the values + /// provided. + /// + /// The page to redirect to. + public RedirectToPageResult(string pageName) + : this(pageName, routeValues: null) + { + } + + /// + /// Initializes a new instance of the with the values + /// provided. + /// + /// The page to redirect to. + /// The parameters for the route. + public RedirectToPageResult(string pageName, object routeValues) + : this(pageName, routeValues, permanent: false) + { + } + + /// + /// Initializes a new instance of the with the values + /// provided. + /// + /// The name of the route. + /// The parameters for the route. + /// If set to true, makes the redirect permanent (301). Otherwise a temporary redirect is used (302). + public RedirectToPageResult( + string pageName, + object routeValues, + bool permanent) + : this(pageName, routeValues, permanent, fragment: null) + { + } + + /// + /// Initializes a new instance of the with the values + /// provided. + /// + /// The name of the route. + /// The parameters for the route. + /// The fragment to add to the URL. + public RedirectToPageResult( + string pageName, + object routeValues, + string fragment) + : this(pageName, routeValues, permanent: false, fragment: fragment) + { + } + + /// + /// Initializes a new instance of the with the values + /// provided. + /// + /// The name of the route. + /// The parameters for the route. + /// If set to true, makes the redirect permanent (301). Otherwise a temporary redirect is used (302). + /// The fragment to add to the URL. + public RedirectToPageResult( + string pageName, + object routeValues, + bool permanent, + string fragment) + { + PageName = pageName; + RouteValues = routeValues == null ? new RouteValueDictionary() : new RouteValueDictionary(routeValues); + Permanent = permanent; + Fragment = fragment; + } + + /// + /// Gets or sets the used to generate URLs. + /// + public IUrlHelper UrlHelper { get; set; } + + /// + /// Gets or sets the name of the page to route to. + /// + public string PageName { get; set; } + + /// + /// Gets or sets the route data to use for generating the URL. + /// + public RouteValueDictionary RouteValues { get; set; } + + /// + /// Gets or sets an indication that the redirect is permanent. + /// + public bool Permanent { get; set; } + + /// + /// Gets or sets the fragment to add to the URL. + /// + public string Fragment { get; set; } + + /// + /// Gets or sets the protocol for the URL, such as "http" or "https". + /// + public string Protocol { get; set; } + + /// + /// Gets or sets the host name of the URL. + /// + public string Host { get; set; } + + /// + public override void ExecuteResult(ActionContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (string.IsNullOrEmpty(PageName)) + { + throw new InvalidOperationException( + Resources.FormatPropertyOfTypeCannotBeNull(nameof(PageName), nameof(RedirectToPageResult))); + } + + var executor = context.HttpContext.RequestServices.GetRequiredService(); + executor.Execute(context, this); + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Resources.resx b/src/Microsoft.AspNetCore.Mvc.RazorPages/Resources.resx index 2e8f00040c..d20e8d0844 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Resources.resx +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Resources.resx @@ -141,4 +141,7 @@ Path must be an application relative path that starts with a forward slash '/'. + + No page named '{0}' matches the supplied values. + \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs index 7ac6625d94..4e85ff597a 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs @@ -649,6 +649,34 @@ Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary`1[AspNetCore._InjectedP Assert.Equal(expected, response.Trim()); } + [Fact] + public async Task RedirectFromPageWorks() + { + // Arrange + var expected = "/Pages/Redirects/Redirect/10"; + + // Act + var response = await Client.GetAsync("/Pages/Redirects/RedirectFromPage"); + + // Assert + Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); + Assert.Equal(expected, response.Headers.Location.ToString()); + } + + [Fact] + public async Task RedirectFromPageModelWorks() + { + // Arrange + var expected = "/Pages/Redirects/Redirect/12"; + + // Act + var response = await Client.GetAsync("/Pages/Redirects/RedirectFromModel"); + + // Assert + Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); + Assert.Equal(expected, response.Headers.Location.ToString()); + } + private async Task AddAntiforgeryHeaders(HttpRequestMessage request) { var getResponse = await Client.GetAsync(request.RequestUri); diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/PageUrlHelperExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/PageUrlHelperExtensionsTest.cs new file mode 100644 index 0000000000..716640c14b --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/PageUrlHelperExtensionsTest.cs @@ -0,0 +1,250 @@ +// 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.Collections.Generic; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.AspNetCore.Routing; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.RazorPages +{ + public class PageUrlHelperExtensionsTest + { + [Fact] + public void Page_WithName_Works() + { + // Arrange + UrlRouteContext actual = null; + var routeData = new RouteData + { + Values = + { + { "page", "ambient-page" }, + } + }; + var actionContext = new ActionContext + { + RouteData = routeData, + }; + var urlHelper = new Mock(); + urlHelper.SetupGet(h => h.ActionContext) + .Returns(actionContext); + urlHelper.Setup(h => h.RouteUrl(It.IsAny())) + .Callback((UrlRouteContext context) => actual = context); + + // Act + urlHelper.Object.Page("TestPage"); + + // Assert + urlHelper.Verify(); + Assert.NotNull(actual); + Assert.Null(actual.RouteName); + Assert.Collection(Assert.IsType(actual.Values), + value => + { + Assert.Equal("page", value.Key); + Assert.Equal("TestPage", value.Value); + }); + Assert.Null(actual.Host); + Assert.Null(actual.Protocol); + Assert.Null(actual.Fragment); + } + + public static TheoryData Page_WithNameAndRouteValues_WorksData + { + get => new TheoryData + { + { new { id = 10 } }, + { + new Dictionary + { + ["id"] = 10, + } + }, + { + new RouteValueDictionary + { + ["id"] = 10, + } + }, + }; + } + + [Theory] + [MemberData(nameof(Page_WithNameAndRouteValues_WorksData))] + public void Page_WithNameAndRouteValues_Works(object values) + { + // Arrange + UrlRouteContext actual = null; + var urlHelper = new Mock(); + urlHelper.Setup(h => h.RouteUrl(It.IsAny())) + .Callback((UrlRouteContext context) => actual = context); + + // Act + urlHelper.Object.Page("TestPage", values); + + // Assert + urlHelper.Verify(); + Assert.NotNull(actual); + Assert.Null(actual.RouteName); + Assert.Collection(Assert.IsType(actual.Values), + value => + { + Assert.Equal("id", value.Key); + Assert.Equal(10, value.Value); + }, + value => + { + Assert.Equal("page", value.Key); + Assert.Equal("TestPage", value.Value); + }); + Assert.Null(actual.Host); + Assert.Null(actual.Protocol); + Assert.Null(actual.Fragment); + } + + [Fact] + public void Page_WithNameRouteValuesAndProtocol_Works() + { + // Arrange + UrlRouteContext actual = null; + var urlHelper = new Mock(); + urlHelper.Setup(h => h.RouteUrl(It.IsAny())) + .Callback((UrlRouteContext context) => actual = context); + + // Act + urlHelper.Object.Page("TestPage", new { id = 13 }, "https"); + + // Assert + urlHelper.Verify(); + Assert.NotNull(actual); + Assert.Null(actual.RouteName); + Assert.Collection(Assert.IsType(actual.Values), + value => + { + Assert.Equal("id", value.Key); + Assert.Equal(13, value.Value); + }, + value => + { + Assert.Equal("page", value.Key); + Assert.Equal("TestPage", value.Value); + }); + Assert.Equal("https", actual.Protocol); + Assert.Null(actual.Host); + Assert.Null(actual.Fragment); + } + + [Fact] + public void Page_WithNameRouteValuesProtocolAndHost_Works() + { + // Arrange + UrlRouteContext actual = null; + var urlHelper = new Mock(); + urlHelper.Setup(h => h.RouteUrl(It.IsAny())) + .Callback((UrlRouteContext context) => actual = context); + + // Act + urlHelper.Object.Page("TestPage", new { id = 13 }, "https", "mytesthost"); + + // Assert + urlHelper.Verify(); + Assert.NotNull(actual); + Assert.Null(actual.RouteName); + Assert.Collection(Assert.IsType(actual.Values), + value => + { + Assert.Equal("id", value.Key); + Assert.Equal(13, value.Value); + }, + value => + { + Assert.Equal("page", value.Key); + Assert.Equal("TestPage", value.Value); + }); + Assert.Equal("https", actual.Protocol); + Assert.Equal("mytesthost", actual.Host); + Assert.Null(actual.Fragment); + } + + [Fact] + public void Page_WithNameRouteValuesProtocolHostAndFragment_Works() + { + // Arrange + UrlRouteContext actual = null; + var urlHelper = new Mock(); + urlHelper.Setup(h => h.RouteUrl(It.IsAny())) + .Callback((UrlRouteContext context) => actual = context); + + // Act + urlHelper.Object.Page("TestPage", new { id = 13 }, "https", "mytesthost", "#toc"); + + // Assert + urlHelper.Verify(); + Assert.NotNull(actual); + Assert.Null(actual.RouteName); + Assert.Collection(Assert.IsType(actual.Values), + value => + { + Assert.Equal("id", value.Key); + Assert.Equal(13, value.Value); + }, + value => + { + Assert.Equal("page", value.Key); + Assert.Equal("TestPage", value.Value); + }); + Assert.Equal("https", actual.Protocol); + Assert.Equal("mytesthost", actual.Host); + Assert.Equal("#toc", actual.Fragment); + } + + [Fact] + public void Page_UsesAmbientRouteValue_WhenPageIsNull() + { + // Arrange + UrlRouteContext actual = null; + var routeData = new RouteData + { + Values = + { + { "page", "ambient-page" }, + } + }; + var actionContext = new ActionContext + { + RouteData = routeData, + }; + + var urlHelper = new Mock(); + urlHelper.SetupGet(p => p.ActionContext) + .Returns(actionContext); + urlHelper.Setup(h => h.RouteUrl(It.IsAny())) + .Callback((UrlRouteContext context) => actual = context); + + // Act + string page = null; + urlHelper.Object.Page(page, new { id = 13 }, "https", "mytesthost", "#toc"); + + // Assert + urlHelper.Verify(); + Assert.NotNull(actual); + Assert.Null(actual.RouteName); + Assert.Collection(Assert.IsType(actual.Values), + value => + { + Assert.Equal("id", value.Key); + Assert.Equal(13, value.Value); + }, + value => + { + Assert.Equal("page", value.Key); + Assert.Equal("ambient-page", value.Value); + }); + Assert.Equal("https", actual.Protocol); + Assert.Equal("mytesthost", actual.Host); + Assert.Equal("#toc", actual.Fragment); + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/RedirectToPageResultTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/RedirectToPageResultTest.cs new file mode 100644 index 0000000000..a284fc5ef5 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/RedirectToPageResultTest.cs @@ -0,0 +1,151 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.RazorPages.Internal; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.RazorPages +{ + public class RedirectToPageResultTest + { + [Fact] + public async Task ExecuteResultAsync_ThrowsOnNullUrl() + { + // Arrange + var httpContext = new DefaultHttpContext + { + RequestServices = CreateServices(), + }; + + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + + var urlHelper = GetUrlHelper(returnValue: null); + var result = new RedirectToPageResult("some-page", new Dictionary()) + { + UrlHelper = urlHelper, + }; + + // Act & Assert + await ExceptionAssert.ThrowsAsync( + () => result.ExecuteResultAsync(actionContext), + "No page named 'some-page' matches the supplied values."); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ExecuteResultAsync_PassesCorrectValuesToRedirect(bool permanentRedirect) + { + // Arrange + var expectedUrl = "SampleAction"; + + var httpContext = new Mock(); + var httpResponse = new Mock(); + httpContext.SetupGet(c => c.RequestServices) + .Returns(CreateServices()); + httpContext.SetupGet(c => c.Response) + .Returns(httpResponse.Object); + + var actionContext = new ActionContext( + httpContext.Object, + new RouteData(), + new ActionDescriptor()); + + var urlHelper = GetUrlHelper(expectedUrl); + var result = new RedirectToPageResult("MyPage", new { id = 10, test = "value" }, permanentRedirect) + { + UrlHelper = urlHelper, + }; + + // Act + await result.ExecuteResultAsync(actionContext); + + // Assert + httpResponse.Verify(r => r.Redirect(expectedUrl, permanentRedirect), Times.Exactly(1)); + } + + [Fact] + public async Task ExecuteResultAsync_WithAllParameters() + { + // Arrange + var httpContext = new DefaultHttpContext + { + RequestServices = CreateServices(), + }; + + var pageContext = new PageContext + { + HttpContext = httpContext, + }; + + UrlRouteContext context = null; + var urlHelper = new Mock(); + urlHelper.Setup(h => h.RouteUrl(It.IsAny())) + .Callback((UrlRouteContext c) => context = c) + .Returns("some-value"); + var values = new { test = "test-value" }; + var result = new RedirectToPageResult("MyPage", values, true, "test-fragment") + { + UrlHelper = urlHelper.Object, + Protocol = "ftp", + }; + + // Act + await result.ExecuteResultAsync(pageContext); + + // Assert + Assert.NotNull(context); + Assert.Null(context.RouteName); + Assert.Collection(Assert.IsType(context.Values), + value => + { + Assert.Equal("test", value.Key); + Assert.Equal("test-value", value.Value); + }, + value => + { + Assert.Equal("page", value.Key); + Assert.Equal("MyPage", value.Value); + }); + Assert.Equal("ftp", context.Protocol); + Assert.Equal("test-fragment", context.Fragment); + } + + private static IServiceProvider CreateServices(IUrlHelperFactory factory = null) + { + var services = new ServiceCollection(); + services.AddSingleton(); + + if (factory != null) + { + services.AddSingleton(factory); + } + else + { + services.AddSingleton(); + } + + services.AddSingleton(NullLoggerFactory.Instance); + return services.BuildServiceProvider(); + } + + private static IUrlHelper GetUrlHelper(string returnValue) + { + var urlHelper = new Mock(); + urlHelper.Setup(o => o.RouteUrl(It.IsAny())).Returns(returnValue); + return urlHelper.Object; + } + } +} diff --git a/test/WebSites/RazorPagesWebSite/Pages/Redirects/Redirect.cshtml b/test/WebSites/RazorPagesWebSite/Pages/Redirects/Redirect.cshtml new file mode 100644 index 0000000000..71ebf6dd23 --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Pages/Redirects/Redirect.cshtml @@ -0,0 +1 @@ +@page "{id:int}" diff --git a/test/WebSites/RazorPagesWebSite/Pages/Redirects/RedirectFromModel.cs b/test/WebSites/RazorPagesWebSite/Pages/Redirects/RedirectFromModel.cs new file mode 100644 index 0000000000..a2d593bed4 --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Pages/Redirects/RedirectFromModel.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace RazorPagesWebSite +{ + public class RedirectFromModel : PageModel + { + public IActionResult OnGet() => RedirectToPage("/Pages/Redirects/Redirect", new { id = 12}); + } +} diff --git a/test/WebSites/RazorPagesWebSite/Pages/Redirects/RedirectFromModel.cshtml b/test/WebSites/RazorPagesWebSite/Pages/Redirects/RedirectFromModel.cshtml new file mode 100644 index 0000000000..b3631bd5fd --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Pages/Redirects/RedirectFromModel.cshtml @@ -0,0 +1,2 @@ +@page +@model RazorPagesWebSite.RedirectFromModel diff --git a/test/WebSites/RazorPagesWebSite/Pages/Redirects/RedirectFromPage.cshtml b/test/WebSites/RazorPagesWebSite/Pages/Redirects/RedirectFromPage.cshtml new file mode 100644 index 0000000000..4c44dc4690 --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Pages/Redirects/RedirectFromPage.cshtml @@ -0,0 +1,5 @@ +@page +@functions +{ + public IActionResult OnGet() => RedirectToPage("/Pages/Redirects/Redirect", new { id = 10}); +} \ No newline at end of file