Revisions to LinkGenerator (#770)

* Revisions to LinkGenerator

* PR feedback
This commit is contained in:
Ryan Nowak 2018-09-05 20:52:09 -07:00 committed by GitHub
parent 950d2e9b74
commit e3b704095b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 2907 additions and 2307 deletions

View File

@ -57,9 +57,10 @@ namespace Microsoft.AspNetCore.Routing.LinkGeneration
[Benchmark] [Benchmark]
public void EndpointRouting() public void EndpointRouting()
{ {
var actualUrl = _linkGenerator.GetLink( var actualUrl = _linkGenerator.GetPathByRouteValues(
_requestContext.HttpContext, _requestContext.HttpContext,
values: new RouteValueDictionary(_lookUpValues)); routeName: null,
values: _lookUpValues);
AssertUrl("/repos/aspnet/routing/issues/comments/20202", actualUrl); AssertUrl("/repos/aspnet/routing/issues/comments/20202", actualUrl);
} }

View File

@ -56,17 +56,17 @@ namespace Microsoft.AspNetCore.Routing.LinkGeneration
[Benchmark] [Benchmark]
public void EndpointRouting() public void EndpointRouting()
{ {
var actualUrl = _linkGenerator.GetLink( var actualUrl = _linkGenerator.GetPathByRouteValues(
_requestContext.HttpContext, _requestContext.HttpContext,
values: new RouteValueDictionary( routeName: null,
new values: new
{ {
controller = "Customers", controller = "Customers",
action = "Details", action = "Details",
category = "Administration", category = "Administration",
region = "US", region = "US",
id = 10 id = 10
})); });
AssertUrl("/Customers/Details/Administration/US/10", actualUrl); AssertUrl("/Customers/Details/Administration/US/10", actualUrl);
} }

View File

@ -53,14 +53,14 @@ namespace Microsoft.AspNetCore.Routing.LinkGeneration
[Benchmark] [Benchmark]
public void EndpointRouting() public void EndpointRouting()
{ {
var actualUrl = _linkGenerator.GetLink( var actualUrl = _linkGenerator.GetPathByRouteValues(
_requestContext.HttpContext, _requestContext.HttpContext,
values: new RouteValueDictionary( routeName: null,
new values: new
{ {
controller = "Products", controller = "Products",
action = "Details", action = "Details",
})); });
AssertUrl("/Products/Details", actualUrl); AssertUrl("/Products/Details", actualUrl);
} }

View File

@ -56,17 +56,17 @@ namespace Microsoft.AspNetCore.Routing.LinkGeneration
[Benchmark] [Benchmark]
public void EndpointRouting() public void EndpointRouting()
{ {
var actualUrl = _linkGenerator.GetLink( var actualUrl = _linkGenerator.GetPathByRouteValues(
_requestContext.HttpContext, _requestContext.HttpContext,
values: new RouteValueDictionary( routeName: null,
new values: new
{ {
controller = "Customers", controller = "Customers",
action = "Details", action = "Details",
category = "Administration", category = "Administration",
region = "US", region = "US",
id = 10 id = 10
})); });
AssertUrl("/Customers/Details/Administration/US/10", actualUrl); AssertUrl("/Customers/Details/Administration/US/10", actualUrl);
} }

View File

@ -103,13 +103,13 @@ namespace RoutingSample.Web
response.StatusCode = 200; response.StatusCode = 200;
response.ContentType = "text/plain"; response.ContentType = "text/plain";
return response.WriteAsync( return response.WriteAsync(
"Link: " + linkGenerator.GetLink(httpContext, "WithSingleAsteriskCatchAll", new { })); "Link: " + linkGenerator.GetPathByRouteValues(httpContext, "WithSingleAsteriskCatchAll", new { }));
}, },
RoutePatternFactory.Parse("/WithSingleAsteriskCatchAll/{*path}"), RoutePatternFactory.Parse("/WithSingleAsteriskCatchAll/{*path}"),
0, 0,
new EndpointMetadataCollection( new EndpointMetadataCollection(
new RouteValuesAddressMetadata( new RouteValuesAddressMetadata(
name: "WithSingleAsteriskCatchAll", routeName: "WithSingleAsteriskCatchAll",
requiredValues: new RouteValueDictionary())), requiredValues: new RouteValueDictionary())),
"WithSingleAsteriskCatchAll"), "WithSingleAsteriskCatchAll"),
new RouteEndpoint((httpContext) => new RouteEndpoint((httpContext) =>
@ -120,13 +120,13 @@ namespace RoutingSample.Web
response.StatusCode = 200; response.StatusCode = 200;
response.ContentType = "text/plain"; response.ContentType = "text/plain";
return response.WriteAsync( return response.WriteAsync(
"Link: " + linkGenerator.GetLink(httpContext, "WithDoubleAsteriskCatchAll", new { })); "Link: " + linkGenerator.GetPathByRouteValues(httpContext, "WithDoubleAsteriskCatchAll", new { }));
}, },
RoutePatternFactory.Parse("/WithDoubleAsteriskCatchAll/{**path}"), RoutePatternFactory.Parse("/WithDoubleAsteriskCatchAll/{**path}"),
0, 0,
new EndpointMetadataCollection( new EndpointMetadataCollection(
new RouteValuesAddressMetadata( new RouteValuesAddressMetadata(
name: "WithDoubleAsteriskCatchAll", routeName: "WithDoubleAsteriskCatchAll",
requiredValues: new RouteValueDictionary())), requiredValues: new RouteValueDictionary())),
"WithDoubleAsteriskCatchAll"), "WithDoubleAsteriskCatchAll"),
}); });

View File

@ -1,29 +1,90 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing namespace Microsoft.AspNetCore.Routing
{ {
/// <summary> /// <summary>
/// Defines a contract to generate a URL from a template. /// Defines a contract to generate a URL from a template.
/// </summary> /// </summary>
/// <remarks>
/// A <see cref="LinkGenerationTemplate"/> can be created from <see cref="LinkGenerator.GetTemplateByAddress{TAddress}(TAddress)"/>
/// by supplying an address value which has matching endpoints. The returned <see cref="LinkGenerationTemplate"/>
/// will be bound to the endpoints matching the address that was originally provided.
/// </remarks>
public abstract class LinkGenerationTemplate public abstract class LinkGenerationTemplate
{ {
/// <summary> /// <summary>
/// Generates a URL with an absolute path from the specified route values. /// Generates a URI with an absolute path based on the provided values.
/// </summary> /// </summary>
/// <param name="values">An object that contains route values.</param> /// <param name="httpContext">The <see cref="HttpContext"/> associated with the current request.</param>
/// <returns>The generated URL.</returns> /// <param name="values">The route values. Used to expand parameters in the route template. Optional.</param>
public string MakeUrl(object values) /// <param name="fragment">An optional URI fragment. Appended to the resulting URI.</param>
{ /// <param name="options">
return MakeUrl(values, options: null); /// 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>.</returns>
public abstract string GetPath(
HttpContext httpContext,
object values,
FragmentString fragment = default,
LinkOptions options = default);
/// <summary> /// <summary>
/// Generates a URL with an absolute path from the specified route values and link options. /// Generates a URI with an absolute path based on the provided values.
/// </summary> /// </summary>
/// <param name="values">An object that contains route values.</param> /// <param name="values">The route values. Used to expand parameters in the route template. Optional.</param>
/// <param name="options">The <see cref="LinkOptions"/>.</param> /// <param name="pathBase">An optional URI path base. Prepended to the path in the resulting URI.</param>
/// <returns>The generated URL.</returns> /// <param name="fragment">An optional URI fragment. Appended to the resulting URI.</param>
public abstract string MakeUrl(object values, LinkOptions options); /// <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>.</returns>
public abstract string GetPath(
object values,
PathString pathBase = default,
FragmentString fragment = default,
LinkOptions options = default);
/// <summary>
/// Generates an absolute URI based on the provided values.
/// </summary>
/// <param name="httpContext">The <see cref="HttpContext"/> associated with the current request.</param>
/// <param name="values">The route values. Used to expand parameters in the route template. Optional.</param>
/// <param name="fragment">An optional URI fragment. 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>.</returns>
public abstract string GetUri(
HttpContext httpContext,
object values,
FragmentString fragment = default,
LinkOptions options = default);
/// <summary>
/// Generates an absolute URI based on the provided values.
/// </summary>
/// <param name="values">The route values. Used to expand parameters in the route template. Optional.</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">An optional URI fragment. 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>An absolute URI, or <c>null</c>.</returns>
public abstract string GetUri(
object values,
string scheme,
HostString host,
PathString pathBase = default,
FragmentString fragment = default,
LinkOptions options = default);
} }
} }

View File

@ -1,463 +1,121 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing namespace Microsoft.AspNetCore.Routing
{ {
/// <summary> /// <summary>
/// Defines a contract to generate URLs to endpoints. /// Defines a contract to generate absolute and related URIs based on endpoint routing.
/// </summary> /// </summary>
/// <remarks>
/// <para>
/// Generating URIs in endpoint routing occurs in two phases. First, an address is bound to a list of
/// endpoints that match the address. Secondly, each endpoint's <c>RoutePattern</c> is evaluated, until
/// a route pattern that matches the supplied values is found. The resulting output is combined with
/// the other URI parts supplied to the link generator and returned.
/// </para>
/// <para>
/// The methods provided by the <see cref="LinkGenerator"/> type are general infrastructure, and support
/// the standard link generator functionality for any type of address. The most convenient way to use
/// <see cref="LinkGenerator"/> is through extension methods that perform operations for a specific
/// address type.
/// </para>
/// </remarks>
public abstract class LinkGenerator public abstract class LinkGenerator
{ {
/// <summary> /// <summary>
/// Generates a URL with an absolute path from the specified route values. /// Generates a URI with an absolute path based on the provided values.
/// </summary> /// </summary>
/// <param name="values">An object that contains route values.</param> /// <typeparam name="TAddress">The address type.</typeparam>
/// <returns>The generated URL.</returns> /// <param name="httpContext">The <see cref="HttpContext"/> associated with the current request.</param>
public string GetLink(object values) /// <param name="address">The address value. Used to resolve endpoints.</param>
{ /// <param name="values">The route values. Used to expand parameters in the route template. Optional.</param>
return GetLink(httpContext: null, routeName: null, values, options: null); /// <param name="fragment">An optional URI fragment. Appended to the resulting URI.</param>
} /// <param name="options">
/// An optional <see cref="LinkOptions"/>. Settings on provided object override the settings with matching
/// <summary> /// names from <c>RouteOptions</c>.
/// Generates a URL with an absolute path from the specified route values and link options. /// </param>
/// </summary> /// <returns>A URI with an absolute path, or <c>null</c>.</returns>
/// <param name="values">An object that contains route values.</param> public abstract string GetPathByAddress<TAddress>(
/// <param name="options">The <see cref="LinkOptions"/>.</param>
/// <returns>The generated URL.</returns>
public string GetLink(object values, LinkOptions options)
{
return GetLink(httpContext: null, routeName: null, values, options);
}
/// <summary>
/// Generates a URL with an absolute path from the specified route values.
/// A return value indicates whether the operation succeeded.
/// </summary>
/// <param name="values">An object that contains route values.</param>
/// <param name="link">The generated URL.</param>
/// <returns><c>true</c> if a URL was generated successfully; otherwise, <c>false</c>.</returns>
public bool TryGetLink(object values, out string link)
{
return TryGetLink(httpContext: null, routeName: null, values, options: null, out link);
}
/// <summary>
/// Generates a URL with an absolute path from the specified route values and link options.
/// A return value indicates whether the operation succeeded.
/// </summary>
/// <param name="values">An object that contains route values.</param>
/// <param name="options">The <see cref="LinkOptions"/>.</param>
/// <param name="link">The generated URL.</param>
/// <returns><c>true</c> if a URL was generated successfully; otherwise, <c>false</c>.</returns>
public bool TryGetLink(object values, LinkOptions options, out string link)
{
return TryGetLink(httpContext: null, routeName: null, values, options, out link);
}
/// <summary>
/// Generates a URL with an absolute path from the specified route values.
/// </summary>
/// <param name="httpContext">The <see cref="HttpContext"/> associated with current request.</param>
/// <param name="values">An object that contains route values.</param>
/// <returns>The generated URL.</returns>
public string GetLink(HttpContext httpContext, object values)
{
return GetLink(httpContext, routeName: null, values, options: null);
}
/// <summary>
/// Generates a URL with an absolute path from the specified route values.
/// A return value indicates whether the operation succeeded.
/// </summary>
/// <param name="httpContext">The <see cref="HttpContext"/> associated with current request.</param>
/// <param name="values">An object that contains route values.</param>
/// <param name="link">The generated URL.</param>
/// <returns><c>true</c> if a URL was generated successfully; otherwise, <c>false</c>.</returns>
public bool TryGetLink(HttpContext httpContext, object values, out string link)
{
return TryGetLink(httpContext, routeName: null, values, options: null, out link);
}
/// <summary>
/// Generates a URL with an absolute path from the specified route values and link options.
/// </summary>
/// <param name="httpContext">The <see cref="HttpContext"/> associated with current request.</param>
/// <param name="values">An object that contains route values.</param>
/// <param name="options">The <see cref="LinkOptions"/>.</param>
/// <returns>The generated URL.</returns>
public string GetLink(HttpContext httpContext, object values, LinkOptions options)
{
return GetLink(httpContext, routeName: null, values, options);
}
/// <summary>
/// Generates a URL with an absolute path from the specified route values and link options.
/// A return value indicates whether the operation succeeded.
/// </summary>
/// <param name="httpContext">The <see cref="HttpContext"/> associated with current request.</param>
/// <param name="values">An object that contains route values.</param>
/// <param name="options">The <see cref="LinkOptions"/>.</param>
/// <param name="link">The generated URL.</param>
/// <returns><c>true</c> if a URL was generated successfully; otherwise, <c>false</c>.</returns>
public bool TryGetLink(HttpContext httpContext, object values, LinkOptions options, out string link)
{
return TryGetLink(httpContext, routeName: null, values, options, out link);
}
/// <summary>
/// Generates a URL with an absolute path from the specified route name and route values.
/// </summary>
/// <param name="routeName">The name of the route to generate the URL to.</param>
/// <param name="values">An object that contains route values.</param>
/// <returns>The generated URL.</returns>
public string GetLink(string routeName, object values)
{
return GetLink(httpContext: null, routeName, values, options: null);
}
/// <summary>
/// Generates a URL with an absolute path from the specified route name and route values.
/// A return value indicates whether the operation succeeded.
/// </summary>
/// <param name="routeName">The name of the route to generate the URL to.</param>
/// <param name="values">An object that contains route values.</param>
/// <param name="link">The generated URL.</param>
/// <returns><c>true</c> if a URL was generated successfully; otherwise, <c>false</c>.</returns>
public bool TryGetLink(string routeName, object values, out string link)
{
return TryGetLink(httpContext: null, routeName, values, options: null, out link);
}
/// <summary>
/// Generates a URL with an absolute path from the specified route name and route values.
/// </summary>
/// <param name="routeName">The name of the route to generate the URL to.</param>
/// <param name="values">An object that contains route values.</param>
/// <param name="options">The <see cref="LinkOptions"/>.</param>
/// <returns>The generated URL.</returns>
public string GetLink(string routeName, object values, LinkOptions options)
{
return GetLink(httpContext: null, routeName, values, options);
}
/// <summary>
/// Generates a URL with an absolute path from the specified route name, route values and link options.
/// A return value indicates whether the operation succeeded.
/// </summary>
/// <param name="routeName">The name of the route to generate the URL to.</param>
/// <param name="values">An object that contains route values.</param>
/// <param name="options">The <see cref="LinkOptions"/>.</param>
/// <param name="link">The generated URL.</param>
/// <returns><c>true</c> if a URL was generated successfully; otherwise, <c>false</c>.</returns>
public bool TryGetLink(string routeName, object values, LinkOptions options, out string link)
{
return TryGetLink(httpContext: null, routeName, values, options, out link);
}
/// <summary>
/// Generates a URL with an absolute path from the specified route name and route values.
/// </summary>
/// <param name="routeName">The name of the route to generate the URL to.</param>
/// <param name="httpContext">The <see cref="HttpContext"/> associated with current request.</param>
/// <param name="values">An object that contains route values.</param>
/// <returns>The generated URL.</returns>
public string GetLink(HttpContext httpContext, string routeName, object values)
{
return GetLink(httpContext, routeName, values, options: null);
}
/// <summary>
/// Generates a URL with an absolute path from the specified route name and route values.
/// A return value indicates whether the operation succeeded.
/// </summary>
/// <param name="httpContext">The <see cref="HttpContext"/> associated with current request.</param>
/// <param name="routeName">The name of the route to generate the URL to.</param>
/// <param name="values">An object that contains route values.</param>
/// <param name="link">The generated URL.</param>
/// <returns><c>true</c> if a URL was generated successfully; otherwise, <c>false</c>.</returns>
public bool TryGetLink(HttpContext httpContext, string routeName, object values, out string link)
{
return TryGetLink(httpContext, routeName, values, options: null, out link);
}
/// <summary>
/// Generates a URL with an absolute path from the specified route name, route values and link options.
/// </summary>
/// <param name="routeName">The name of the route to generate the URL to.</param>
/// <param name="httpContext">The <see cref="HttpContext"/> associated with current request.</param>
/// <param name="values">An object that contains route values.</param>
/// <param name="options">The <see cref="LinkOptions"/>.</param>
/// <returns>The generated URL.</returns>
public string GetLink(HttpContext httpContext, string routeName, object values, LinkOptions options)
{
if (TryGetLink(httpContext, routeName, values, options, out var link))
{
return link;
}
throw new InvalidOperationException("Could not find a matching endpoint to generate a link.");
}
/// <summary>
/// Generates a URL with an absolute path from the specified route name, route values and link options.
/// A return value indicates whether the operation succeeded.
/// </summary>
/// <param name="httpContext">The <see cref="HttpContext"/> associated with current request.</param>
/// <param name="routeName">The name of the route to generate the URL to.</param>
/// <param name="values">An object that contains route values.</param>
/// <param name="options">The <see cref="LinkOptions"/>.</param>
/// <param name="link">The generated URL.</param>
/// <returns><c>true</c> if a URL was generated successfully; otherwise, <c>false</c>.</returns>
public abstract bool TryGetLink(
HttpContext httpContext,
string routeName,
object values,
LinkOptions options,
out string link);
/// <summary>
/// Generates a URL with an absolute path from the specified lookup information and route values.
/// This lookup information is used to find endpoints using a registered 'IEndpointFinder&lt;TAddress&gt;'.
/// </summary>
/// <typeparam name="TAddress">The address type to look up endpoints.</typeparam>
/// <param name="address">The information used to look up endpoints for generating a URL.</param>
/// <param name="values">An object that contains route values.</param>
/// <returns>The generated URL.</returns>
public string GetLinkByAddress<TAddress>(TAddress address, object values)
{
return GetLinkByAddress(httpContext: null, address, values, options: null);
}
/// <summary>
/// Generates a URL with an absolute path from the specified lookup information and route values.
/// This lookup information is used to find endpoints using a registered 'IEndpointFinder&lt;TAddress&gt;'.
/// A return value indicates whether the operation succeeded.
/// </summary>
/// <typeparam name="TAddress">The address type to look up endpoints.</typeparam>
/// <param name="address">The information used to look up endpoints for generating a URL.</param>
/// <param name="values">An object that contains route values.</param>
/// <param name="link">The generated URL.</param>
/// <returns><c>true</c> if a URL was generated successfully; otherwise, <c>false</c>.</returns>
public bool TryGetLinkByAddress<TAddress>(TAddress address, object values, out string link)
{
return TryGetLinkByAddress(address, values, options: null, out link);
}
/// <summary>
/// Generates a URL with an absolute path from the specified lookup information, route values and link options.
/// This lookup information is used to find endpoints using a registered 'IEndpointFinder&lt;TAddress&gt;'.
/// </summary>
/// <typeparam name="TAddress">The address type to look up endpoints.</typeparam>
/// <param name="address">The information used to look up endpoints for generating a URL.</param>
/// <param name="values">An object that contains route values.</param>
/// <param name="options">The <see cref="LinkOptions"/>.</param>
/// <returns>The generated URL.</returns>
public string GetLinkByAddress<TAddress>(TAddress address, object values, LinkOptions options)
{
return GetLinkByAddress(httpContext: null, address, values, options);
}
/// <summary>
/// Generates a URL with an absolute path from the specified lookup information, route values and link options.
/// This lookup information is used to find endpoints using a registered 'IEndpointFinder&lt;TAddress&gt;'.
/// A return value indicates whether the operation succeeded.
/// </summary>
/// <typeparam name="TAddress">The address type to look up endpoints.</typeparam>
/// <param name="address">The information used to look up endpoints for generating a URL.</param>
/// <param name="values">An object that contains route values.</param>
/// <param name="options">The <see cref="LinkOptions"/>.</param>
/// <param name="link">The generated URL.</param>
/// <returns><c>true</c> if a URL was generated successfully; otherwise, <c>false</c>.</returns>
public bool TryGetLinkByAddress<TAddress>(
TAddress address,
object values,
LinkOptions options,
out string link)
{
return TryGetLinkByAddress(httpContext: null, address, values, options, out link);
}
/// <summary>
/// Generates a URL with an absolute path from the specified lookup information, route values and link options.
/// This lookup information is used to find endpoints using a registered 'IEndpointFinder&lt;TAddress&gt;'.
/// </summary>
/// <typeparam name="TAddress">The address type to look up endpoints.</typeparam>
/// <param name="address">The information used to look up endpoints for generating a URL.</param>
/// <param name="httpContext">The <see cref="HttpContext"/> associated with current request.</param>
/// <param name="values">An object that contains route values.</param>
/// <returns>The generated URL.</returns>
public string GetLinkByAddress<TAddress>(HttpContext httpContext, TAddress address, object values)
{
return GetLinkByAddress(httpContext, address, values, options: null);
}
/// <summary>
/// Generates a URL with an absolute path from the specified lookup information and route values.
/// This lookup information is used to find endpoints using a registered 'IEndpointFinder&lt;TAddress&gt;'.
/// A return value indicates whether the operation succeeded.
/// </summary>
/// <typeparam name="TAddress">The address type to look up endpoints.</typeparam>
/// <param name="address">The information used to look up endpoints for generating a URL.</param>
/// <param name="httpContext">The <see cref="HttpContext"/> associated with current request.</param>
/// <param name="values">An object that contains route values.</param>
/// <param name="link">The generated URL.</param>
/// <returns><c>true</c> if a URL was generated successfully; otherwise, <c>false</c>.</returns>
public bool TryGetLinkByAddress<TAddress>(
HttpContext httpContext, HttpContext httpContext,
TAddress address, TAddress address,
object values, RouteValueDictionary values,
out string link) FragmentString fragment = default,
{ LinkOptions options = default);
return TryGetLinkByAddress(httpContext, address, values, options: null, out link);
}
/// <summary> /// <summary>
/// Generates a URL with an absolute path from the specified lookup information, route values and link options. /// Generates a URI with an absolute path based on the provided values.
/// This lookup information is used to find endpoints using a registered 'IEndpointFinder&lt;TAddress&gt;'.
/// </summary> /// </summary>
/// <typeparam name="TAddress">The address type to look up endpoints.</typeparam> /// <typeparam name="TAddress">The address type.</typeparam>
/// <param name="address">The information used to look up endpoints for generating a URL.</param> /// <param name="address">The address value. Used to resolve endpoints.</param>
/// <param name="httpContext">The <see cref="HttpContext"/> associated with current request.</param> /// <param name="values">The route values. Used to expand parameters in the route template. Optional.</param>
/// <param name="values">An object that contains route values.</param> /// <param name="pathBase">An optional URI path base. Prepended to the path in the resulting URI.</param>
/// <param name="options">The <see cref="LinkOptions"/>.</param> /// <param name="fragment">An optional URI fragment. Appended to the resulting URI.</param>
/// <returns>The generated URL.</returns> /// <param name="options">
public string GetLinkByAddress<TAddress>( /// 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>.</returns>
public abstract string GetPathByAddress<TAddress>(
TAddress address,
RouteValueDictionary values,
PathString pathBase = default,
FragmentString fragment = default,
LinkOptions options = default);
/// <summary>
/// Generates an absolute URI based on the provided values.
/// </summary>
/// <typeparam name="TAddress">The address type.</typeparam>
/// <param name="httpContext">The <see cref="HttpContext"/> associated with the current request.</param>
/// <param name="address">The address value. Used to resolve endpoints.</param>
/// <param name="values">The route values. Used to expand parameters in the route template. Optional.</param>
/// <param name="fragment">An optional URI fragment. 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>.</returns>
public abstract string GetUriByAddress<TAddress>(
HttpContext httpContext, HttpContext httpContext,
TAddress address, TAddress address,
object values, RouteValueDictionary values,
LinkOptions options) FragmentString fragment = default,
{ LinkOptions options = default);
if (TryGetLinkByAddress(httpContext, address, values, options, out var link))
{
return link;
}
throw new InvalidOperationException("Could not find a matching endpoint to generate a link.");
}
/// <summary> /// <summary>
/// Generates a URL with an absolute path from the specified lookup information, route values and link options. /// Generates an absolute URI based on the provided values.
/// This lookup information is used to find endpoints using a registered 'IEndpointFinder&lt;TAddress&gt;'.
/// A return value indicates whether the operation succeeded.
/// </summary> /// </summary>
/// <typeparam name="TAddress">The address type to look up endpoints.</typeparam> /// <typeparam name="TAddress">The address type.</typeparam>
/// <param name="address">The information used to look up endpoints for generating a URL.</param> /// <param name="address">The address value. Used to resolve endpoints.</param>
/// <param name="httpContext">The <see cref="HttpContext"/> associated with current request.</param> /// <param name="values">The route values. Used to expand parameters in the route template. Optional.</param>
/// <param name="values">An object that contains route values.</param> /// <param name="scheme">The URI scheme, applied to the resulting URI.</param>
/// <param name="options">The <see cref="LinkOptions"/>.</param> /// <param name="host">The URI host/authority, applied to the resulting URI.</param>
/// <param name="link">The generated URL.</param> /// <param name="pathBase">An optional URI path base. Prepended to the path in the resulting URI.</param>
/// <returns><c>true</c> if a URL was generated successfully; otherwise, <c>false</c>.</returns> /// <param name="fragment">An optional URI fragment. Appended to the resulting URI.</param>
public abstract bool TryGetLinkByAddress<TAddress>( /// <param name="options">
HttpContext httpContext, /// An optional <see cref="LinkOptions"/>. Settings on provided object override the settings with matching
/// names from <c>RouteOptions</c>.
/// </param>
/// <returns>An absolute URI, or <c>null</c>.</returns>
public abstract string GetUriByAddress<TAddress>(
TAddress address, TAddress address,
object values, RouteValueDictionary values,
LinkOptions options, string scheme,
out string link); HostString host,
PathString pathBase = default,
FragmentString fragment = default,
LinkOptions options = default);
/// <summary> /// <summary>
/// Gets a <see cref="LinkGenerationTemplate"/> to generate a URL from the specified route values. /// Gets a <see cref="LinkGenerationTemplate"/> based on the provided <paramref name="address"/>.
/// This template object holds information of the endpoint(s) that were found and which can later be used to
/// generate a URL using the <see cref="LinkGenerationTemplate.MakeUrl(object, LinkOptions)"/> api.
/// </summary> /// </summary>
/// <param name="values"> /// <typeparam name="TAddress">The address type.</typeparam>
/// An object that contains route values. These values are used to lookup endpoint(s). /// <param name="address">The address value. Used to resolve endpoints.</param>
/// </param>
/// <returns> /// <returns>
/// If an endpoint(s) was found successfully, then this returns a template object representing that, /// A <see cref="LinkGenerationTemplate"/> if one or more endpoints matching the address can be found, otherwise <c>null</c>.
/// <c>null</c> otherwise.
/// </returns> /// </returns>
public LinkGenerationTemplate GetTemplate(object values) public abstract LinkGenerationTemplate GetTemplateByAddress<TAddress>(TAddress address);
{
return GetTemplate(httpContext: null, routeName: null, values);
}
/// <summary>
/// Gets a <see cref="LinkGenerationTemplate"/> to generate a URL from the specified route name and route values.
/// This template object holds information of the endpoint(s) that were found and which can later be used to
/// generate a URL using the <see cref="LinkGenerationTemplate.MakeUrl(object, LinkOptions)"/> api.
/// </summary>
/// <param name="routeName">The name of the route to generate the URL to.</param>
/// <param name="values">
/// An object that contains route values. These values are used to lookup for endpoint(s).
/// </param>
/// <returns>
/// If an endpoint(s) was found successfully, then this returns a template object representing that,
/// <c>null</c> otherwise.
/// </returns>
public LinkGenerationTemplate GetTemplate(string routeName, object values)
{
return GetTemplate(httpContext: null, routeName, values);
}
/// <summary>
/// Gets a <see cref="LinkGenerationTemplate"/> to generate a URL from the specified route values.
/// This template object holds information of the endpoint(s) that were found and which can later be used to
/// generate a URL using the <see cref="LinkGenerationTemplate.MakeUrl(object, LinkOptions)"/> api.
/// </summary>
/// <param name="httpContext">The <see cref="HttpContext"/> associated with current request.</param>
/// <param name="values">
/// An object that contains route values. These values are used to lookup for endpoint(s).
/// </param>
/// <returns>
/// If an endpoint(s) was found successfully, then this returns a template object representing that,
/// <c>null</c> otherwise.
/// </returns>
public LinkGenerationTemplate GetTemplate(HttpContext httpContext, object values)
{
return GetTemplate(httpContext, routeName: null, values);
}
/// <summary>
/// Gets a <see cref="LinkGenerationTemplate"/> to generate a URL from the specified route name and route values.
/// This template object holds information of the endpoint(s) that were found and which can later be used to
/// generate a URL using the <see cref="LinkGenerationTemplate.MakeUrl(object, LinkOptions)"/> api.
/// </summary>
/// <param name="httpContext">The <see cref="HttpContext"/> associated with current request.</param>
/// <param name="routeName">The name of the route to generate the URL to.</param>
/// <param name="values">
/// An object that contains route values. These values are used to lookup for endpoint(s).
/// </param>
/// <returns>
/// If an endpoint(s) was found successfully, then this returns a template object representing that,
/// <c>null</c> otherwise.
/// </returns>
public abstract LinkGenerationTemplate GetTemplate(HttpContext httpContext, string routeName, object values);
/// <summary>
/// Gets a <see cref="LinkGenerationTemplate"/> to generate a URL from the specified lookup information.
/// This template object holds information of the endpoint(s) that were found and which can later be used to
/// generate a URL using the <see cref="LinkGenerationTemplate.MakeUrl(object, LinkOptions)"/> api.
/// The lookup information is used to find endpoints using a registered 'IEndpointFinder&lt;TAddress&gt;'.
/// </summary>
/// <typeparam name="TAddress">The address type to look up endpoints.</typeparam>
/// <param name="address">The information used to look up endpoints for creating a template.</param>
/// <returns>
/// If an endpoint(s) was found successfully, then this returns a template object representing that,
/// <c>null</c> otherwise.
/// </returns>
public LinkGenerationTemplate GetTemplateByAddress<TAddress>(TAddress address)
{
return GetTemplateByAddress(httpContext: null, address);
}
/// <summary>
/// Gets a <see cref="LinkGenerationTemplate"/> to generate a URL from the specified lookup information.
/// This template object holds information of the endpoint(s) that were found and which can later be used to
/// generate a URL using the <see cref="LinkGenerationTemplate.MakeUrl(object, LinkOptions)"/> api.
/// The lookup information is used to find endpoints using a registered 'IEndpointFinder&lt;TAddress&gt;'.
/// </summary>
/// <typeparam name="TAddress">The address type to look up endpoints.</typeparam>
/// <param name="address">The information used to look up endpoints for creating a template.</param>
/// <param name="httpContext">The <see cref="HttpContext"/> associated with current request.</param>
/// <returns>
/// If an endpoint(s) was found successfully, then this returns a template object representing that,
/// <c>null</c> otherwise.
/// </returns>
public abstract LinkGenerationTemplate GetTemplateByAddress<TAddress>(
HttpContext httpContext,
TAddress address);
} }
} }

View File

@ -142,7 +142,7 @@ namespace Microsoft.AspNetCore.Routing
sb.Append(" }"); sb.Append(" }");
var routeValuesAddressMetadata = routeEndpoint.Metadata.GetMetadata<IRouteValuesAddressMetadata>(); var routeValuesAddressMetadata = routeEndpoint.Metadata.GetMetadata<IRouteValuesAddressMetadata>();
sb.Append(", Route Name: "); sb.Append(", Route Name: ");
sb.Append(routeValuesAddressMetadata?.Name); sb.Append(routeValuesAddressMetadata?.RouteName);
if (routeValuesAddressMetadata?.RequiredValues != null) if (routeValuesAddressMetadata?.RequiredValues != null)
{ {
sb.Append(", Required Values: new { "); sb.Append(", Required Values: new { ");

View File

@ -1,62 +1,98 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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.Collections.Generic;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Matching;
namespace Microsoft.AspNetCore.Routing namespace Microsoft.AspNetCore.Routing
{ {
internal class DefaultLinkGenerationTemplate : LinkGenerationTemplate internal sealed class DefaultLinkGenerationTemplate : LinkGenerationTemplate
{ {
public DefaultLinkGenerationTemplate( public DefaultLinkGenerationTemplate(DefaultLinkGenerator linkGenerator, List<RouteEndpoint> endpoints)
DefaultLinkGenerator linkGenerator,
IEnumerable<RouteEndpoint> endpoints,
HttpContext httpContext,
RouteValueDictionary explicitValues,
RouteValueDictionary ambientValues)
{ {
LinkGenerator = linkGenerator; LinkGenerator = linkGenerator;
Endpoints = endpoints; Endpoints = endpoints;
HttpContext = httpContext;
EarlierExplicitValues = explicitValues;
AmbientValues = ambientValues;
} }
internal DefaultLinkGenerator LinkGenerator { get; } public DefaultLinkGenerator LinkGenerator { get; }
internal IEnumerable<RouteEndpoint> Endpoints { get; } public List<RouteEndpoint> Endpoints { get; }
internal HttpContext HttpContext { get; } public override string GetPath(
HttpContext httpContext,
internal RouteValueDictionary EarlierExplicitValues { get; } object values,
FragmentString fragment = default,
internal RouteValueDictionary AmbientValues { get; } LinkOptions options = null)
public override string MakeUrl(object values, LinkOptions options)
{ {
var currentValues = new RouteValueDictionary(values); if (httpContext == null)
var mergedValuesDictionary = new RouteValueDictionary(EarlierExplicitValues);
foreach (var kvp in currentValues)
{ {
mergedValuesDictionary[kvp.Key] = kvp.Value; throw new ArgumentNullException(nameof(httpContext));
} }
foreach (var endpoint in Endpoints) return LinkGenerator.GetPathByEndpoints(
Endpoints,
DefaultLinkGenerator.GetAmbientValues(httpContext),
new RouteValueDictionary(values),
httpContext.Request.PathBase,
fragment,
options);
}
public override string GetPath(
object values,
PathString pathBase = default,
FragmentString fragment = default,
LinkOptions options = null)
{
return LinkGenerator.GetPathByEndpoints(
Endpoints,
ambientValues: null,
new RouteValueDictionary(values),
pathBase,
fragment,
options);
}
public override string GetUri(
HttpContext httpContext,
object values,
FragmentString fragment = default,
LinkOptions options = null)
{
if (httpContext == null)
{ {
var link = LinkGenerator.MakeLink( throw new ArgumentNullException(nameof(httpContext));
HttpContext,
endpoint,
AmbientValues,
mergedValuesDictionary,
options);
if (link != null)
{
return link;
}
} }
return null;
return LinkGenerator.GetUriByEndpoints(
Endpoints,
DefaultLinkGenerator.GetAmbientValues(httpContext),
new RouteValueDictionary(values),
httpContext.Request.Scheme,
httpContext.Request.Host,
httpContext.Request.PathBase,
fragment,
options);
}
public override string GetUri(
object values,
string scheme,
HostString host,
PathString pathBase = default,
FragmentString fragment = default,
LinkOptions options = null)
{
return LinkGenerator.GetUriByEndpoints(
Endpoints,
ambientValues: null,
new RouteValueDictionary(values),
scheme,
host,
pathBase,
fragment,
options);
} }
} }
} }

View File

@ -6,9 +6,9 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Routing.Internal; using Microsoft.AspNetCore.Routing.Internal;
using Microsoft.AspNetCore.Routing.Matching;
using Microsoft.AspNetCore.Routing.Template; using Microsoft.AspNetCore.Routing.Template;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -17,14 +17,17 @@ using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Routing namespace Microsoft.AspNetCore.Routing
{ {
internal class DefaultLinkGenerator : LinkGenerator internal sealed class DefaultLinkGenerator : LinkGenerator
{ {
private readonly static char[] UrlQueryDelimiters = new char[] { '?', '#' }; private static readonly char[] UrlQueryDelimiters = new char[] { '?', '#' };
private readonly ParameterPolicyFactory _parameterPolicyFactory; private readonly ParameterPolicyFactory _parameterPolicyFactory;
private readonly ObjectPool<UriBuildingContext> _uriBuildingContextPool; private readonly ObjectPool<UriBuildingContext> _uriBuildingContextPool;
private readonly ILogger<DefaultLinkGenerator> _logger; private readonly ILogger<DefaultLinkGenerator> _logger;
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
private readonly RouteOptions _options;
// A LinkOptions object initialized with the values from RouteOptions
// Used when the user didn't specify something more global.
private readonly LinkOptions _globalLinkOptions;
public DefaultLinkGenerator( public DefaultLinkGenerator(
ParameterPolicyFactory parameterPolicyFactory, ParameterPolicyFactory parameterPolicyFactory,
@ -35,73 +38,218 @@ namespace Microsoft.AspNetCore.Routing
{ {
_parameterPolicyFactory = parameterPolicyFactory; _parameterPolicyFactory = parameterPolicyFactory;
_uriBuildingContextPool = uriBuildingContextPool; _uriBuildingContextPool = uriBuildingContextPool;
_options = routeOptions.Value;
_logger = logger; _logger = logger;
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
_globalLinkOptions = new LinkOptions()
{
AppendTrailingSlash = routeOptions.Value.AppendTrailingSlash,
LowercaseQueryStrings = routeOptions.Value.LowercaseQueryStrings,
LowercaseUrls = routeOptions.Value.LowercaseUrls,
};
} }
public override bool TryGetLink( public override string GetPathByAddress<TAddress>(
HttpContext httpContext,
string routeName,
object values,
LinkOptions options,
out string link)
{
return TryGetLinkByRouteValues(
httpContext,
routeName,
values,
options,
out link);
}
public override bool TryGetLinkByAddress<TAddress>(
HttpContext httpContext, HttpContext httpContext,
TAddress address, TAddress address,
object values, RouteValueDictionary values,
LinkOptions options, FragmentString fragment = default,
out string link) LinkOptions options = null)
{ {
return TryGetLinkByAddressInternal( if (httpContext == null)
httpContext, {
address, throw new ArgumentNullException(nameof(httpContext));
explicitValues: values, }
ambientValues: GetAmbientValues(httpContext),
options, var endpoints = GetEndpoints(address);
out link); if (endpoints.Count == 0)
{
return null;
}
return GetPathByEndpoints(
endpoints,
GetAmbientValues(httpContext),
values,
httpContext.Request.PathBase,
fragment,
options);
} }
public override LinkGenerationTemplate GetTemplate(HttpContext httpContext, string routeName, object values) public override string GetPathByAddress<TAddress>(
TAddress address,
RouteValueDictionary values,
PathString pathBase = default,
FragmentString fragment = default,
LinkOptions options = null)
{ {
var ambientValues = GetAmbientValues(httpContext); var endpoints = GetEndpoints(address);
var explicitValues = new RouteValueDictionary(values); if (endpoints.Count == 0)
{
return null;
}
return GetTemplateInternal( return GetPathByEndpoints(
httpContext, endpoints,
new RouteValuesAddress ambientValues: null,
{ values,
RouteName = routeName, pathBase,
ExplicitValues = explicitValues, fragment,
AmbientValues = ambientValues options);
},
ambientValues,
explicitValues,
values);
} }
public override LinkGenerationTemplate GetTemplateByAddress<TAddress>( public override string GetUriByAddress<TAddress>(
HttpContext httpContext, HttpContext httpContext,
TAddress address) TAddress address,
RouteValueDictionary values,
FragmentString fragment = default,
LinkOptions options = null)
{ {
return GetTemplateInternal(httpContext, address, values: null); if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
var endpoints = GetEndpoints(address);
if (endpoints.Count == 0)
{
return null;
}
return GetUriByEndpoints(
endpoints,
GetAmbientValues(httpContext),
values,
httpContext.Request.Scheme,
httpContext.Request.Host,
httpContext.Request.PathBase,
fragment,
options);
} }
internal string MakeLink( public override string GetUriByAddress<TAddress>(
TAddress address,
RouteValueDictionary values,
string scheme,
HostString host,
PathString pathBase = default,
FragmentString fragment = default,
LinkOptions options = null)
{
if (!host.HasValue)
{
throw new ArgumentNullException(nameof(host));
}
var endpoints = GetEndpoints(address);
if (endpoints.Count == 0)
{
return null;
}
return GetUriByEndpoints(
endpoints,
ambientValues: null,
values,
scheme,
host,
pathBase,
fragment,
options);
}
public override LinkGenerationTemplate GetTemplateByAddress<TAddress>(TAddress address)
{
var endpoints = GetEndpoints(address);
if (endpoints.Count == 0)
{
return null;
}
return new DefaultLinkGenerationTemplate(this, endpoints);
}
private List<RouteEndpoint> GetEndpoints<TAddress>(TAddress address)
{
var addressingScheme = _serviceProvider.GetRequiredService<IEndpointFinder<TAddress>>();
return addressingScheme.FindEndpoints(address).OfType<RouteEndpoint>().ToList();
}
// Also called from DefaultLinkGenerationTemplate
public string GetPathByEndpoints(
List<RouteEndpoint> endpoints,
RouteValueDictionary ambientValues,
RouteValueDictionary values,
PathString pathBase,
FragmentString fragment,
LinkOptions options)
{
for (var i = 0; i < endpoints.Count; i++)
{
var endpoint = endpoints[i];
if (TryProcessTemplate(
httpContext: null,
endpoint,
ambientValues: ambientValues,
values,
options,
out var result))
{
return UriHelper.BuildRelative(
pathBase,
result.path,
result.query,
fragment);
}
}
return null;
}
// Also called from DefaultLinkGenerationTemplate
public string GetUriByEndpoints(
List<RouteEndpoint> endpoints,
RouteValueDictionary ambientValues,
RouteValueDictionary values,
string scheme,
HostString host,
PathString pathBase,
FragmentString fragment,
LinkOptions options)
{
for (var i = 0; i < endpoints.Count; i++)
{
var endpoint = endpoints[i];
if (TryProcessTemplate(
httpContext: null,
endpoint,
ambientValues: ambientValues,
values,
options,
out var result))
{
return UriHelper.BuildAbsolute(
scheme,
host,
pathBase,
result.path,
result.query,
fragment);
}
}
return null;
}
// Internal for testing
internal bool TryProcessTemplate(
HttpContext httpContext, HttpContext httpContext,
RouteEndpoint endpoint, RouteEndpoint endpoint,
RouteValueDictionary ambientValues, RouteValueDictionary ambientValues,
RouteValueDictionary explicitValues, RouteValueDictionary explicitValues,
LinkOptions options) LinkOptions options,
out (PathString path, QueryString query) result)
{ {
var templateBinder = new TemplateBinder( var templateBinder = new TemplateBinder(
UrlEncoder.Default, UrlEncoder.Default,
@ -117,118 +265,22 @@ namespace Microsoft.AspNetCore.Routing
if (templateValuesResult == null) if (templateValuesResult == null)
{ {
// We're missing one of the required values for this route. // We're missing one of the required values for this route.
return null; result = default;
return false;
} }
if (!MatchesConstraints(httpContext, endpoint, templateValuesResult.CombinedValues)) if (!MatchesConstraints(httpContext, endpoint, templateValuesResult.CombinedValues))
{ {
return null; result = default;
return false;
} }
var url = templateBinder.BindValues(templateValuesResult.AcceptedValues); if (!templateBinder.TryBindValues(templateValuesResult.AcceptedValues, options, _globalLinkOptions, out result))
return Normalize(url, options);
}
private bool TryGetLinkByRouteValues(
HttpContext httpContext,
string routeName,
object values,
LinkOptions options,
out string link)
{
var ambientValues = GetAmbientValues(httpContext);
var address = new RouteValuesAddress
{
RouteName = routeName,
ExplicitValues = new RouteValueDictionary(values),
AmbientValues = ambientValues
};
return TryGetLinkByAddressInternal(
httpContext,
address,
explicitValues: values,
ambientValues: ambientValues,
options,
out link);
}
private bool TryGetLinkByAddressInternal<TAddress>(
HttpContext httpContext,
TAddress address,
object explicitValues,
RouteValueDictionary ambientValues,
LinkOptions options,
out string link)
{
link = null;
var endpoints = FindEndpoints(address);
if (endpoints == null)
{ {
return false; return false;
} }
foreach (var endpoint in endpoints) return true;
{
link = MakeLink(
httpContext,
endpoint,
ambientValues,
new RouteValueDictionary(explicitValues),
options);
if (link != null)
{
return true;
}
}
return false;
}
private LinkGenerationTemplate GetTemplateInternal<TAddress>(
HttpContext httpContext,
TAddress address,
object values)
{
var endpoints = FindEndpoints(address);
if (endpoints == null)
{
return null;
}
var ambientValues = GetAmbientValues(httpContext);
var explicitValues = new RouteValueDictionary(values);
return new DefaultLinkGenerationTemplate(
this,
endpoints,
httpContext,
explicitValues,
ambientValues);
}
private LinkGenerationTemplate GetTemplateInternal<TAddress>(
HttpContext httpContext,
TAddress address,
RouteValueDictionary ambientValues,
RouteValueDictionary explicitValues,
object values)
{
var endpoints = FindEndpoints(address);
if (endpoints == null)
{
return null;
}
return new DefaultLinkGenerationTemplate(
this,
endpoints,
httpContext,
explicitValues,
ambientValues);
} }
private bool MatchesConstraints( private bool MatchesConstraints(
@ -260,75 +312,10 @@ namespace Microsoft.AspNetCore.Routing
return true; return true;
} }
private string Normalize(string url, LinkOptions options) // Also called from DefaultLinkGenerationTemplate
public static RouteValueDictionary GetAmbientValues(HttpContext httpContext)
{ {
var lowercaseUrls = options?.LowercaseUrls ?? _options.LowercaseUrls; return httpContext?.Features.Get<IRouteValuesFeature>()?.RouteValues;
var lowercaseQueryStrings = options?.LowercaseQueryStrings ?? _options.LowercaseQueryStrings;
var appendTrailingSlash = options?.AppendTrailingSlash ?? _options.AppendTrailingSlash;
if (!string.IsNullOrEmpty(url) && (lowercaseUrls || appendTrailingSlash))
{
var indexOfSeparator = url.IndexOfAny(UrlQueryDelimiters);
var urlWithoutQueryString = url;
var queryString = string.Empty;
if (indexOfSeparator != -1)
{
urlWithoutQueryString = url.Substring(0, indexOfSeparator);
queryString = url.Substring(indexOfSeparator);
}
if (lowercaseUrls)
{
urlWithoutQueryString = urlWithoutQueryString.ToLowerInvariant();
}
if (lowercaseUrls && lowercaseQueryStrings)
{
queryString = queryString.ToLowerInvariant();
}
if (appendTrailingSlash && !urlWithoutQueryString.EndsWith("/", StringComparison.Ordinal))
{
urlWithoutQueryString += "/";
}
// queryString will contain the delimiter ? or # as the first character, so it's safe to append.
url = urlWithoutQueryString + queryString;
}
return url;
}
private RouteValueDictionary GetAmbientValues(HttpContext httpContext)
{
if (httpContext != null)
{
var feature = httpContext.Features.Get<IRouteValuesFeature>();
if (feature != null)
{
return feature.RouteValues;
}
}
return new RouteValueDictionary();
}
private IEnumerable<RouteEndpoint> FindEndpoints<TAddress>(TAddress address)
{
var finder = _serviceProvider.GetRequiredService<IEndpointFinder<TAddress>>();
var endpoints = finder.FindEndpoints(address);
if (endpoints == null)
{
return null;
}
var routeEndpoints = endpoints.OfType<RouteEndpoint>();
if (!routeEndpoints.Any())
{
return null;
}
return routeEndpoints;
} }
} }
} }

View File

@ -7,7 +7,7 @@ namespace Microsoft.AspNetCore.Routing
{ {
public interface IRouteValuesAddressMetadata public interface IRouteValuesAddressMetadata
{ {
string Name { get; } string RouteName { get; }
IReadOnlyDictionary<string, object> RequiredValues { get; } IReadOnlyDictionary<string, object> RequiredValues { get; }
} }
} }

View File

@ -6,16 +6,18 @@ using System.Diagnostics;
using System.IO; using System.IO;
using System.Text; using System.Text;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing.Internal namespace Microsoft.AspNetCore.Routing.Internal
{ {
[DebuggerDisplay("{DebuggerToString(),nq}")] [DebuggerDisplay("{DebuggerToString(),nq}")]
public class UriBuildingContext public class UriBuildingContext
{ {
// Holds the 'accepted' parts of the uri. // Holds the 'accepted' parts of the path.
private readonly StringBuilder _uri; private readonly StringBuilder _path;
private StringBuilder _query;
// Holds the 'optional' parts of the uri. We need a secondary buffer to handle cases where an optional // Holds the 'optional' parts of the path. We need a secondary buffer to handle cases where an optional
// segment is in the middle of the uri. We don't know if we need to write it out - if it's // segment is in the middle of the uri. We don't know if we need to write it out - if it's
// followed by other optional segments than we will just throw it away. // followed by other optional segments than we will just throw it away.
private readonly List<BufferValue> _buffer; private readonly List<BufferValue> _buffer;
@ -27,20 +29,30 @@ namespace Microsoft.AspNetCore.Routing.Internal
public UriBuildingContext(UrlEncoder urlEncoder) public UriBuildingContext(UrlEncoder urlEncoder)
{ {
_urlEncoder = urlEncoder; _urlEncoder = urlEncoder;
_uri = new StringBuilder(); _path = new StringBuilder();
_query = new StringBuilder();
_buffer = new List<BufferValue>(); _buffer = new List<BufferValue>();
Writer = new StringWriter(_uri); PathWriter = new StringWriter(_path);
QueryWriter = new StringWriter(_query);
_lastValueOffset = -1; _lastValueOffset = -1;
BufferState = SegmentState.Beginning; BufferState = SegmentState.Beginning;
UriState = SegmentState.Beginning; UriState = SegmentState.Beginning;
} }
public bool LowercaseUrls { get; set; }
public bool LowercaseQueryStrings { get; set; }
public bool AppendTrailingSlash { get; set; }
public SegmentState BufferState { get; private set; } public SegmentState BufferState { get; private set; }
public SegmentState UriState { get; private set; } public SegmentState UriState { get; private set; }
public TextWriter Writer { get; } public TextWriter PathWriter { get; }
public TextWriter QueryWriter { get; }
public bool Accept(string value) public bool Accept(string value)
{ {
@ -68,6 +80,12 @@ namespace Microsoft.AspNetCore.Routing.Internal
return false; return false;
} }
// NOTE: this needs to be above all 'EncodeValue' and _path.Append calls
if (LowercaseUrls)
{
value = value.ToLowerInvariant();
}
for (var i = 0; i < _buffer.Count; i++) for (var i = 0; i < _buffer.Count; i++)
{ {
if (_buffer[i].RequiresEncoding) if (_buffer[i].RequiresEncoding)
@ -76,29 +94,29 @@ namespace Microsoft.AspNetCore.Routing.Internal
} }
else else
{ {
_uri.Append(_buffer[i].Value); _path.Append(_buffer[i].Value);
} }
} }
_buffer.Clear(); _buffer.Clear();
if (UriState == SegmentState.Beginning && BufferState == SegmentState.Beginning) if (UriState == SegmentState.Beginning && BufferState == SegmentState.Beginning)
{ {
if (_uri.Length != 0) if (_path.Length != 0)
{ {
_uri.Append("/"); _path.Append("/");
} }
} }
BufferState = SegmentState.Inside; BufferState = SegmentState.Inside;
UriState = SegmentState.Inside; UriState = SegmentState.Inside;
_lastValueOffset = _uri.Length; _lastValueOffset = _path.Length;
// Allow the first segment to have a leading slash. // Allow the first segment to have a leading slash.
// This prevents the leading slash from PathString segments from being encoded. // This prevents the leading slash from PathString segments from being encoded.
if (_uri.Length == 0 && value.Length > 0 && value[0] == '/') if (_path.Length == 0 && value.Length > 0 && value[0] == '/')
{ {
_uri.Append("/"); _path.Append("/");
EncodeValue(value, 1, value.Length - 1, encodeSlashes); EncodeValue(value, 1, value.Length - 1, encodeSlashes);
} }
else else
@ -112,7 +130,7 @@ namespace Microsoft.AspNetCore.Routing.Internal
public void Remove(string literal) public void Remove(string literal)
{ {
Debug.Assert(_lastValueOffset != -1, "Cannot invoke Remove more than once."); Debug.Assert(_lastValueOffset != -1, "Cannot invoke Remove more than once.");
_uri.Length = _lastValueOffset; _path.Length = _lastValueOffset;
_lastValueOffset = -1; _lastValueOffset = -1;
} }
@ -152,7 +170,7 @@ namespace Microsoft.AspNetCore.Routing.Internal
if (UriState == SegmentState.Beginning && BufferState == SegmentState.Beginning) if (UriState == SegmentState.Beginning && BufferState == SegmentState.Beginning)
{ {
if (_uri.Length != 0 || _buffer.Count != 0) if (_path.Length != 0 || _buffer.Count != 0)
{ {
_buffer.Add(new BufferValue("/", requiresEncoding: false)); _buffer.Add(new BufferValue("/", requiresEncoding: false));
} }
@ -172,11 +190,17 @@ namespace Microsoft.AspNetCore.Routing.Internal
public void Clear() public void Clear()
{ {
_uri.Clear(); _path.Clear();
if (_uri.Capacity > 128) if (_path.Capacity > 128)
{ {
// We don't want to retain too much memory if this is getting pooled. // We don't want to retain too much memory if this is getting pooled.
_uri.Capacity = 128; _path.Capacity = 128;
}
_query.Clear();
if (_query.Capacity > 128)
{
_query.Capacity = 128;
} }
_buffer.Clear(); _buffer.Clear();
@ -189,18 +213,52 @@ namespace Microsoft.AspNetCore.Routing.Internal
_lastValueOffset = -1; _lastValueOffset = -1;
BufferState = SegmentState.Beginning; BufferState = SegmentState.Beginning;
UriState = SegmentState.Beginning; UriState = SegmentState.Beginning;
AppendTrailingSlash = false;
LowercaseQueryStrings = false;
LowercaseUrls = false;
} }
// Used by TemplateBinder.BindValues - the legacy code path of IRouter
public override string ToString() public override string ToString()
{ {
// We can ignore any currently buffered segments - they are are guaranteed to be 'defaults'. // We can ignore any currently buffered segments - they are are guaranteed to be 'defaults'.
if (_uri.Length > 0 && _uri[0] != '/') if (_path.Length > 0 && _path[0] != '/')
{ {
// Normalize generated paths so that they always contain a leading slash. // Normalize generated paths so that they always contain a leading slash.
_uri.Insert(0, '/'); _path.Insert(0, '/');
} }
return _uri.ToString(); return _path.ToString() + _query.ToString();
}
// Used by TemplateBinder.TryBindValues - the new code path of LinkGenerator
public PathString ToPathString()
{
if (_path.Length > 0 && _path[0] != '/')
{
// Normalize generated paths so that they always contain a leading slash.
_path.Insert(0, '/');
}
if (AppendTrailingSlash && _path.Length > 0 && _path[_path.Length - 1] != '/')
{
_path.Append('/');
}
return new PathString(_path.ToString());
}
// Used by TemplateBinder.TryBindValues - the new code path of LinkGenerator
public QueryString ToQueryString()
{
if (_query.Length > 0 && _query[0] != '?')
{
// Normalize generated query so that they always contain a leading ?.
_query.Insert(0, '?');
}
return new QueryString(_query.ToString());
} }
private void EncodeValue(string value) private void EncodeValue(string value)
@ -219,7 +277,7 @@ namespace Microsoft.AspNetCore.Routing.Internal
// Just encode everything if its ok to encode slashes // Just encode everything if its ok to encode slashes
if (encodeSlashes) if (encodeSlashes)
{ {
_urlEncoder.Encode(Writer, value, start, characterCount); _urlEncoder.Encode(PathWriter, value, start, characterCount);
} }
else else
{ {
@ -227,8 +285,8 @@ namespace Microsoft.AspNetCore.Routing.Internal
int length = start + characterCount; int length = start + characterCount;
while ((end = value.IndexOf('/', start, characterCount)) >= 0) while ((end = value.IndexOf('/', start, characterCount)) >= 0)
{ {
_urlEncoder.Encode(Writer, value, start, end - start); _urlEncoder.Encode(PathWriter, value, start, end - start);
_uri.Append("/"); _path.Append("/");
start = end + 1; start = end + 1;
characterCount = length - start; characterCount = length - start;
@ -236,14 +294,14 @@ namespace Microsoft.AspNetCore.Routing.Internal
if (end < 0 && characterCount >= 0) if (end < 0 && characterCount >= 0)
{ {
_urlEncoder.Encode(Writer, value, start, length - start); _urlEncoder.Encode(PathWriter, value, start, length - start);
} }
} }
} }
private string DebuggerToString() private string DebuggerToString()
{ {
return string.Format("{{Accepted: '{0}' Buffered: '{1}'}}", _uri, string.Join("", _buffer)); return string.Format("{{Accepted: '{0}' Buffered: '{1}'}}", _path, string.Join("", _buffer));
} }
} }
} }

View File

@ -0,0 +1,95 @@
// 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 System;
namespace Microsoft.AspNetCore.Routing
{
/// <summary>
/// Extension methods for using <see cref="LinkGenerator"/> with <see cref="RouteValuesAddress"/>.
/// </summary>
public static class LinkGeneratorRouteValuesAddressExtensions
{
public static string GetPathByRouteValues(
this LinkGenerator generator,
HttpContext httpContext,
string routeName,
object values,
FragmentString fragment = default,
LinkOptions options = default)
{
if (generator == null)
{
throw new ArgumentNullException(nameof(generator));
}
var address = CreateAddress(httpContext, routeName, values);
return generator.GetPathByAddress<RouteValuesAddress>(httpContext, address, address.ExplicitValues, fragment, options);
}
public static string GetPathByRouteValues(
this LinkGenerator generator,
string routeName,
object values,
PathString pathBase = default,
FragmentString fragment = default,
LinkOptions options = default)
{
if (generator == null)
{
throw new ArgumentNullException(nameof(generator));
}
var address = CreateAddress(httpContext: null, routeName, values);
return generator.GetPathByAddress<RouteValuesAddress>(address, address.ExplicitValues, pathBase, fragment, options);
}
public static string GetUriByRouteValues(
this LinkGenerator generator,
HttpContext httpContext,
string routeName,
object values,
FragmentString fragment = default,
LinkOptions options = default)
{
if (generator == null)
{
throw new ArgumentNullException(nameof(generator));
}
var address = CreateAddress(httpContext: null, routeName, values);
return generator.GetUriByAddress<RouteValuesAddress>(httpContext, address, address.ExplicitValues, fragment, options);
}
public static string GetUriByRouteValues(
this LinkGenerator generator,
string routeName,
object values,
string scheme,
HostString host,
PathString pathBase = default,
FragmentString fragment = default,
LinkOptions options = default)
{
if (generator == null)
{
throw new ArgumentNullException(nameof(generator));
}
var address = CreateAddress(httpContext: null, routeName, values);
return generator.GetUriByAddress<RouteValuesAddress>(address, address.ExplicitValues, scheme, host, pathBase, fragment, options);
}
private static RouteValuesAddress CreateAddress(HttpContext httpContext, string routeName, object values)
{
return new RouteValuesAddress()
{
AmbientValues = DefaultLinkGenerator.GetAmbientValues(httpContext),
ExplicitValues = new RouteValueDictionary(values),
RouteName = routeName,
};
}
}
}

View File

@ -11,19 +11,19 @@ namespace Microsoft.AspNetCore.Routing
[DebuggerDisplay("{DebuggerToString(),nq}")] [DebuggerDisplay("{DebuggerToString(),nq}")]
public sealed class RouteValuesAddressMetadata : IRouteValuesAddressMetadata public sealed class RouteValuesAddressMetadata : IRouteValuesAddressMetadata
{ {
public RouteValuesAddressMetadata(string name, IReadOnlyDictionary<string, object> requiredValues) public RouteValuesAddressMetadata(string routeName, IReadOnlyDictionary<string, object> requiredValues)
{ {
Name = name; RouteName = routeName;
RequiredValues = requiredValues; RequiredValues = requiredValues;
} }
public string Name { get; } public string RouteName { get; }
public IReadOnlyDictionary<string, object> RequiredValues { get; } public IReadOnlyDictionary<string, object> RequiredValues { get; }
internal string DebuggerToString() internal string DebuggerToString()
{ {
return $"Name: {Name} - Required values: {string.Join(", ", FormatValues(RequiredValues))}"; return $"Name: {RouteName} - Required values: {string.Join(", ", FormatValues(RequiredValues))}";
IEnumerable<string> FormatValues(IEnumerable<KeyValuePair<string, object>> values) IEnumerable<string> FormatValues(IEnumerable<KeyValuePair<string, object>> values)
{ {

View File

@ -147,7 +147,7 @@ namespace Microsoft.AspNetCore.Routing
RequiredLinkValues = new RouteValueDictionary(routeValuesAddressMetadata?.RequiredValues), RequiredLinkValues = new RouteValueDictionary(routeValuesAddressMetadata?.RequiredValues),
RouteTemplate = new RouteTemplate(endpoint.RoutePattern), RouteTemplate = new RouteTemplate(endpoint.RoutePattern),
Data = endpoint, Data = endpoint,
RouteName = routeValuesAddressMetadata?.Name, RouteName = routeValuesAddressMetadata?.RouteName,
}; };
entry.Defaults = new RouteValueDictionary(endpoint.RoutePattern.Defaults); entry.Defaults = new RouteValueDictionary(endpoint.RoutePattern.Defaults);
return entry; return entry;

View File

@ -160,12 +160,48 @@ namespace Microsoft.AspNetCore.Routing.Template
public string BindValues(RouteValueDictionary acceptedValues) public string BindValues(RouteValueDictionary acceptedValues)
{ {
var context = _pool.Get(); var context = _pool.Get();
var result = BindValues(context, acceptedValues);
_pool.Return(context); try
return result; {
return TryBindValuesCore(context, acceptedValues) ? context.ToString() : null;
}
finally
{
_pool.Return(context);
}
} }
private string BindValues(UriBuildingContext context, RouteValueDictionary acceptedValues) // Step 2: If the route is a match generate the appropriate URI
internal bool TryBindValues(
RouteValueDictionary acceptedValues,
LinkOptions options,
LinkOptions globalOptions,
out (PathString path, QueryString query) result)
{
var context = _pool.Get();
context.AppendTrailingSlash = options?.AppendTrailingSlash ?? globalOptions.AppendTrailingSlash ?? false;
context.LowercaseQueryStrings = options?.LowercaseQueryStrings ?? globalOptions.LowercaseQueryStrings ?? false;
context.LowercaseUrls = options?.LowercaseUrls ?? globalOptions.LowercaseUrls ?? false;
try
{
if (TryBindValuesCore(context, acceptedValues))
{
result = (context.ToPathString(), context.ToQueryString());
return true;
}
result = default;
return false;
}
finally
{
_pool.Return(context);
}
}
private bool TryBindValuesCore(UriBuildingContext context, RouteValueDictionary acceptedValues)
{ {
for (var i = 0; i < _pattern.PathSegments.Count; i++) for (var i = 0; i < _pattern.PathSegments.Count; i++)
{ {
@ -182,14 +218,14 @@ namespace Microsoft.AspNetCore.Routing.Template
{ {
if (!context.Accept(literalPart.Content)) if (!context.Accept(literalPart.Content))
{ {
return null; return false;
} }
} }
else if (part is RoutePatternSeparatorPart separatorPart) else if (part is RoutePatternSeparatorPart separatorPart)
{ {
if (!context.Accept(separatorPart.Content)) if (!context.Accept(separatorPart.Content))
{ {
return null; return false;
} }
} }
else if (part is RoutePatternParameterPart parameterPart) else if (part is RoutePatternParameterPart parameterPart)
@ -216,7 +252,7 @@ namespace Microsoft.AspNetCore.Routing.Template
// we won't necessarily add it to the URI we generate. // we won't necessarily add it to the URI we generate.
if (!context.Buffer(converted)) if (!context.Buffer(converted))
{ {
return null; return false;
} }
} }
else else
@ -237,7 +273,7 @@ namespace Microsoft.AspNetCore.Routing.Template
} }
else else
{ {
return null; return false;
} }
} }
} }
@ -262,26 +298,33 @@ namespace Microsoft.AspNetCore.Routing.Template
{ {
foreach (var value in values) foreach (var value in values)
{ {
wroteFirst |= AddParameterToContext(context, kvp.Key, value, wroteFirst); wroteFirst |= AddQueryKeyValueToContext(context, kvp.Key, value, wroteFirst);
} }
} }
else else
{ {
wroteFirst |= AddParameterToContext(context, kvp.Key, kvp.Value, wroteFirst); wroteFirst |= AddQueryKeyValueToContext(context, kvp.Key, kvp.Value, wroteFirst);
} }
} }
return context.ToString();
return true;
} }
private bool AddParameterToContext(UriBuildingContext context, string key, object value, bool wroteFirst) private bool AddQueryKeyValueToContext(UriBuildingContext context, string key, object value, bool wroteFirst)
{ {
var converted = Convert.ToString(value, CultureInfo.InvariantCulture); var converted = Convert.ToString(value, CultureInfo.InvariantCulture);
if (!string.IsNullOrEmpty(converted)) if (!string.IsNullOrEmpty(converted))
{ {
context.Writer.Write(wroteFirst ? '&' : '?'); if (context.LowercaseQueryStrings)
_urlEncoder.Encode(context.Writer, key); {
context.Writer.Write('='); key = key.ToLowerInvariant();
_urlEncoder.Encode(context.Writer, converted); converted = converted.ToLowerInvariant();
}
context.QueryWriter.Write(wroteFirst ? '&' : '?');
_urlEncoder.Encode(context.QueryWriter, key);
context.QueryWriter.Write('=');
_urlEncoder.Encode(context.QueryWriter, converted);
return true; return true;
} }
return false; return false;

View File

@ -0,0 +1,153 @@
// 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.Http;
using Xunit;
namespace Microsoft.AspNetCore.Routing
{
// Tests DefaultLinkGenerationTemplate functionality - these are pretty light since most of the functionality
// is a direct subset of DefaultLinkGenerator
//
// Does not cover template processing in detail, those scenarios are validated by TemplateBinderTests
// and DefaultLinkGeneratorProcessTemplateTest
public class DefaultLinkGenerationTemplateTest : LinkGeneratorTestBase
{
[Fact]
public void GetPath_WithoutHttpContext_WithPathBaseAndFragment()
{
// Arrange
var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}");
var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}");
var linkGenerator = CreateLinkGenerator();
var template = new DefaultLinkGenerationTemplate(linkGenerator, new List<RouteEndpoint>() { endpoint1, endpoint2, });
// Act
var path = template.GetPath(
values: new RouteValueDictionary(new { controller = "Home", action = "In?dex", query = "some?query" }),
new PathString("/Foo/Bar?encodeme?"),
new FragmentString("#Fragment?"),
new LinkOptions() { AppendTrailingSlash = true, });
// Assert
Assert.Equal("/Foo/Bar%3Fencodeme%3F/Home/In%3Fdex/?query=some%3Fquery#Fragment?", path);
}
[Fact]
public void GetPath_WithHttpContext_WithPathBaseAndFragment()
{
// Arrange
var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}");
var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}");
var linkGenerator = CreateLinkGenerator();
var template = new DefaultLinkGenerationTemplate(linkGenerator, new List<RouteEndpoint>() { endpoint1, endpoint2, });
var httpContext = CreateHttpContext();
httpContext.Request.PathBase = new PathString("/Foo/Bar?encodeme?");
// Act
var path = template.GetPath(
httpContext,
values: new RouteValueDictionary(new { controller = "Home", action = "In?dex", query = "some?query" }),
new FragmentString("#Fragment?"),
new LinkOptions() { AppendTrailingSlash = true, });
// Assert
Assert.Equal("/Foo/Bar%3Fencodeme%3F/Home/In%3Fdex/?query=some%3Fquery#Fragment?", path);
}
[Fact]
public void GetUri_WithoutHttpContext_WithPathBaseAndFragment()
{
// Arrange
var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}");
var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}");
var linkGenerator = CreateLinkGenerator();
var template = new DefaultLinkGenerationTemplate(linkGenerator, new List<RouteEndpoint>() { endpoint1, endpoint2, });
// Act
var path = template.GetUri(
values: new RouteValueDictionary(new { controller = "Home", action = "In?dex", 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/In%3Fdex/?query=some%3Fquery#Fragment?", path);
}
[Fact]
public void GetUri_WithHttpContext_WithPathBaseAndFragment()
{
// Arrange
var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}");
var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}");
var linkGenerator = CreateLinkGenerator();
var template = new DefaultLinkGenerationTemplate(linkGenerator, new List<RouteEndpoint>() { endpoint1, endpoint2, });
var httpContext = CreateHttpContext();
httpContext.Request.Scheme = "http";
httpContext.Request.Host = new HostString("example.com");
httpContext.Request.PathBase = new PathString("/Foo/Bar?encodeme?");
// Act
var uri = template.GetUri(
httpContext,
values: new RouteValueDictionary(new { controller = "Home", action = "In?dex", query = "some?query" }),
new FragmentString("#Fragment?"),
new LinkOptions() { AppendTrailingSlash = true, });
// Assert
Assert.Equal("http://example.com/Foo/Bar%3Fencodeme%3F/Home/In%3Fdex/?query=some%3Fquery#Fragment?", uri);
}
[Fact]
public void GetPath_WithHttpContext_IncludesAmbientValues()
{
// Arrange
var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}");
var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}");
var linkGenerator = CreateLinkGenerator();
var template = new DefaultLinkGenerationTemplate(linkGenerator, new List<RouteEndpoint>() { endpoint1, endpoint2, });
var httpContext = CreateHttpContext(new { controller = "Home", });
httpContext.Request.Scheme = "http";
httpContext.Request.Host = new HostString("example.com");
// Act
var uri = template.GetPath(httpContext, values: new RouteValueDictionary(new { action = "Index", }));
// Assert
Assert.Equal("/Home/Index", uri);
}
[Fact]
public void GetUri_WithHttpContext_IncludesAmbientValues()
{
// Arrange
var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}");
var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}");
var linkGenerator = CreateLinkGenerator();
var template = new DefaultLinkGenerationTemplate(linkGenerator, new List<RouteEndpoint>() { endpoint1, endpoint2, });
var httpContext = CreateHttpContext(new { controller = "Home", });
httpContext.Request.Scheme = "http";
httpContext.Request.Host = new HostString("example.com");
// Act
var uri = template.GetUri(httpContext, values: new { action = "Index", });
// Assert
Assert.Equal("http://example.com/Home/Index", uri);
}
}
}

View File

@ -0,0 +1,128 @@
// 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 Xunit;
namespace Microsoft.AspNetCore.Routing
{
// Integration tests for GetXyzByRouteValues. These are basic because important behavioral details
// are covered elsewhere.
//
// Does not cover template processing in detail, those scenarios are validated by TemplateBinderTests
// and DefaultLinkGeneratorProcessTemplateTest
//
// Does not cover the RouteValueBasedEndpointFinder in detail. see RouteValueBasedEndpointFinderTest
public class LinkGeneratorRouteValuesAddressExtensionsTest : LinkGeneratorTestBase
{
[Fact]
public void GetPathByRouteValues_WithoutHttpContext_WithPathBaseAndFragment()
{
// Arrange
var endpoint1 = EndpointFactory.CreateRouteEndpoint(
"{controller}/{action}/{id}",
metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "In?dex", })) });
var endpoint2 = EndpointFactory.CreateRouteEndpoint(
"{controller}/{action}/{id?}",
metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "In?dex", })) });
var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
// Act
var path = linkGenerator.GetPathByRouteValues(
routeName: null,
values: new RouteValueDictionary(new { controller = "Home", action = "In?dex", query = "some?query" }),
new PathString("/Foo/Bar?encodeme?"),
new FragmentString("#Fragment?"),
new LinkOptions() { AppendTrailingSlash = true, });
// Assert
Assert.Equal("/Foo/Bar%3Fencodeme%3F/Home/In%3Fdex/?query=some%3Fquery#Fragment?", path);
}
[Fact]
public void GetPathByRouteValues_WithHttpContext_WithPathBaseAndFragment()
{
// Arrange
var endpoint1 = EndpointFactory.CreateRouteEndpoint(
"{controller}/{action}/{id}",
metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "In?dex", })) });
var endpoint2 = EndpointFactory.CreateRouteEndpoint(
"{controller}/{action}/{id?}",
metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "In?dex", })) });
var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
var httpContext = CreateHttpContext();
httpContext.Request.PathBase = new PathString("/Foo/Bar?encodeme?");
// Act
var path = linkGenerator.GetPathByRouteValues(
httpContext,
routeName: null,
values: new RouteValueDictionary(new { controller = "Home", action = "In?dex", query = "some?query" }),
new FragmentString("#Fragment?"),
new LinkOptions() { AppendTrailingSlash = true, });
// Assert
Assert.Equal("/Foo/Bar%3Fencodeme%3F/Home/In%3Fdex/?query=some%3Fquery#Fragment?", path);
}
[Fact]
public void GetUriByRouteValues_WithoutHttpContext_WithPathBaseAndFragment()
{
// Arrange
var endpoint1 = EndpointFactory.CreateRouteEndpoint(
"{controller}/{action}/{id}",
metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "In?dex", })) });
var endpoint2 = EndpointFactory.CreateRouteEndpoint(
"{controller}/{action}/{id?}",
metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "In?dex", })) });
var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
// Act
var path = linkGenerator.GetUriByRouteValues(
routeName: null,
values: new RouteValueDictionary(new { controller = "Home", action = "In?dex", 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/In%3Fdex/?query=some%3Fquery#Fragment?", path);
}
[Fact]
public void GetUri_WithHttpContext_WithPathBaseAndFragment()
{
// Arrange
var endpoint1 = EndpointFactory.CreateRouteEndpoint(
"{controller}/{action}/{id}",
metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "In?dex", })) });
var endpoint2 = EndpointFactory.CreateRouteEndpoint(
"{controller}/{action}/{id?}",
metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "In?dex", })) });
var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
var httpContext = CreateHttpContext();
httpContext.Request.Scheme = "http";
httpContext.Request.Host = new HostString("example.com");
httpContext.Request.PathBase = new PathString("/Foo/Bar?encodeme?");
// Act
var uri = linkGenerator.GetUriByRouteValues(
httpContext,
routeName: null,
values: new RouteValueDictionary(new { controller = "Home", action = "In?dex", query = "some?query" }),
new FragmentString("#Fragment?"),
new LinkOptions() { AppendTrailingSlash = true, });
// Assert
Assert.Equal("http://example.com/Foo/Bar%3Fencodeme%3F/Home/In%3Fdex/?query=some%3Fquery#Fragment?", uri);
}
}
}

View File

@ -0,0 +1,77 @@
// 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.Routing.Internal;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Routing
{
public abstract class LinkGeneratorTestBase
{
protected 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;
}
private ServiceCollection GetBasicServices()
{
var services = new ServiceCollection();
services.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
services.AddOptions();
services.AddRouting();
services.AddLogging();
return services;
}
protected virtual void AddAdditionalServices(IServiceCollection services)
{
}
private protected DefaultLinkGenerator CreateLinkGenerator(params Endpoint[] endpoints)
{
return CreateLinkGenerator(routeOptions: null, services: null, endpoints);
}
private protected DefaultLinkGenerator CreateLinkGenerator(RouteOptions routeOptions = null, IServiceCollection services = null, params Endpoint[] endpoints)
{
if (services == null)
{
services = GetBasicServices();
AddAdditionalServices(services);
}
if (endpoints != null || endpoints.Length > 0)
{
services.Configure<EndpointOptions>(o =>
{
o.DataSources.Add(new DefaultEndpointDataSource(endpoints));
});
}
routeOptions = routeOptions ?? new RouteOptions();
var options = Options.Create(routeOptions);
var serviceProvider = services.BuildServiceProvider();
return new DefaultLinkGenerator(
new DefaultParameterPolicyFactory(options, serviceProvider),
new DefaultObjectPool<UriBuildingContext>(new UriBuilderContextPooledObjectPolicy()),
options,
NullLogger<DefaultLinkGenerator>.Instance,
serviceProvider);
}
}
}

View File

@ -1,18 +1,14 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Internal; using Microsoft.AspNetCore.Routing.Internal;
using Microsoft.AspNetCore.Routing.Matching;
using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.AspNetCore.Routing.TestObjects; using Microsoft.AspNetCore.Routing.TestObjects;
using Microsoft.AspNetCore.Routing.Tree; using Microsoft.AspNetCore.Routing.Tree;
using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.Options;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Routing namespace Microsoft.AspNetCore.Routing