Merge pull request #8498 from dotnet-maestro-bot/merge/release/2.2-to-master

[automated] Merge branch 'release/2.2' => 'master'
This commit is contained in:
Ryan Nowak 2018-09-26 12:45:27 -07:00 committed by GitHub
commit f2612f4cc0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 1215 additions and 72 deletions

View File

@ -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
{
/// <summary>
/// Extension methods for using <see cref="LinkGenerator"/> to generate links to MVC controllers.
/// </summary>
public static class ControllerLinkGeneratorExtensions
{
private static readonly LinkGenerationTemplateOptions _templateOptions = new LinkGenerationTemplateOptions()
{
UseAmbientValues = true,
};
/// <summary>
/// Generates a URI with an absolute path based on the provided values.
/// </summary>
/// <param name="generator">The <see cref="LinkGenerator"/>.</param>
/// <param name="httpContext">The <see cref="HttpContext"/> associated with the current request.</param>
/// <param name="action">
/// The action name. Used to resolve endpoints. Optional. If <c>null</c> is provided, the current action route value
/// will be used.
/// </param>
/// <param name="controller">
/// The controller name. Used to resolve endpoints. Optional. If <c>null</c> is provided, the current controller route value
/// will be used.
/// </param>
/// <param name="values">The route values. Optional. Used to resolve endpoints and expand parameters in the route template.</param>
/// <param name="pathBase">
/// An optional URI path base. Prepended to the path in the resulting URI. If not provided, the value of <see cref="HttpRequest.PathBase"/> will be used.
/// </param>
/// <param name="fragment">A URI fragment. Optional. Appended to the resulting URI.</param>
/// <param name="options">
/// An optional <see cref="LinkOptions"/>. Settings on provided object override the settings with matching
/// names from <c>RouteOptions</c>.
/// </param>
/// <returns>A URI with an absolute path, or <c>null</c> if a URI cannot be created.</returns>
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<RouteValuesAddress>(
httpContext,
address,
address.ExplicitValues,
address.AmbientValues,
pathBase,
fragment,
options);
}
/// <summary>
/// Generates a URI with an absolute path based on the provided values.
/// </summary>
/// <param name="generator">The <see cref="LinkGenerator"/>.</param>
/// <param name="action">The action name. Used to resolve endpoints.</param>
/// <param name="controller">The controller name. Used to resolve endpoints.</param>
/// <param name="values">The route values. Optional. Used to resolve endpoints and expand parameters in the route template.</param>
/// <param name="pathBase">An optional URI path base. Prepended to the path in the resulting URI.</param>
/// <param name="fragment">A URI fragment. Optional. Appended to the resulting URI.</param>
/// <param name="options">
/// An optional <see cref="LinkOptions"/>. Settings on provided object override the settings with matching
/// names from <c>RouteOptions</c>.
/// </param>
/// <returns>A URI with an absolute path, or <c>null</c> if a URI cannot be created.</returns>
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<RouteValuesAddress>(address, address.ExplicitValues, pathBase, fragment, options);
}
/// <summary>
/// Generates an absolute URI based on the provided values.
/// </summary>
/// <param name="generator">The <see cref="LinkGenerator"/>.</param>
/// <param name="httpContext">The <see cref="HttpContext"/> associated with the current request.</param>
/// <param name="action">
/// The action name. Used to resolve endpoints. Optional. If <c>null</c> is provided, the current action route value
/// will be used.
/// </param>
/// <param name="controller">
/// The controller name. Used to resolve endpoints. Optional. If <c>null</c> is provided, the current controller route value
/// will be used.
/// </param>
/// <param name="values">The route values. Optional. Used to resolve endpoints and expand parameters in the route template.</param>
/// <param name="scheme">
/// The URI scheme, applied to the resulting URI. Optional. If not provided, the value of <see cref="HttpRequest.Scheme"/> will be used.
/// </param>
/// <param name="host">
/// The URI host/authority, applied to the resulting URI. Optional. If not provided, the value <see cref="HttpRequest.Host"/> will be used.
/// </param>
/// <param name="pathBase">
/// An optional URI path base. Prepended to the path in the resulting URI. If not provided, the value of <see cref="HttpRequest.PathBase"/> will be used.
/// </param>
/// <param name="fragment">A URI fragment. Optional. Appended to the resulting URI.</param>
/// <param name="options">
/// An optional <see cref="LinkOptions"/>. Settings on provided object override the settings with matching
/// names from <c>RouteOptions</c>.
/// </param>
/// <returns>A absolute URI, or <c>null</c> if a URI cannot be created.</returns>
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<RouteValuesAddress>(
httpContext,
address,
address.ExplicitValues,
address.AmbientValues,
scheme,
host,
pathBase,
fragment,
options);
}
/// <summary>
/// Generates an absolute URI based on the provided values.
/// </summary>
/// <param name="generator">The <see cref="LinkGenerator"/>.</param>
/// <param name="action">The action name. Used to resolve endpoints.</param>
/// <param name="controller">The controller name. Used to resolve endpoints.</param>
/// <param name="values">The route values. May be null. Used to resolve endpoints and expand parameters in the route template.</param>
/// <param name="scheme">The URI scheme, applied to the resulting URI.</param>
/// <param name="host">The URI host/authority, applied to the resulting URI.</param>
/// <param name="pathBase">An optional URI path base. Prepended to the path in the resulting URI.</param>
/// <param name="fragment">A URI fragment. Optional. Appended to the resulting URI.</param>
/// <param name="options">
/// An optional <see cref="LinkOptions"/>. Settings on provided object override the settings with matching
/// names from <c>RouteOptions</c>.
/// </param>
/// <returns>A absolute URI, or <c>null</c> if a URI cannot be created.</returns>
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<RouteValuesAddress>(address, address.ExplicitValues, scheme, host, pathBase, fragment, options);
}
/// <summary>
/// Gets a <see cref="LinkGenerationTemplate"/> based on the provided <paramref name="action"/>, <paramref name="controller"/>, and <paramref name="values"/>.
/// </summary>
/// <param name="generator">The <see cref="LinkGenerator"/>.</param>
/// <param name="action">The action name. Used to resolve endpoints.</param>
/// <param name="controller">The controller name. Used to resolve endpoints.</param>
/// <param name="values">The route values. Optional. Used to resolve endpoints and expand parameters in the route template.</param>
/// <returns>
/// A <see cref="LinkGenerationTemplate"/> if one or more endpoints matching the address can be found, otherwise <c>null</c>.
/// </returns>
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<RouteValuesAddress>(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<IRouteValuesFeature>()?.RouteValues;
}
}
}

View File

@ -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
{
/// <summary>
/// Extension methods for using <see cref="LinkGenerator"/> to generate links to Razor Pages.
/// </summary>
public static class PageLinkGeneratorExtensions
{
private static readonly LinkGenerationTemplateOptions _templateOptions = new LinkGenerationTemplateOptions()
{
UseAmbientValues = true,
};
/// <summary>
/// Generates a URI with an absolute path based on the provided values.
/// </summary>
/// <param name="generator">The <see cref="LinkGenerator"/>.</param>
/// <param name="httpContext">The <see cref="HttpContext"/> associated with the current request.</param>
/// <param name="page">
/// The page name. Used to resolve endpoints. Optional. If <c>null</c> is provided, the current page route value
/// will be used.
/// </param>
/// <param name="handler">
/// The page handler name. Used to resolve endpoints. Optional.
/// </param>
/// <param name="values">The route values. Optional. Used to resolve endpoints and expand parameters in the route template.</param>
/// <param name="pathBase">
/// An optional URI path base. Prepended to the path in the resulting URI. If not provided, the value of <see cref="HttpRequest.PathBase"/> will be used.
/// </param>
/// <param name="fragment">A URI fragment. Optional. Appended to the resulting URI.</param>
/// <param name="options">
/// An optional <see cref="LinkOptions"/>. Settings on provided object override the settings with matching
/// names from <c>RouteOptions</c>.
/// </param>
/// <returns>A URI with an absolute path, or <c>null</c> if a URI cannot be created.</returns>
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<RouteValuesAddress>(
httpContext,
address,
address.ExplicitValues,
address.AmbientValues,
pathBase,
fragment,
options);
}
/// <summary>
/// Generates a URI with an absolute path based on the provided values.
/// </summary>
/// <param name="generator">The <see cref="LinkGenerator"/>.</param>
/// <param name="page">
/// The page name. Used to resolve endpoints.
/// </param>
/// <param name="handler">
/// The page handler name. Used to resolve endpoints. Optional.
/// </param>
/// <param name="values">The route values. Optional. Used to resolve endpoints and expand parameters in the route template.</param>
/// <param name="pathBase">An optional URI path base. Prepended to the path in the resulting URI.</param>
/// <param name="fragment">A URI fragment. Optional. Appended to the resulting URI.</param>
/// <param name="options">
/// An optional <see cref="LinkOptions"/>. Settings on provided object override the settings with matching
/// names from <c>RouteOptions</c>.
/// </param>
/// <returns>A URI with an absolute path, or <c>null</c> if a URI cannot be created.</returns>
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<RouteValuesAddress>(address, address.ExplicitValues, pathBase, fragment, options);
}
/// <summary>
/// Generates an absolute URI based on the provided values.
/// </summary>
/// <param name="generator">The <see cref="LinkGenerator"/>.</param>
/// <param name="httpContext">The <see cref="HttpContext"/> associated with the current request.</param>
/// <param name="page">
/// The page name. Used to resolve endpoints. Optional. If <c>null</c> is provided, the current page route value
/// will be used.
/// </param>
/// <param name="handler">
/// The page handler name. Used to resolve endpoints. Optional.
/// </param>
/// <param name="values">The route values. Optional. Used to resolve endpoints and expand parameters in the route template.</param>
/// <param name="scheme">
/// The URI scheme, applied to the resulting URI. Optional. If not provided, the value of <see cref="HttpRequest.Scheme"/> will be used.
/// </param>
/// <param name="host">
/// The URI host/authority, applied to the resulting URI. Optional. If not provided, the value <see cref="HttpRequest.Host"/> will be used.
/// </param>
/// <param name="pathBase">
/// An optional URI path base. Prepended to the path in the resulting URI. If not provided, the value of <see cref="HttpRequest.PathBase"/> will be used.
/// </param>
/// <param name="fragment">A URI fragment. Optional. Appended to the resulting URI.</param>
/// <param name="options">
/// An optional <see cref="LinkOptions"/>. Settings on provided object override the settings with matching
/// names from <c>RouteOptions</c>.
/// </param>
/// <returns>A absolute URI, or <c>null</c> if a URI cannot be created.</returns>
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<RouteValuesAddress>(
httpContext,
address,
address.ExplicitValues,
address.AmbientValues,
scheme,
host,
pathBase,
fragment,
options);
}
/// <summary>
/// Generates an absolute URI based on the provided values.
/// </summary>
/// <param name="generator">The <see cref="LinkGenerator"/>.</param>
/// <param name="page">The page name. Used to resolve endpoints.</param>
/// <param name="handler">The page handler name. May be null.</param>
/// <param name="values">The route values. May be null. Used to resolve endpoints and expand parameters in the route template.</param>
/// <param name="scheme">The URI scheme, applied to the resulting URI.</param>
/// <param name="host">The URI host/authority, applied to the resulting URI.</param>
/// <param name="pathBase">An optional URI path base. Prepended to the path in the resulting URI.</param>
/// <param name="fragment">A URI fragment. Optional. Appended to the resulting URI.</param>
/// <param name="options">
/// An optional <see cref="LinkOptions"/>. Settings on provided object override the settings with matching
/// names from <c>RouteOptions</c>.
/// </param>
/// <returns>A absolute URI, or <c>null</c> if a URI cannot be created.</returns>
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<RouteValuesAddress>(address, address.ExplicitValues, scheme, host, pathBase, fragment, options);
}
/// <summary>
/// Gets a <see cref="LinkGenerationTemplate"/> based on the provided <paramref name="page"/>, <paramref name="handler"/>, and <paramref name="values"/>.
/// </summary>
/// <param name="generator">The <see cref="LinkGenerator"/>.</param>
/// <param name="page">The page name. Used to resolve endpoints.</param>
/// <param name="handler">The page handler name. Optional.</param>
/// <param name="values">The route values. Optional. Used to resolve endpoints and expand parameters in the route template.</param>
/// <returns>
/// A <see cref="LinkGenerationTemplate"/> if one or more endpoints matching the address can be found, otherwise <c>null</c>.
/// </returns>
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<RouteValuesAddress>(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<IRouteValuesFeature>()?.RouteValues;
}
}
}

View File

@ -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);

View File

@ -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)
{

View File

@ -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;
}
}
}

View File

@ -86,7 +86,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Json.Internal
/// <param name="context">The <see cref="ActionContext"/>.</param>
/// <param name="result">The <see cref="JsonResult"/>.</param>
/// <returns>A <see cref="Task"/> which will complete when writing has completed.</returns>
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();
}
}
}
}

View File

@ -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<object>()),
null);
}
private IServiceProvider CreateServices(IEnumerable<Endpoint> endpoints)
{
if (endpoints == null)
{
endpoints = Enumerable.Empty<Endpoint>();
}
var services = new ServiceCollection();
services.AddOptions();
services.AddLogging();
services.AddRouting();
services
.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>()
.AddSingleton<UrlEncoder>(UrlEncoder.Default);
services.TryAddEnumerable(ServiceDescriptor.Singleton<EndpointDataSource>(new DefaultEndpointDataSource(endpoints)));
return services.BuildServiceProvider();
}
private LinkGenerator CreateLinkGenerator(params Endpoint[] endpoints)
{
var services = CreateServices(endpoints);
return services.GetRequiredService<LinkGenerator>();
}
private HttpContext CreateHttpContext(object ambientValues = null)
{
var httpContext = new DefaultHttpContext();
var feature = new EndpointFeature
{
RouteValues = new RouteValueDictionary(ambientValues)
};
httpContext.Features.Set<IEndpointFeature>(feature);
httpContext.Features.Set<IRouteValuesFeature>(feature);
return httpContext;
}
}
}

View File

@ -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 PageLinkGeneratorExtensionsTest
{
[Fact]
public void GetPathByPage_WithHttpContext_PromotesAmbientValues()
{
// Arrange
var endpoint1 = CreateEndpoint(
"About/{id}",
defaults: new { page = "/About", },
metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { page = "/About", })) });
var endpoint2 = CreateEndpoint(
"Admin/ManageUsers/{handler?}",
defaults: new { page = "/Admin/ManageUsers", },
metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { page = "/Admin/ManageUsers", })) });
var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
var httpContext = CreateHttpContext(new { page = "/About", id = 17, });
httpContext.Request.PathBase = new PathString("/Foo/Bar?encodeme?");
// Act
var path = linkGenerator.GetPathByPage(
httpContext,
values: new RouteValueDictionary(new { id = 18, query = "some?query" }),
fragment: new FragmentString("#Fragment?"),
options: new LinkOptions() { AppendTrailingSlash = true, });
// Assert
Assert.Equal("/Foo/Bar%3Fencodeme%3F/About/18/?query=some%3Fquery#Fragment?", path);
}
[Fact]
public void GetPathByPage_WithoutHttpContext_WithPathBaseAndFragment()
{
// Arrange
var endpoint1 = CreateEndpoint(
"About/{id}",
defaults: new { page = "/About", },
metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { page = "/About", })) });
var endpoint2 = CreateEndpoint(
"Admin/ManageUsers/{handler?}",
defaults: new { page = "/Admin/ManageUsers", },
metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { page = "/Admin/ManageUsers", })) });
var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
// Act
var path = linkGenerator.GetPathByPage(
page: "/Admin/ManageUsers",
handler: "Delete",
values: new RouteValueDictionary(new { user = "jamesnk", query = "some?query" }),
new PathString("/Foo/Bar?encodeme?"),
new FragmentString("#Fragment?"),
new LinkOptions() { AppendTrailingSlash = true, });
// Assert
Assert.Equal("/Foo/Bar%3Fencodeme%3F/Admin/ManageUsers/Delete/?user=jamesnk&query=some%3Fquery#Fragment?", path);
}
[Fact]
public void GetPathByPage_WithHttpContext_WithPathBaseAndFragment()
{
// Arrange
var endpoint1 = CreateEndpoint(
"About/{id}",
defaults: new { page = "/About", },
metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { page = "/About", })) });
var endpoint2 = CreateEndpoint(
"Admin/ManageUsers",
defaults: new { page = "/Admin/ManageUsers", },
metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { page = "/Admin/ManageUsers", })) });
var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
var httpContext = CreateHttpContext(new { page = "/Admin/ManageUsers", handler = "DeleteUser", });
httpContext.Request.PathBase = new PathString("/Foo/Bar?encodeme?");
// Act
var path = linkGenerator.GetPathByPage(
httpContext,
page: "/About",
values: new RouteValueDictionary(new { id = 19, query = "some?query" }),
fragment: new FragmentString("#Fragment?"),
options: new LinkOptions() { AppendTrailingSlash = true, });
// Assert
Assert.Equal("/Foo/Bar%3Fencodeme%3F/About/19/?query=some%3Fquery#Fragment?", path);
}
[Fact]
public void GetUriByPage_WithoutHttpContext_WithPathBaseAndFragment()
{
// Arrange
var endpoint1 = CreateEndpoint(
"About/{id}",
defaults: new { page = "/About", },
metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { page = "/About", })) });
var endpoint2 = CreateEndpoint(
"Admin/ManageUsers",
defaults: new { page = "/Admin/ManageUsers", },
metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { page = "/Admin/ManageUsers", })) });
var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
// Act
var path = linkGenerator.GetUriByPage(
page: "/About",
handler: null,
values: new RouteValueDictionary(new { id = 19, 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/About/19/?query=some%3Fquery#Fragment?", path);
}
[Fact]
public void GetUriByPage_WithHttpContext_WithPathBaseAndFragment()
{
// Arrange
var endpoint1 = CreateEndpoint(
"About/{id}",
defaults: new { page = "/About", },
metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { page = "/About", })) });
var endpoint2 = CreateEndpoint(
"Admin/ManageUsers",
defaults: new { page = "/Admin/ManageUsers", },
metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { page = "/Admin/ManageUsers", })) });
var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
var httpContext = CreateHttpContext(new { page = "/Admin/ManageUsers", });
httpContext.Request.Scheme = "http";
httpContext.Request.Host = new HostString("example.com");
httpContext.Request.PathBase = new PathString("/Foo/Bar?encodeme?");
// Act
var uri = linkGenerator.GetUriByPage(
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/Admin/ManageUsers/?query=some%3Fquery#Fragment?", uri);
}
[Fact]
public void GetTemplateByAction_CreatesTemplate()
{
// Arrange
var endpoint1 = CreateEndpoint(
"About/{id}",
defaults: new { page = "/About", },
metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { page = "/About", })) });
var endpoint2 = CreateEndpoint(
"Admin/ManageUsers",
defaults: new { page = "/Admin/ManageUsers", },
metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { page = "/Admin/ManageUsers", })) });
var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
// Act
var template = linkGenerator.GetTemplateByPage(page: "/About");
// Assert
Assert.NotNull(template);
Assert.Equal("/About/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<object>()),
null);
}
private IServiceProvider CreateServices(IEnumerable<Endpoint> endpoints)
{
if (endpoints == null)
{
endpoints = Enumerable.Empty<Endpoint>();
}
var services = new ServiceCollection();
services.AddOptions();
services.AddLogging();
services.AddRouting();
services
.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>()
.AddSingleton<UrlEncoder>(UrlEncoder.Default);
services.TryAddEnumerable(ServiceDescriptor.Singleton<EndpointDataSource>(new DefaultEndpointDataSource(endpoints)));
return services.BuildServiceProvider();
}
private LinkGenerator CreateLinkGenerator(params Endpoint[] endpoints)
{
var services = CreateServices(endpoints);
return services.GetRequiredService<LinkGenerator>();
}
private HttpContext CreateHttpContext(object ambientValues = null)
{
var httpContext = new DefaultHttpContext();
var feature = new EndpointFeature
{
RouteValues = new RouteValueDictionary(ambientValues)
};
httpContext.Features.Set<IEndpointFeature>(feature);
httpContext.Features.Set<IRouteValuesFeature>(feature);
return httpContext;
}
}
}

View File

@ -5,6 +5,7 @@ using System;
using System.Buffers;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
@ -14,6 +15,7 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
using Moq;
using Newtonsoft.Json;
using Xunit;
@ -217,6 +219,66 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Json.Internal
Assert.Equal(expected, logger.MostRecentMessage);
}
[Fact]
public async Task ExecuteAsync_WritesToTheResponseStream_WhenContentIsLargerThanBuffer()
{
// Arrange
var writeLength = 2 * TestHttpResponseStreamWriterFactory.DefaultBufferSize + 4;
var text = new string('a', writeLength);
var expectedWriteCallCount = Math.Ceiling((double)writeLength / TestHttpResponseStreamWriterFactory.DefaultBufferSize);
var stream = new Mock<Stream>();
stream.SetupGet(s => s.CanWrite).Returns(true);
var httpContext = new DefaultHttpContext();
httpContext.Response.Body = stream.Object;
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
var result = new JsonResult(text);
var executor = CreateExecutor();
// Act
await executor.ExecuteAsync(actionContext, result);
// Assert
// HttpResponseStreamWriter buffers content up to the buffer size (16k). When writes exceed the buffer size, it'll perform a synchronous
// write to the response stream.
stream.Verify(s => s.Write(It.IsAny<byte[]>(), It.IsAny<int>(), TestHttpResponseStreamWriterFactory.DefaultBufferSize), Times.Exactly(2));
// Remainder buffered content is written asynchronously as part of the FlushAsync.
stream.Verify(s => s.WriteAsync(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Once());
// Dispose does not call Flush
stream.Verify(s => s.Flush(), Times.Never());
}
[Theory]
[InlineData(5)]
[InlineData(TestHttpResponseStreamWriterFactory.DefaultBufferSize - 30)]
public async Task ExecuteAsync_DoesNotWriteSynchronouslyToTheResponseBody_WhenContentIsSmallerThanBufferSize(int writeLength)
{
// Arrange
var text = new string('a', writeLength);
var stream = new Mock<Stream>();
stream.SetupGet(s => s.CanWrite).Returns(true);
var httpContext = new DefaultHttpContext();
httpContext.Response.Body = stream.Object;
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
var result = new JsonResult(text);
var executor = CreateExecutor();
// Act
await executor.ExecuteAsync(actionContext, result);
// Assert
// HttpResponseStreamWriter buffers content up to the buffer size (16k) and will asynchronously write content to the response as part
// of the FlushAsync call if the content written to it is smaller than the buffer size.
// This test verifies that no synchronous writes are performed in this scenario.
stream.Verify(s => s.Flush(), Times.Never());
stream.Verify(s => s.Write(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>()), Times.Never());
}
private static JsonResultExecutor CreateExecutor(ILogger<JsonResultExecutor> logger = null)
{
return new JsonResultExecutor(