diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Routing/ControllerLinkGeneratorExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Core/Routing/ControllerLinkGeneratorExtensions.cs
new file mode 100644
index 0000000000..01481bf488
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Routing/ControllerLinkGeneratorExtensions.cs
@@ -0,0 +1,281 @@
+// 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 Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.Mvc.Routing;
+using System;
+
+namespace Microsoft.AspNetCore.Routing
+{
+ ///
+ /// Extension methods for using to generate links to MVC controllers.
+ ///
+ public static class ControllerLinkGeneratorExtensions
+ {
+ private static readonly LinkGenerationTemplateOptions _templateOptions = new LinkGenerationTemplateOptions()
+ {
+ UseAmbientValues = true,
+ };
+
+ ///
+ /// Generates a URI with an absolute path based on the provided values.
+ ///
+ /// The .
+ /// The associated with the current request.
+ ///
+ /// The action name. Used to resolve endpoints. Optional. If null is provided, the current action route value
+ /// will be used.
+ ///
+ ///
+ /// The controller name. Used to resolve endpoints. Optional. If null is provided, the current controller route value
+ /// will be used.
+ ///
+ /// The route values. Optional. Used to resolve endpoints and expand parameters in the route template.
+ ///
+ /// An optional URI path base. Prepended to the path in the resulting URI. If not provided, the value of will be used.
+ ///
+ /// A URI fragment. Optional. Appended to the resulting URI.
+ ///
+ /// An optional . Settings on provided object override the settings with matching
+ /// names from RouteOptions.
+ ///
+ /// A URI with an absolute path, or null if a URI cannot be created.
+ public static string GetPathByAction(
+ this LinkGenerator generator,
+ HttpContext httpContext,
+ string action = default,
+ string controller = default,
+ object values = default,
+ PathString? pathBase = default,
+ FragmentString fragment = default,
+ LinkOptions options = default)
+ {
+ if (generator == null)
+ {
+ throw new ArgumentNullException(nameof(generator));
+ }
+
+ if (httpContext == null)
+ {
+ throw new ArgumentNullException(nameof(httpContext));
+ }
+
+ var address = CreateAddress(httpContext, action, controller, values);
+ return generator.GetPathByAddress(
+ httpContext,
+ address,
+ address.ExplicitValues,
+ address.AmbientValues,
+ pathBase,
+ fragment,
+ options);
+ }
+
+ ///
+ /// Generates a URI with an absolute path based on the provided values.
+ ///
+ /// The .
+ /// The action name. Used to resolve endpoints.
+ /// The controller name. Used to resolve endpoints.
+ /// The route values. Optional. Used to resolve endpoints and expand parameters in the route template.
+ /// An optional URI path base. Prepended to the path in the resulting URI.
+ /// A URI fragment. Optional. Appended to the resulting URI.
+ ///
+ /// An optional . Settings on provided object override the settings with matching
+ /// names from RouteOptions.
+ ///
+ /// A URI with an absolute path, or null if a URI cannot be created.
+ public static string GetPathByAction(
+ this LinkGenerator generator,
+ string action,
+ string controller,
+ object values = default,
+ PathString pathBase = default,
+ FragmentString fragment = default,
+ LinkOptions options = default)
+ {
+ if (generator == null)
+ {
+ throw new ArgumentNullException(nameof(generator));
+ }
+
+ if (action == null)
+ {
+ throw new ArgumentNullException(nameof(action));
+ }
+
+ if (controller == null)
+ {
+ throw new ArgumentNullException(nameof(controller));
+ }
+
+ var address = CreateAddress(httpContext: null, action, controller, values);
+ return generator.GetPathByAddress(address, address.ExplicitValues, pathBase, fragment, options);
+ }
+
+ ///
+ /// Generates an absolute URI based on the provided values.
+ ///
+ /// The .
+ /// The associated with the current request.
+ ///
+ /// The action name. Used to resolve endpoints. Optional. If null is provided, the current action route value
+ /// will be used.
+ ///
+ ///
+ /// The controller name. Used to resolve endpoints. Optional. If null is provided, the current controller route value
+ /// will be used.
+ ///
+ /// The route values. Optional. Used to resolve endpoints and expand parameters in the route template.
+ ///
+ /// The URI scheme, applied to the resulting URI. Optional. If not provided, the value of will be used.
+ ///
+ ///
+ /// The URI host/authority, applied to the resulting URI. Optional. If not provided, the value will be used.
+ ///
+ ///
+ /// An optional URI path base. Prepended to the path in the resulting URI. If not provided, the value of will be used.
+ ///
+ /// A URI fragment. Optional. Appended to the resulting URI.
+ ///
+ /// An optional . Settings on provided object override the settings with matching
+ /// names from RouteOptions.
+ ///
+ /// A absolute URI, or null if a URI cannot be created.
+ public static string GetUriByAction(
+ this LinkGenerator generator,
+ HttpContext httpContext,
+ string action = default,
+ string controller = default,
+ object values = default,
+ string scheme = default,
+ HostString? host = default,
+ PathString? pathBase = default,
+ FragmentString fragment = default,
+ LinkOptions options = default)
+ {
+ if (generator == null)
+ {
+ throw new ArgumentNullException(nameof(generator));
+ }
+
+ if (httpContext == null)
+ {
+ throw new ArgumentNullException(nameof(httpContext));
+ }
+
+ var address = CreateAddress(httpContext, action, controller, values);
+ return generator.GetUriByAddress(
+ httpContext,
+ address,
+ address.ExplicitValues,
+ address.AmbientValues,
+ scheme,
+ host,
+ pathBase,
+ fragment,
+ options);
+ }
+
+ ///
+ /// Generates an absolute URI based on the provided values.
+ ///
+ /// The .
+ /// The action name. Used to resolve endpoints.
+ /// The controller name. Used to resolve endpoints.
+ /// The route values. May be null. Used to resolve endpoints and expand parameters in the route template.
+ /// The URI scheme, applied to the resulting URI.
+ /// The URI host/authority, applied to the resulting URI.
+ /// An optional URI path base. Prepended to the path in the resulting URI.
+ /// A URI fragment. Optional. Appended to the resulting URI.
+ ///
+ /// An optional . Settings on provided object override the settings with matching
+ /// names from RouteOptions.
+ ///
+ /// A absolute URI, or null if a URI cannot be created.
+ public static string GetUriByAction(
+ this LinkGenerator generator,
+ string action,
+ string controller,
+ object values,
+ string scheme,
+ HostString host,
+ PathString pathBase = default,
+ FragmentString fragment = default,
+ LinkOptions options = default)
+ {
+ if (generator == null)
+ {
+ throw new ArgumentNullException(nameof(generator));
+ }
+
+ if (action == null)
+ {
+ throw new ArgumentNullException(nameof(action));
+ }
+
+ if (controller == null)
+ {
+ throw new ArgumentNullException(nameof(controller));
+ }
+
+ var address = CreateAddress(httpContext: null, action, controller, values);
+ return generator.GetUriByAddress(address, address.ExplicitValues, scheme, host, pathBase, fragment, options);
+ }
+
+ ///
+ /// Gets a based on the provided , , and .
+ ///
+ /// The .
+ /// The action name. Used to resolve endpoints.
+ /// The controller name. Used to resolve endpoints.
+ /// The route values. Optional. Used to resolve endpoints and expand parameters in the route template.
+ ///
+ /// A if one or more endpoints matching the address can be found, otherwise null.
+ ///
+ public static LinkGenerationTemplate GetTemplateByAction(
+ this LinkGenerator generator,
+ string action,
+ string controller,
+ object values = default)
+ {
+ if (generator == null)
+ {
+ throw new ArgumentNullException(nameof(generator));
+ }
+
+ if (action == null)
+ {
+ throw new ArgumentNullException(nameof(action));
+ }
+
+ if (controller == null)
+ {
+ throw new ArgumentNullException(nameof(controller));
+ }
+
+ var address = CreateAddress(httpContext: null, action, controller, values);
+ return generator.GetTemplateByAddress(address, _templateOptions);
+ }
+
+ private static RouteValuesAddress CreateAddress(HttpContext httpContext, string action, string controller, object values)
+ {
+ var explicitValues = new RouteValueDictionary(values);
+ var ambientValues = GetAmbientValues(httpContext);
+
+ UrlHelperBase.NormalizeRouteValuesForAction(action, controller, explicitValues, ambientValues);
+
+ return new RouteValuesAddress()
+ {
+ AmbientValues = ambientValues,
+ ExplicitValues = explicitValues
+ };
+ }
+
+ private static RouteValueDictionary GetAmbientValues(HttpContext httpContext)
+ {
+ return httpContext?.Features.Get()?.RouteValues;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Routing/PageLinkGeneratorExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Core/Routing/PageLinkGeneratorExtensions.cs
new file mode 100644
index 0000000000..ec508a9645
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Routing/PageLinkGeneratorExtensions.cs
@@ -0,0 +1,268 @@
+// 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 Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.Mvc.Routing;
+using System;
+
+namespace Microsoft.AspNetCore.Routing
+{
+ ///
+ /// Extension methods for using to generate links to Razor Pages.
+ ///
+ public static class PageLinkGeneratorExtensions
+ {
+ private static readonly LinkGenerationTemplateOptions _templateOptions = new LinkGenerationTemplateOptions()
+ {
+ UseAmbientValues = true,
+ };
+
+ ///
+ /// Generates a URI with an absolute path based on the provided values.
+ ///
+ /// The .
+ /// The associated with the current request.
+ ///
+ /// The page name. Used to resolve endpoints. Optional. If null is provided, the current page route value
+ /// will be used.
+ ///
+ ///
+ /// The page handler name. Used to resolve endpoints. Optional.
+ ///
+ /// The route values. Optional. Used to resolve endpoints and expand parameters in the route template.
+ ///
+ /// An optional URI path base. Prepended to the path in the resulting URI. If not provided, the value of will be used.
+ ///
+ /// A URI fragment. Optional. Appended to the resulting URI.
+ ///
+ /// An optional . Settings on provided object override the settings with matching
+ /// names from RouteOptions.
+ ///
+ /// A URI with an absolute path, or null if a URI cannot be created.
+ public static string GetPathByPage(
+ this LinkGenerator generator,
+ HttpContext httpContext,
+ string page = default,
+ string handler = default,
+ object values = default,
+ PathString? pathBase = default,
+ FragmentString fragment = default,
+ LinkOptions options = default)
+ {
+ if (generator == null)
+ {
+ throw new ArgumentNullException(nameof(generator));
+ }
+
+ if (httpContext == null)
+ {
+ throw new ArgumentNullException(nameof(httpContext));
+ }
+
+ var address = CreateAddress(httpContext, page, handler, values);
+ return generator.GetPathByAddress(
+ httpContext,
+ address,
+ address.ExplicitValues,
+ address.AmbientValues,
+ pathBase,
+ fragment,
+ options);
+ }
+
+ ///
+ /// Generates a URI with an absolute path based on the provided values.
+ ///
+ /// The .
+ ///
+ /// The page name. Used to resolve endpoints.
+ ///
+ ///
+ /// The page handler name. Used to resolve endpoints. Optional.
+ ///
+ /// The route values. Optional. Used to resolve endpoints and expand parameters in the route template.
+ /// An optional URI path base. Prepended to the path in the resulting URI.
+ /// A URI fragment. Optional. Appended to the resulting URI.
+ ///
+ /// An optional . Settings on provided object override the settings with matching
+ /// names from RouteOptions.
+ ///
+ /// A URI with an absolute path, or null if a URI cannot be created.
+ public static string GetPathByPage(
+ this LinkGenerator generator,
+ string page,
+ string handler = default,
+ object values = default,
+ PathString pathBase = default,
+ FragmentString fragment = default,
+ LinkOptions options = default)
+ {
+ if (generator == null)
+ {
+ throw new ArgumentNullException(nameof(generator));
+ }
+
+ if (page == null)
+ {
+ throw new ArgumentNullException(nameof(page));
+ }
+
+ var address = CreateAddress(httpContext: null, page, handler, values);
+ return generator.GetPathByAddress(address, address.ExplicitValues, pathBase, fragment, options);
+ }
+
+ ///
+ /// Generates an absolute URI based on the provided values.
+ ///
+ /// The .
+ /// The associated with the current request.
+ ///
+ /// The page name. Used to resolve endpoints. Optional. If null is provided, the current page route value
+ /// will be used.
+ ///
+ ///
+ /// The page handler name. Used to resolve endpoints. Optional.
+ ///
+ /// The route values. Optional. Used to resolve endpoints and expand parameters in the route template.
+ ///
+ /// The URI scheme, applied to the resulting URI. Optional. If not provided, the value of will be used.
+ ///
+ ///
+ /// The URI host/authority, applied to the resulting URI. Optional. If not provided, the value will be used.
+ ///
+ ///
+ /// An optional URI path base. Prepended to the path in the resulting URI. If not provided, the value of will be used.
+ ///
+ /// A URI fragment. Optional. Appended to the resulting URI.
+ ///
+ /// An optional . Settings on provided object override the settings with matching
+ /// names from RouteOptions.
+ ///
+ /// A absolute URI, or null if a URI cannot be created.
+ public static string GetUriByPage(
+ this LinkGenerator generator,
+ HttpContext httpContext,
+ string page = default,
+ string handler = default,
+ object values = default,
+ string scheme = default,
+ HostString? host = default,
+ PathString? pathBase = default,
+ FragmentString fragment = default,
+ LinkOptions options = default)
+ {
+ if (generator == null)
+ {
+ throw new ArgumentNullException(nameof(generator));
+ }
+
+ if (httpContext == null)
+ {
+ throw new ArgumentNullException(nameof(httpContext));
+ }
+
+ var address = CreateAddress(httpContext, page, handler, values);
+ return generator.GetUriByAddress(
+ httpContext,
+ address,
+ address.ExplicitValues,
+ address.AmbientValues,
+ scheme,
+ host,
+ pathBase,
+ fragment,
+ options);
+ }
+
+ ///
+ /// Generates an absolute URI based on the provided values.
+ ///
+ /// The .
+ /// The page name. Used to resolve endpoints.
+ /// The page handler name. May be null.
+ /// The route values. May be null. Used to resolve endpoints and expand parameters in the route template.
+ /// The URI scheme, applied to the resulting URI.
+ /// The URI host/authority, applied to the resulting URI.
+ /// An optional URI path base. Prepended to the path in the resulting URI.
+ /// A URI fragment. Optional. Appended to the resulting URI.
+ ///
+ /// An optional . Settings on provided object override the settings with matching
+ /// names from RouteOptions.
+ ///
+ /// A absolute URI, or null if a URI cannot be created.
+ public static string GetUriByPage(
+ this LinkGenerator generator,
+ string page,
+ string handler,
+ object values,
+ string scheme,
+ HostString host,
+ PathString pathBase = default,
+ FragmentString fragment = default,
+ LinkOptions options = default)
+ {
+ if (generator == null)
+ {
+ throw new ArgumentNullException(nameof(generator));
+ }
+
+ if (page == null)
+ {
+ throw new ArgumentNullException(nameof(page));
+ }
+
+ var address = CreateAddress(httpContext: null, page, handler, values);
+ return generator.GetUriByAddress(address, address.ExplicitValues, scheme, host, pathBase, fragment, options);
+ }
+
+ ///
+ /// Gets a based on the provided , , and .
+ ///
+ /// The .
+ /// The page name. Used to resolve endpoints.
+ /// The page handler name. Optional.
+ /// The route values. Optional. Used to resolve endpoints and expand parameters in the route template.
+ ///
+ /// A if one or more endpoints matching the address can be found, otherwise null.
+ ///
+ public static LinkGenerationTemplate GetTemplateByPage(
+ this LinkGenerator generator,
+ string page,
+ string handler = default,
+ object values = default)
+ {
+ if (generator == null)
+ {
+ throw new ArgumentNullException(nameof(generator));
+ }
+
+ if (page == null)
+ {
+ throw new ArgumentNullException(nameof(page));
+ }
+
+ var address = CreateAddress(httpContext: null, page, handler, values);
+ return generator.GetTemplateByAddress(address, _templateOptions);
+ }
+
+ private static RouteValuesAddress CreateAddress(HttpContext httpContext, string page, string handler, object values)
+ {
+ var explicitValues = new RouteValueDictionary(values);
+ var ambientValues = GetAmbientValues(httpContext);
+
+ UrlHelperBase.NormalizeRouteValuesForPage(context: null, page, handler, explicitValues, ambientValues);
+
+ return new RouteValuesAddress()
+ {
+ AmbientValues = ambientValues,
+ ExplicitValues = explicitValues
+ };
+ }
+
+ private static RouteValueDictionary GetAmbientValues(HttpContext httpContext)
+ {
+ return httpContext?.Features.Get()?.RouteValues;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelper.cs b/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelper.cs
index 8bab2581d4..33f7d36f3e 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelper.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelper.cs
@@ -44,31 +44,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
var valuesDictionary = GetValuesDictionary(actionContext.Values);
- if (actionContext.Action == null)
- {
- if (!valuesDictionary.ContainsKey("action") &&
- AmbientValues.TryGetValue("action", out var action))
- {
- valuesDictionary["action"] = action;
- }
- }
- else
- {
- valuesDictionary["action"] = actionContext.Action;
- }
-
- if (actionContext.Controller == null)
- {
- if (!valuesDictionary.ContainsKey("controller") &&
- AmbientValues.TryGetValue("controller", out var controller))
- {
- valuesDictionary["controller"] = controller;
- }
- }
- else
- {
- valuesDictionary["controller"] = actionContext.Controller;
- }
+ NormalizeRouteValuesForAction(actionContext.Action, actionContext.Controller, valuesDictionary, AmbientValues);
var virtualPathData = GetVirtualPathData(routeName: null, values: valuesDictionary);
return GenerateUrl(actionContext.Protocol, actionContext.Host, virtualPathData, actionContext.Fragment);
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelperBase.cs b/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelperBase.cs
index a68c9c988b..5c48596532 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelperBase.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelperBase.cs
@@ -3,8 +3,12 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Text;
using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Core;
+using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Routing;
namespace Microsoft.AspNetCore.Mvc.Routing
@@ -263,6 +267,109 @@ namespace Microsoft.AspNetCore.Mvc.Routing
}
}
+ internal static void NormalizeRouteValuesForAction(
+ string action,
+ string controller,
+ RouteValueDictionary values,
+ RouteValueDictionary ambientValues)
+ {
+ object obj = null;
+ if (action == null)
+ {
+ if (!values.ContainsKey("action") &&
+ (ambientValues?.TryGetValue("action", out obj) ?? false))
+ {
+ values["action"] = obj;
+ }
+ }
+ else
+ {
+ values["action"] = action;
+ }
+
+ if (controller == null)
+ {
+ if (!values.ContainsKey("controller") &&
+ (ambientValues?.TryGetValue("controller", out obj) ?? false))
+ {
+ values["controller"] = obj;
+ }
+ }
+ else
+ {
+ values["controller"] = controller;
+ }
+ }
+
+ internal static void NormalizeRouteValuesForPage(
+ ActionContext context,
+ string page,
+ string handler,
+ RouteValueDictionary values,
+ RouteValueDictionary ambientValues)
+ {
+ object value = null;
+ if (string.IsNullOrEmpty(page))
+ {
+ if (!values.ContainsKey("page") &&
+ (ambientValues?.TryGetValue("page", out value) ?? false))
+ {
+ values["page"] = value;
+ }
+ }
+ else
+ {
+ values["page"] = CalculatePageName(context, ambientValues, page);
+ }
+
+ if (string.IsNullOrEmpty(handler))
+ {
+ if (!values.ContainsKey("handler") &&
+ (ambientValues?.ContainsKey("handler") ?? false))
+ {
+ // Clear out form action unless it's explicitly specified in the routeValues.
+ values["handler"] = null;
+ }
+ }
+ else
+ {
+ values["handler"] = handler;
+ }
+ }
+
+ private static object CalculatePageName(ActionContext context, RouteValueDictionary ambientValues, string pageName)
+ {
+ Debug.Assert(pageName.Length > 0);
+ // Paths not qualified with a leading slash are treated as relative to the current page.
+ if (pageName[0] != '/')
+ {
+ // OK now we should get the best 'normalized' version of the page route value that we can.
+ string currentPagePath;
+ if (context != null)
+ {
+ currentPagePath = NormalizedRouteValue.GetNormalizedRouteValue(context, "page");
+ }
+ else if (ambientValues != null)
+ {
+ currentPagePath = ambientValues["page"]?.ToString();
+ }
+ else
+ {
+ currentPagePath = null;
+ }
+
+ if (string.IsNullOrEmpty(currentPagePath))
+ {
+ // Disallow the use sibling page routing, a Razor page specific feature, from a non-page action.
+ throw new InvalidOperationException(Resources.FormatUrlHelper_RelativePagePathIsNotSupported(pageName));
+ }
+
+ return ViewEnginePath.CombinePath(currentPagePath, pageName);
+ }
+
+ return pageName;
+ }
+
// for unit testing
internal static void AppendPathAndFragment(StringBuilder builder, PathString pathBase, string virtualPath, string fragment)
{
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/UrlHelperExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Core/UrlHelperExtensions.cs
index 42da1bc1ac..e790b879ef 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/UrlHelperExtensions.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/UrlHelperExtensions.cs
@@ -444,32 +444,8 @@ namespace Microsoft.AspNetCore.Mvc
var routeValues = new RouteValueDictionary(values);
var ambientValues = urlHelper.ActionContext.RouteData.Values;
- if (string.IsNullOrEmpty(pageName))
- {
- if (!routeValues.ContainsKey("page") &&
- ambientValues.TryGetValue("page", out var value))
- {
- routeValues["page"] = value;
- }
- }
- else
- {
- routeValues["page"] = CalculatePageName(urlHelper.ActionContext, pageName);
- }
- if (string.IsNullOrEmpty(pageHandler))
- {
- if (!routeValues.ContainsKey("handler") &&
- ambientValues.TryGetValue("handler", out var handler))
- {
- // Clear out form action unless it's explicitly specified in the routeValues.
- routeValues["handler"] = null;
- }
- }
- else
- {
- routeValues["handler"] = pageHandler;
- }
+ UrlHelperBase.NormalizeRouteValuesForPage(urlHelper.ActionContext, pageName, pageHandler, routeValues, ambientValues);
return urlHelper.RouteUrl(
routeName: null,
@@ -478,24 +454,5 @@ namespace Microsoft.AspNetCore.Mvc
host: host,
fragment: fragment);
}
-
- private static object CalculatePageName(ActionContext actionContext, string pageName)
- {
- Debug.Assert(pageName.Length > 0);
- // Paths not qualified with a leading slash are treated as relative to the current page.
- if (pageName[0] != '/')
- {
- var currentPagePath = NormalizedRouteValue.GetNormalizedRouteValue(actionContext, "page");
- if (string.IsNullOrEmpty(currentPagePath))
- {
- // Disallow the use sibling page routing, a Razor page specific feature, from a non-page action.
- throw new InvalidOperationException(Resources.FormatUrlHelper_RelativePagePathIsNotSupported(pageName));
- }
-
- return ViewEnginePath.CombinePath(currentPagePath, pageName);
- }
-
- return pageName;
- }
}
}
diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Json/Internal/JsonResultExecutor.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/Internal/JsonResultExecutor.cs
index f7e2fc09f2..332d1bdc59 100644
--- a/src/Microsoft.AspNetCore.Mvc.Formatters.Json/Internal/JsonResultExecutor.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/Internal/JsonResultExecutor.cs
@@ -86,7 +86,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Json.Internal
/// The .
/// The .
/// A which will complete when writing has completed.
- public virtual Task ExecuteAsync(ActionContext context, JsonResult result)
+ public virtual async Task ExecuteAsync(ActionContext context, JsonResult result)
{
if (context == null)
{
@@ -128,9 +128,11 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Json.Internal
var jsonSerializer = JsonSerializer.Create(serializerSettings);
jsonSerializer.Serialize(jsonWriter, result.Value);
}
- }
- return Task.CompletedTask;
+ // Perf: call FlushAsync to call WriteAsync on the stream with any content left in the TextWriter's
+ // buffers. This is better than just letting dispose handle it (which would result in a synchronous write).
+ await writer.FlushAsync();
+ }
}
}
}
diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/ControllerLinkGeneratorExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/ControllerLinkGeneratorExtensionsTest.cs
new file mode 100644
index 0000000000..5d0f01fafb
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/ControllerLinkGeneratorExtensionsTest.cs
@@ -0,0 +1,245 @@
+// 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.Linq;
+using System.Text.Encodings.Web;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.Routing.Patterns;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.ObjectPool;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing
+{
+ public class ControllerLinkGeneratorExtensionsTest
+ {
+ [Fact]
+ public void GetPathByAction_WithHttpContext_PromotesAmbientValues()
+ {
+ // Arrange
+ var endpoint1 = CreateEndpoint(
+ "Home/Index/{id}",
+ defaults: new { controller = "Home", action = "Index", },
+ metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "Index", })) });
+ var endpoint2 = CreateEndpoint(
+ "Home/Index/{id?}",
+ defaults: new { controller = "Home", action = "Index", },
+ metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "Index", })) });
+
+ var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
+
+ var httpContext = CreateHttpContext(new { controller = "Home", });
+ httpContext.Request.PathBase = new PathString("/Foo/Bar?encodeme?");
+
+ // Act
+ var path = linkGenerator.GetPathByAction(
+ httpContext,
+ action: "Index",
+ values: new RouteValueDictionary(new { query = "some?query" }),
+ fragment: new FragmentString("#Fragment?"),
+ options: new LinkOptions() { AppendTrailingSlash = true, });
+
+ // Assert
+ Assert.Equal("/Foo/Bar%3Fencodeme%3F/Home/Index/?query=some%3Fquery#Fragment?", path);
+ }
+
+ [Fact]
+ public void GetPathByAction_WithoutHttpContext_WithPathBaseAndFragment()
+ {
+ // Arrange
+ var endpoint1 = CreateEndpoint(
+ "Home/Index/{id}",
+ defaults: new { controller = "Home", action = "Index", },
+ metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "Index", })) });
+ var endpoint2 = CreateEndpoint(
+ "Home/Index/{id?}",
+ defaults: new { controller = "Home", action = "Index", },
+ metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "Index", })) });
+
+ var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
+
+ // Act
+ var path = linkGenerator.GetPathByAction(
+ action: "Index",
+ controller: "Home",
+ values: new RouteValueDictionary(new { query = "some?query" }),
+ new PathString("/Foo/Bar?encodeme?"),
+ new FragmentString("#Fragment?"),
+ new LinkOptions() { AppendTrailingSlash = true, });
+
+ // Assert
+ Assert.Equal("/Foo/Bar%3Fencodeme%3F/Home/Index/?query=some%3Fquery#Fragment?", path);
+ }
+
+ [Fact]
+ public void GetPathByAction_WithHttpContext_WithPathBaseAndFragment()
+ {
+ // Arrange
+ var endpoint1 = CreateEndpoint(
+ "Home/Index/{id}",
+ defaults: new { controller = "Home", action = "Index", },
+ metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "Index", })) });
+ var endpoint2 = CreateEndpoint(
+ "Home/Index/{id?}",
+ defaults: new { controller = "Home", action = "Index", },
+ metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "Index", })) });
+
+ var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
+
+ var httpContext = CreateHttpContext();
+ httpContext.Request.PathBase = new PathString("/Foo/Bar?encodeme?");
+
+ // Act
+ var path = linkGenerator.GetPathByAction(
+ httpContext,
+ action: "Index",
+ controller: "Home",
+ values: new RouteValueDictionary(new { query = "some?query" }),
+ fragment: new FragmentString("#Fragment?"),
+ options: new LinkOptions() { AppendTrailingSlash = true, });
+
+ // Assert
+ Assert.Equal("/Foo/Bar%3Fencodeme%3F/Home/Index/?query=some%3Fquery#Fragment?", path);
+ }
+
+ [Fact]
+ public void GetUriByAction_WithoutHttpContext_WithPathBaseAndFragment()
+ {
+ // Arrange
+ var endpoint1 = CreateEndpoint(
+ "Home/Index/{id}",
+ defaults: new { controller = "Home", action = "Index", },
+ metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "Index", })) });
+ var endpoint2 = CreateEndpoint(
+ "Home/Index/{id?}",
+ defaults: new { controller = "Home", action = "Index", },
+ metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "Index", })) });
+
+ var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
+
+ // Act
+ var path = linkGenerator.GetUriByAction(
+ action: "Index",
+ controller: "Home",
+ values: new RouteValueDictionary(new { query = "some?query" }),
+ "http",
+ new HostString("example.com"),
+ new PathString("/Foo/Bar?encodeme?"),
+ new FragmentString("#Fragment?"),
+ new LinkOptions() { AppendTrailingSlash = true, });
+
+ // Assert
+ Assert.Equal("http://example.com/Foo/Bar%3Fencodeme%3F/Home/Index/?query=some%3Fquery#Fragment?", path);
+ }
+
+ [Fact]
+ public void GetUriByAction_WithHttpContext_WithPathBaseAndFragment()
+ {
+ // Arrange
+ var endpoint1 = CreateEndpoint(
+ "Home/Index/{id}",
+ defaults: new { controller = "Home", action = "Index", },
+ metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "Index", })) });
+ var endpoint2 = CreateEndpoint(
+ "Home/Index/{id?}",
+ defaults: new { controller = "Home", action = "Index", },
+ metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "Index", })) });
+
+ var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
+
+ var httpContext = CreateHttpContext(new { controller = "Home", action = "Index", });
+ httpContext.Request.Scheme = "http";
+ httpContext.Request.Host = new HostString("example.com");
+ httpContext.Request.PathBase = new PathString("/Foo/Bar?encodeme?");
+
+ // Act
+ var uri = linkGenerator.GetUriByAction(
+ httpContext,
+ values: new RouteValueDictionary(new { query = "some?query" }),
+ fragment: new FragmentString("#Fragment?"),
+ options: new LinkOptions() { AppendTrailingSlash = true, });
+
+ // Assert
+ Assert.Equal("http://example.com/Foo/Bar%3Fencodeme%3F/Home/Index/?query=some%3Fquery#Fragment?", uri);
+ }
+
+ [Fact]
+ public void GetTemplateByAction_CreatesTemplate()
+ {
+ // Arrange
+ var endpoint1 = CreateEndpoint(
+ "Home/Index/{id}",
+ metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "Index", })) });
+ var endpoint2 = CreateEndpoint(
+ "Home/Index/{id?}",
+ metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "Index", })) });
+
+ var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
+
+ // Act
+ var template = linkGenerator.GetTemplateByAction(action: "Index", controller: "Home");
+
+ // Assert
+ Assert.NotNull(template);
+ Assert.Equal("/Home/Index/17", template.GetPath(new { id = 17 }));
+ }
+
+ private RouteEndpoint CreateEndpoint(
+ string template,
+ object defaults = null,
+ object requiredValues = null,
+ int order = 0,
+ object[] metadata = null)
+ {
+ return new RouteEndpoint(
+ (httpContext) => Task.CompletedTask,
+ RoutePatternFactory.Parse(template, defaults, parameterPolicies: null),
+ order,
+ new EndpointMetadataCollection(metadata ?? Array.Empty