Add convenience methods for redirecting to another Razor Page
Fixes #5956
This commit is contained in:
parent
2d19a82678
commit
925ad75cdf
|
|
@ -53,15 +53,19 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
// Internal for testing.
|
||||
internal static void AddServices(IServiceCollection services)
|
||||
{
|
||||
// Options
|
||||
services.TryAddEnumerable(
|
||||
ServiceDescriptor.Transient<IConfigureOptions<RazorPagesOptions>, RazorPagesOptionsSetup>());
|
||||
|
||||
// Action Invoker
|
||||
services.TryAddEnumerable(
|
||||
ServiceDescriptor.Singleton<IActionDescriptorProvider, PageActionDescriptorProvider>());
|
||||
services.TryAddSingleton<IActionDescriptorChangeProvider, PageActionDescriptorChangeProvider>();
|
||||
|
||||
services.TryAddEnumerable(
|
||||
ServiceDescriptor.Singleton<IActionInvokerProvider, PageActionInvokerProvider>());
|
||||
|
||||
// Page and Page model creation and activation
|
||||
services.TryAddSingleton<IPageModelActivatorProvider, DefaultPageModelActivatorProvider>();
|
||||
services.TryAddSingleton<IPageModelFactoryProvider, DefaultPageModelFactoryProvider>();
|
||||
|
||||
|
|
@ -70,12 +74,15 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
|
||||
services.TryAddSingleton<IPageLoader, DefaultPageLoader>();
|
||||
services.TryAddSingleton<IPageHandlerMethodSelector, DefaultPageHandlerMethodSelector>();
|
||||
services.TryAddSingleton<PageResultExecutor>();
|
||||
|
||||
// Page model binding
|
||||
services.TryAddSingleton<PageArgumentBinder, DefaultPageArgumentBinder>();
|
||||
|
||||
services.TryAddSingleton<IActionDescriptorChangeProvider, PageActionDescriptorChangeProvider>();
|
||||
// Action executors
|
||||
services.TryAddSingleton<PageResultExecutor>();
|
||||
services.TryAddSingleton<RedirectToPageResultExecutor>();
|
||||
|
||||
// Random infrastructure
|
||||
services.TryAddSingleton<TempDataPropertyProvider>();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
|||
private static readonly Action<ILogger, string, double, Exception> _pageExecuted;
|
||||
private static readonly Action<ILogger, object, Exception> _exceptionFilterShortCircuit;
|
||||
private static readonly Action<ILogger, object, Exception> _pageFilterShortCircuit;
|
||||
private static readonly Action<ILogger, string, Exception> _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<string>(
|
||||
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<KeyValuePair<string, object>>
|
||||
{
|
||||
private readonly ActionDescriptor _action;
|
||||
|
|
@ -111,7 +119,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
|||
|
||||
public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
|
||||
{
|
||||
for (int i = 0; i < Count; ++i)
|
||||
for (var i = 0; i < Count; ++i)
|
||||
{
|
||||
yield return this[i];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<RedirectToRouteResult>();
|
||||
_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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -138,6 +138,84 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
|
|||
return new RedirectResult(url);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Redirects (<see cref="StatusCodes.Status302Found"/>) to the specified <paramref name="pageName"/>.
|
||||
/// </summary>
|
||||
/// <param name="pageName">The name of the page.</param>
|
||||
/// <returns>The <see cref="RedirectToPageResult"/>.</returns>
|
||||
protected RedirectToPageResult RedirectToPage(string pageName)
|
||||
=> RedirectToPage(pageName, routeValues: null);
|
||||
|
||||
/// <summary>
|
||||
/// Redirects (<see cref="StatusCodes.Status302Found"/>) to the specified <paramref name="pageName"/>
|
||||
/// using the specified <paramref name="routeValues"/>.
|
||||
/// </summary>
|
||||
/// <param name="pageName">The name of the page.</param>
|
||||
/// <param name="routeValues">The parameters for a route.</param>
|
||||
/// <returns>The <see cref="RedirectToPageResult"/>.</returns>
|
||||
protected RedirectToPageResult RedirectToPage(string pageName, object routeValues)
|
||||
=> RedirectToPage(pageName, routeValues, fragment: null);
|
||||
|
||||
/// <summary>
|
||||
/// Redirects (<see cref="StatusCodes.Status302Found"/>) to the specified <paramref name="pageName"/>
|
||||
/// using the specified <paramref name="fragment"/>.
|
||||
/// </summary>
|
||||
/// <param name="pageName">The name of the page.</param>
|
||||
/// <param name="fragment">The fragment to add to the URL.</param>
|
||||
/// <returns>The <see cref="RedirectToPageResult"/>.</returns>
|
||||
protected RedirectToPageResult RedirectToPage(string pageName, string fragment)
|
||||
=> RedirectToPage(pageName, routeValues: null, fragment: fragment);
|
||||
|
||||
/// <summary>
|
||||
/// Redirects (<see cref="StatusCodes.Status302Found"/>) to the specified <paramref name="pageName"/>
|
||||
/// using the specified <paramref name="routeValues"/> and <paramref name="fragment"/>.
|
||||
/// </summary>
|
||||
/// <param name="pageName">The name of the page.</param>
|
||||
/// <param name="routeValues">The parameters for a route.</param>
|
||||
/// <param name="fragment">The fragment to add to the URL.</param>
|
||||
/// <returns>The <see cref="RedirectToPageResult"/>.</returns>
|
||||
protected RedirectToPageResult RedirectToPage(string pageName, object routeValues, string fragment)
|
||||
=> new RedirectToPageResult(pageName, routeValues, fragment);
|
||||
|
||||
/// <summary>
|
||||
/// Redirects (<see cref="StatusCodes.Status301MovedPermanently"/>) to the specified <paramref name="pageName"/>.
|
||||
/// </summary>
|
||||
/// <param name="pageName">The name of the page.</param>
|
||||
/// <returns>The <see cref="RedirectToPageResult"/> with <see cref="RedirectToPageResult.Permanent"/> set.</returns>
|
||||
protected RedirectToPageResult RedirectToPagePermanent(string pageName)
|
||||
=> RedirectToPagePermanent(pageName, routeValues: null);
|
||||
|
||||
/// <summary>
|
||||
/// Redirects (<see cref="StatusCodes.Status301MovedPermanently"/>) to the specified <paramref name="pageName"/>
|
||||
/// using the specified <paramref name="routeValues"/>.
|
||||
/// </summary>
|
||||
/// <param name="pageName">The name of the page.</param>
|
||||
/// <param name="routeValues">The parameters for a route.</param>
|
||||
/// <returns>The <see cref="RedirectToPageResult"/> with <see cref="RedirectToPageResult.Permanent"/> set.</returns>
|
||||
protected RedirectToPageResult RedirectToPagePermanent(string pageName, object routeValues)
|
||||
=> RedirectToPagePermanent(pageName, routeValues, fragment: null);
|
||||
|
||||
/// <summary>
|
||||
/// Redirects (<see cref="StatusCodes.Status301MovedPermanently"/>) to the specified <paramref name="pageName"/>
|
||||
/// using the specified <paramref name="fragment"/>.
|
||||
/// </summary>
|
||||
/// <param name="pageName">The name of the page.</param>
|
||||
/// <param name="fragment">The fragment to add to the URL.</param>
|
||||
/// <returns>The <see cref="RedirectToPageResult"/> with <see cref="RedirectToPageResult.Permanent"/> set.</returns>
|
||||
protected RedirectToPageResult RedirectToPagePermanent(string pageName, string fragment)
|
||||
=> RedirectToPagePermanent(pageName, routeValues: null, fragment: fragment);
|
||||
|
||||
/// <summary>
|
||||
/// Redirects (<see cref="StatusCodes.Status301MovedPermanently"/>) to the specified <paramref name="pageName"/>
|
||||
/// using the specified <paramref name="routeValues"/> and <paramref name="fragment"/>.
|
||||
/// </summary>
|
||||
/// <param name="pageName">The name of the page.</param>
|
||||
/// <param name="routeValues">The parameters for a route.</param>
|
||||
/// <param name="fragment">The fragment to add to the URL.</param>
|
||||
/// <returns>The <see cref="RedirectToPageResult"/> with <see cref="RedirectToPageResult.Permanent"/> set.</returns>
|
||||
protected RedirectToPageResult RedirectToPagePermanent(string pageName, object routeValues, string fragment)
|
||||
=> new RedirectToPageResult(pageName, routeValues, permanent: true, fragment: fragment);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="PageViewResult"/> object that renders this page as a view to the response.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -176,6 +176,84 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
|
|||
return new RedirectResult(url);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Redirects (<see cref="StatusCodes.Status302Found"/>) to the specified <paramref name="pageName"/>.
|
||||
/// </summary>
|
||||
/// <param name="pageName">The name of the page.</param>
|
||||
/// <returns>The <see cref="RedirectToPageResult"/>.</returns>
|
||||
protected internal RedirectToPageResult RedirectToPage(string pageName)
|
||||
=> RedirectToPage(pageName, routeValues: null);
|
||||
|
||||
/// <summary>
|
||||
/// Redirects (<see cref="StatusCodes.Status302Found"/>) to the specified <paramref name="pageName"/>
|
||||
/// using the specified <paramref name="routeValues"/>.
|
||||
/// </summary>
|
||||
/// <param name="pageName">The name of the page.</param>
|
||||
/// <param name="routeValues">The parameters for a route.</param>
|
||||
/// <returns>The <see cref="RedirectToPageResult"/>.</returns>
|
||||
protected internal RedirectToPageResult RedirectToPage(string pageName, object routeValues)
|
||||
=> RedirectToPage(pageName, routeValues, fragment: null);
|
||||
|
||||
/// <summary>
|
||||
/// Redirects (<see cref="StatusCodes.Status302Found"/>) to the specified <paramref name="pageName"/>
|
||||
/// using the specified <paramref name="fragment"/>.
|
||||
/// </summary>
|
||||
/// <param name="pageName">The name of the page.</param>
|
||||
/// <param name="fragment">The fragment to add to the URL.</param>
|
||||
/// <returns>The <see cref="RedirectToPageResult"/>.</returns>
|
||||
protected internal RedirectToPageResult RedirectToPage(string pageName, string fragment)
|
||||
=> RedirectToPage(pageName, routeValues: null, fragment: fragment);
|
||||
|
||||
/// <summary>
|
||||
/// Redirects (<see cref="StatusCodes.Status302Found"/>) to the specified <paramref name="pageName"/>
|
||||
/// using the specified <paramref name="routeValues"/> and <paramref name="fragment"/>.
|
||||
/// </summary>
|
||||
/// <param name="pageName">The name of the page.</param>
|
||||
/// <param name="routeValues">The parameters for a route.</param>
|
||||
/// <param name="fragment">The fragment to add to the URL.</param>
|
||||
/// <returns>The <see cref="RedirectToPageResult"/>.</returns>
|
||||
protected internal RedirectToPageResult RedirectToPage(string pageName, object routeValues, string fragment)
|
||||
=> new RedirectToPageResult(pageName, routeValues, fragment);
|
||||
|
||||
/// <summary>
|
||||
/// Redirects (<see cref="StatusCodes.Status301MovedPermanently"/>) to the specified <paramref name="pageName"/>.
|
||||
/// </summary>
|
||||
/// <param name="pageName">The name of the page.</param>
|
||||
/// <returns>The <see cref="RedirectToPageResult"/> with <see cref="RedirectToPageResult.Permanent"/> set.</returns>
|
||||
protected internal RedirectToPageResult RedirectToPagePermanent(string pageName)
|
||||
=> RedirectToPagePermanent(pageName, routeValues: null);
|
||||
|
||||
/// <summary>
|
||||
/// Redirects (<see cref="StatusCodes.Status301MovedPermanently"/>) to the specified <paramref name="pageName"/>
|
||||
/// using the specified <paramref name="routeValues"/>.
|
||||
/// </summary>
|
||||
/// <param name="pageName">The name of the page.</param>
|
||||
/// <param name="routeValues">The parameters for a route.</param>
|
||||
/// <returns>The <see cref="RedirectToPageResult"/> with <see cref="RedirectToPageResult.Permanent"/> set.</returns>
|
||||
protected internal RedirectToPageResult RedirectToPagePermanent(string pageName, object routeValues)
|
||||
=> RedirectToPagePermanent(pageName, routeValues, fragment: null);
|
||||
|
||||
/// <summary>
|
||||
/// Redirects (<see cref="StatusCodes.Status301MovedPermanently"/>) to the specified <paramref name="pageName"/>
|
||||
/// using the specified <paramref name="fragment"/>.
|
||||
/// </summary>
|
||||
/// <param name="pageName">The name of the page.</param>
|
||||
/// <param name="fragment">The fragment to add to the URL.</param>
|
||||
/// <returns>The <see cref="RedirectToPageResult"/> with <see cref="RedirectToPageResult.Permanent"/> set.</returns>
|
||||
protected internal RedirectToPageResult RedirectToPagePermanent(string pageName, string fragment)
|
||||
=> RedirectToPagePermanent(pageName, routeValues: null, fragment: fragment);
|
||||
|
||||
/// <summary>
|
||||
/// Redirects (<see cref="StatusCodes.Status301MovedPermanently"/>) to the specified <paramref name="pageName"/>
|
||||
/// using the specified <paramref name="routeValues"/> and <paramref name="fragment"/>.
|
||||
/// </summary>
|
||||
/// <param name="pageName">The name of the page.</param>
|
||||
/// <param name="routeValues">The parameters for a route.</param>
|
||||
/// <param name="fragment">The fragment to add to the URL.</param>
|
||||
/// <returns>The <see cref="RedirectToPageResult"/> with <see cref="RedirectToPageResult.Permanent"/> set.</returns>
|
||||
protected internal RedirectToPageResult RedirectToPagePermanent(string pageName, object routeValues, string fragment)
|
||||
=> new RedirectToPageResult(pageName, routeValues, permanent: true, fragment: fragment);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="PageViewResult"/> object that renders the page.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Razor page specific extensions for <see cref="IUrlHelper"/>.
|
||||
/// </summary>
|
||||
public static class PageNameUrlHelperExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a URL with an absolute path for the specified <paramref name="pageName"/>.
|
||||
/// </summary>
|
||||
/// <param name="urlHelper">The <see cref="IUrlHelper"/>.</param>
|
||||
/// <param name="pageName">The page name to generate the url for.</param>
|
||||
/// <returns>The generated URL.</returns>
|
||||
public static string Page(this IUrlHelper urlHelper, string pageName)
|
||||
=> Page(urlHelper, pageName, values: null);
|
||||
|
||||
/// <summary>
|
||||
/// Generates a URL with an absolute path for the specified <paramref name="pageName"/>.
|
||||
/// </summary>
|
||||
/// <param name="urlHelper">The <see cref="IUrlHelper"/>.</param>
|
||||
/// <param name="pageName">The page name to generate the url for.</param>
|
||||
/// <param name="values">An object that contains route values.</param>
|
||||
/// <returns>The generated URL.</returns>
|
||||
public static string Page(
|
||||
this IUrlHelper urlHelper,
|
||||
string pageName,
|
||||
object values)
|
||||
=> Page(urlHelper, pageName, values, protocol: null);
|
||||
|
||||
/// <summary>
|
||||
/// Generates a URL with an absolute path for the specified <paramref name="pageName"/>.
|
||||
/// </summary>
|
||||
/// <param name="urlHelper">The <see cref="IUrlHelper"/>.</param>
|
||||
/// <param name="pageName">The page name to generate the url for.</param>
|
||||
/// <param name="values">An object that contains route values.</param>
|
||||
/// <param name="protocol">The protocol for the URL, such as "http" or "https".</param>
|
||||
/// <returns>The generated URL.</returns>
|
||||
public static string Page(
|
||||
this IUrlHelper urlHelper,
|
||||
string pageName,
|
||||
object values,
|
||||
string protocol)
|
||||
=> Page(urlHelper, pageName, values, protocol, host: null, fragment: null);
|
||||
|
||||
/// <summary>
|
||||
/// Generates a URL with an absolute path for the specified <paramref name="pageName"/>.
|
||||
/// </summary>
|
||||
/// <param name="urlHelper">The <see cref="IUrlHelper"/>.</param>
|
||||
/// <param name="pageName">The page name to generate the url for.</param>
|
||||
/// <param name="values">An object that contains route values.</param>
|
||||
/// <param name="protocol">The protocol for the URL, such as "http" or "https".</param>
|
||||
/// <param name="host">The host name for the URL.</param>
|
||||
/// <returns>The generated URL.</returns>
|
||||
public static string Page(
|
||||
this IUrlHelper urlHelper,
|
||||
string pageName,
|
||||
object values,
|
||||
string protocol,
|
||||
string host)
|
||||
=> Page(urlHelper, pageName, values, protocol, host, fragment: null);
|
||||
|
||||
/// <summary>
|
||||
/// Generates a URL with an absolute path for the specified <paramref name="pageName"/>.
|
||||
/// </summary>
|
||||
/// <param name="urlHelper">The <see cref="IUrlHelper"/>.</param>
|
||||
/// <param name="pageName">The page name to generate the url for.</param>
|
||||
/// <param name="values">An object that contains route values.</param>
|
||||
/// <param name="protocol">The protocol for the URL, such as "http" or "https".</param>
|
||||
/// <param name="host">The host name for the URL.</param>
|
||||
/// <param name="fragment">The fragment for the URL.</param>
|
||||
/// <returns>The generated URL.</returns>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -122,6 +122,20 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
|
|||
internal static string FormatPathMustBeAnAppRelativePath()
|
||||
=> GetString("PathMustBeAnAppRelativePath");
|
||||
|
||||
/// <summary>
|
||||
/// No page named '{0}' matches the supplied values.
|
||||
/// </summary>
|
||||
internal static string NoRoutesMatched
|
||||
{
|
||||
get => GetString("NoRoutesMatched");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// No page named '{0}' matches the supplied values.
|
||||
/// </summary>
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="ActionResult"/> that returns a Found (302)
|
||||
/// or Moved Permanently (301) response with a Location header.
|
||||
/// Targets a registered route.
|
||||
/// </summary>
|
||||
public class RedirectToPageResult : ActionResult, IKeepTempDataResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RedirectToPageResult"/> with the values
|
||||
/// provided.
|
||||
/// </summary>
|
||||
/// <param name="pageName">The page to redirect to.</param>
|
||||
public RedirectToPageResult(string pageName)
|
||||
: this(pageName, routeValues: null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RedirectToPageResult"/> with the values
|
||||
/// provided.
|
||||
/// </summary>
|
||||
/// <param name="pageName">The page to redirect to.</param>
|
||||
/// <param name="routeValues">The parameters for the route.</param>
|
||||
public RedirectToPageResult(string pageName, object routeValues)
|
||||
: this(pageName, routeValues, permanent: false)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RedirectToPageResult"/> with the values
|
||||
/// provided.
|
||||
/// </summary>
|
||||
/// <param name="pageName">The name of the route.</param>
|
||||
/// <param name="routeValues">The parameters for the route.</param>
|
||||
/// <param name="permanent">If set to true, makes the redirect permanent (301). Otherwise a temporary redirect is used (302).</param>
|
||||
public RedirectToPageResult(
|
||||
string pageName,
|
||||
object routeValues,
|
||||
bool permanent)
|
||||
: this(pageName, routeValues, permanent, fragment: null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RedirectToPageResult"/> with the values
|
||||
/// provided.
|
||||
/// </summary>
|
||||
/// <param name="pageName">The name of the route.</param>
|
||||
/// <param name="routeValues">The parameters for the route.</param>
|
||||
/// <param name="fragment">The fragment to add to the URL.</param>
|
||||
public RedirectToPageResult(
|
||||
string pageName,
|
||||
object routeValues,
|
||||
string fragment)
|
||||
: this(pageName, routeValues, permanent: false, fragment: fragment)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RedirectToPageResult"/> with the values
|
||||
/// provided.
|
||||
/// </summary>
|
||||
/// <param name="pageName">The name of the route.</param>
|
||||
/// <param name="routeValues">The parameters for the route.</param>
|
||||
/// <param name="permanent">If set to true, makes the redirect permanent (301). Otherwise a temporary redirect is used (302).</param>
|
||||
/// <param name="fragment">The fragment to add to the URL.</param>
|
||||
public RedirectToPageResult(
|
||||
string pageName,
|
||||
object routeValues,
|
||||
bool permanent,
|
||||
string fragment)
|
||||
{
|
||||
PageName = pageName;
|
||||
RouteValues = routeValues == null ? new RouteValueDictionary() : new RouteValueDictionary(routeValues);
|
||||
Permanent = permanent;
|
||||
Fragment = fragment;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="IUrlHelper" /> used to generate URLs.
|
||||
/// </summary>
|
||||
public IUrlHelper UrlHelper { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the page to route to.
|
||||
/// </summary>
|
||||
public string PageName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the route data to use for generating the URL.
|
||||
/// </summary>
|
||||
public RouteValueDictionary RouteValues { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an indication that the redirect is permanent.
|
||||
/// </summary>
|
||||
public bool Permanent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the fragment to add to the URL.
|
||||
/// </summary>
|
||||
public string Fragment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the protocol for the URL, such as "http" or "https".
|
||||
/// </summary>
|
||||
public string Protocol { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the host name of the URL.
|
||||
/// </summary>
|
||||
public string Host { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
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<RedirectToPageResultExecutor>();
|
||||
executor.Execute(context, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -141,4 +141,7 @@
|
|||
<data name="PathMustBeAnAppRelativePath" xml:space="preserve">
|
||||
<value>Path must be an application relative path that starts with a forward slash '/'.</value>
|
||||
</data>
|
||||
<data name="NoRoutesMatched" xml:space="preserve">
|
||||
<value>No page named '{0}' matches the supplied values.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<IUrlHelper>();
|
||||
urlHelper.SetupGet(h => h.ActionContext)
|
||||
.Returns(actionContext);
|
||||
urlHelper.Setup(h => h.RouteUrl(It.IsAny<UrlRouteContext>()))
|
||||
.Callback((UrlRouteContext context) => actual = context);
|
||||
|
||||
// Act
|
||||
urlHelper.Object.Page("TestPage");
|
||||
|
||||
// Assert
|
||||
urlHelper.Verify();
|
||||
Assert.NotNull(actual);
|
||||
Assert.Null(actual.RouteName);
|
||||
Assert.Collection(Assert.IsType<RouteValueDictionary>(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<object>
|
||||
{
|
||||
{ new { id = 10 } },
|
||||
{
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
["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<IUrlHelper>();
|
||||
urlHelper.Setup(h => h.RouteUrl(It.IsAny<UrlRouteContext>()))
|
||||
.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<RouteValueDictionary>(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<IUrlHelper>();
|
||||
urlHelper.Setup(h => h.RouteUrl(It.IsAny<UrlRouteContext>()))
|
||||
.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<RouteValueDictionary>(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<IUrlHelper>();
|
||||
urlHelper.Setup(h => h.RouteUrl(It.IsAny<UrlRouteContext>()))
|
||||
.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<RouteValueDictionary>(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<IUrlHelper>();
|
||||
urlHelper.Setup(h => h.RouteUrl(It.IsAny<UrlRouteContext>()))
|
||||
.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<RouteValueDictionary>(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<IUrlHelper>();
|
||||
urlHelper.SetupGet(p => p.ActionContext)
|
||||
.Returns(actionContext);
|
||||
urlHelper.Setup(h => h.RouteUrl(It.IsAny<UrlRouteContext>()))
|
||||
.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<RouteValueDictionary>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<string, object>())
|
||||
{
|
||||
UrlHelper = urlHelper,
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
await ExceptionAssert.ThrowsAsync<InvalidOperationException>(
|
||||
() => 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<HttpContext>();
|
||||
var httpResponse = new Mock<HttpResponse>();
|
||||
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<IUrlHelper>();
|
||||
urlHelper.Setup(h => h.RouteUrl(It.IsAny<UrlRouteContext>()))
|
||||
.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<RouteValueDictionary>(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<RedirectToPageResultExecutor>();
|
||||
|
||||
if (factory != null)
|
||||
{
|
||||
services.AddSingleton(factory);
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddSingleton<IUrlHelperFactory, UrlHelperFactory>();
|
||||
}
|
||||
|
||||
services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
private static IUrlHelper GetUrlHelper(string returnValue)
|
||||
{
|
||||
var urlHelper = new Mock<IUrlHelper>();
|
||||
urlHelper.Setup(o => o.RouteUrl(It.IsAny<UrlRouteContext>())).Returns(returnValue);
|
||||
return urlHelper.Object;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
@page "{id:int}"
|
||||
|
|
@ -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});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
@page
|
||||
@model RazorPagesWebSite.RedirectFromModel
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
@page
|
||||
@functions
|
||||
{
|
||||
public IActionResult OnGet() => RedirectToPage("/Pages/Redirects/Redirect", new { id = 10});
|
||||
}
|
||||
Loading…
Reference in New Issue