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]
public void EndpointRouting()
{
var actualUrl = _linkGenerator.GetLink(
var actualUrl = _linkGenerator.GetPathByRouteValues(
_requestContext.HttpContext,
values: new RouteValueDictionary(_lookUpValues));
routeName: null,
values: _lookUpValues);
AssertUrl("/repos/aspnet/routing/issues/comments/20202", actualUrl);
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,29 +1,90 @@
// 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;
namespace Microsoft.AspNetCore.Routing
{
/// <summary>
/// Defines a contract to generate a URL from a template.
/// </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
{
/// <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>
/// <param name="values">An object that contains route values.</param>
/// <returns>The generated URL.</returns>
public string MakeUrl(object values)
{
return MakeUrl(values, options: null);
}
/// <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 GetPath(
HttpContext httpContext,
object values,
FragmentString fragment = default,
LinkOptions options = default);
/// <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>
/// <param name="values">An object that contains route values.</param>
/// <param name="options">The <see cref="LinkOptions"/>.</param>
/// <returns>The generated URL.</returns>
public abstract string MakeUrl(object values, LinkOptions options);
/// <param name="values">The route values. Used to expand parameters in the route template. Optional.</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>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.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing
{
/// <summary>
/// Defines a contract to generate URLs to endpoints.
/// Defines a contract to generate absolute and related URIs based on endpoint routing.
/// </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
{
/// <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>
/// <param name="values">An object that contains route values.</param>
/// <returns>The generated URL.</returns>
public string GetLink(object values)
{
return GetLink(httpContext: null, routeName: null, values, options: null);
}
/// <summary>
/// Generates a URL with an absolute path from the specified route values and link options.
/// </summary>
/// <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(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>(
/// <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 GetPathByAddress<TAddress>(
HttpContext httpContext,
TAddress address,
object values,
out string link)
{
return TryGetLinkByAddress(httpContext, address, values, options: null, out link);
}
RouteValueDictionary values,
FragmentString fragment = default,
LinkOptions options = default);
/// <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;'.
/// Generates a URI with an absolute path based on the provided values.
/// </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="options">The <see cref="LinkOptions"/>.</param>
/// <returns>The generated URL.</returns>
public string GetLinkByAddress<TAddress>(
/// <typeparam name="TAddress">The address type.</typeparam>
/// <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="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>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,
TAddress address,
object values,
LinkOptions options)
{
if (TryGetLinkByAddress(httpContext, address, values, options, out var link))
{
return link;
}
throw new InvalidOperationException("Could not find a matching endpoint to generate a link.");
}
RouteValueDictionary values,
FragmentString fragment = default,
LinkOptions options = default);
/// <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.
/// Generates an absolute URI based on the provided values.
/// </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="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 TryGetLinkByAddress<TAddress>(
HttpContext httpContext,
/// <typeparam name="TAddress">The address type.</typeparam>
/// <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="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 GetUriByAddress<TAddress>(
TAddress address,
object values,
LinkOptions options,
out string link);
RouteValueDictionary values,
string scheme,
HostString host,
PathString pathBase = default,
FragmentString fragment = default,
LinkOptions options = default);
/// <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.
/// Gets a <see cref="LinkGenerationTemplate"/> based on the provided <paramref name="address"/>.
/// </summary>
/// <param name="values">
/// An object that contains route values. These values are used to lookup endpoint(s).
/// </param>
/// <typeparam name="TAddress">The address type.</typeparam>
/// <param name="address">The address value. Used to resolve endpoints.</param>
/// <returns>
/// If an endpoint(s) was found successfully, then this returns a template object representing that,
/// <c>null</c> otherwise.
/// A <see cref="LinkGenerationTemplate"/> if one or more endpoints matching the address can be found, otherwise <c>null</c>.
/// </returns>
public LinkGenerationTemplate GetTemplate(object values)
{
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);
public abstract LinkGenerationTemplate GetTemplateByAddress<TAddress>(TAddress address);
}
}

View File

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

View File

@ -1,62 +1,98 @@
// 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 Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Matching;
namespace Microsoft.AspNetCore.Routing
{
internal class DefaultLinkGenerationTemplate : LinkGenerationTemplate
internal sealed class DefaultLinkGenerationTemplate : LinkGenerationTemplate
{
public DefaultLinkGenerationTemplate(
DefaultLinkGenerator linkGenerator,
IEnumerable<RouteEndpoint> endpoints,
HttpContext httpContext,
RouteValueDictionary explicitValues,
RouteValueDictionary ambientValues)
public DefaultLinkGenerationTemplate(DefaultLinkGenerator linkGenerator, List<RouteEndpoint> endpoints)
{
LinkGenerator = linkGenerator;
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; }
internal RouteValueDictionary EarlierExplicitValues { get; }
internal RouteValueDictionary AmbientValues { get; }
public override string MakeUrl(object values, LinkOptions options)
public override string GetPath(
HttpContext httpContext,
object values,
FragmentString fragment = default,
LinkOptions options = null)
{
var currentValues = new RouteValueDictionary(values);
var mergedValuesDictionary = new RouteValueDictionary(EarlierExplicitValues);
foreach (var kvp in currentValues)
if (httpContext == null)
{
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(
HttpContext,
endpoint,
AmbientValues,
mergedValuesDictionary,
options);
if (link != null)
{
return link;
}
throw new ArgumentNullException(nameof(httpContext));
}
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.Text.Encodings.Web;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Routing.Internal;
using Microsoft.AspNetCore.Routing.Matching;
using Microsoft.AspNetCore.Routing.Template;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@ -17,14 +17,17 @@ using Microsoft.Extensions.Options;
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 ObjectPool<UriBuildingContext> _uriBuildingContextPool;
private readonly ILogger<DefaultLinkGenerator> _logger;
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(
ParameterPolicyFactory parameterPolicyFactory,
@ -35,73 +38,218 @@ namespace Microsoft.AspNetCore.Routing
{
_parameterPolicyFactory = parameterPolicyFactory;
_uriBuildingContextPool = uriBuildingContextPool;
_options = routeOptions.Value;
_logger = logger;
_serviceProvider = serviceProvider;
_globalLinkOptions = new LinkOptions()
{
AppendTrailingSlash = routeOptions.Value.AppendTrailingSlash,
LowercaseQueryStrings = routeOptions.Value.LowercaseQueryStrings,
LowercaseUrls = routeOptions.Value.LowercaseUrls,
};
}
public override bool TryGetLink(
HttpContext httpContext,
string routeName,
object values,
LinkOptions options,
out string link)
{
return TryGetLinkByRouteValues(
httpContext,
routeName,
values,
options,
out link);
}
public override bool TryGetLinkByAddress<TAddress>(
public override string GetPathByAddress<TAddress>(
HttpContext httpContext,
TAddress address,
object values,
LinkOptions options,
out string link)
RouteValueDictionary values,
FragmentString fragment = default,
LinkOptions options = null)
{
return TryGetLinkByAddressInternal(
httpContext,
address,
explicitValues: values,
ambientValues: GetAmbientValues(httpContext),
options,
out link);
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
var endpoints = GetEndpoints(address);
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 explicitValues = new RouteValueDictionary(values);
var endpoints = GetEndpoints(address);
if (endpoints.Count == 0)
{
return null;
}
return GetTemplateInternal(
httpContext,
new RouteValuesAddress
{
RouteName = routeName,
ExplicitValues = explicitValues,
AmbientValues = ambientValues
},
ambientValues,
explicitValues,
values);
return GetPathByEndpoints(
endpoints,
ambientValues: null,
values,
pathBase,
fragment,
options);
}
public override LinkGenerationTemplate GetTemplateByAddress<TAddress>(
public override string GetUriByAddress<TAddress>(
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,
RouteEndpoint endpoint,
RouteValueDictionary ambientValues,
RouteValueDictionary explicitValues,
LinkOptions options)
LinkOptions options,
out (PathString path, QueryString query) result)
{
var templateBinder = new TemplateBinder(
UrlEncoder.Default,
@ -117,118 +265,22 @@ namespace Microsoft.AspNetCore.Routing
if (templateValuesResult == null)
{
// We're missing one of the required values for this route.
return null;
result = default;
return false;
}
if (!MatchesConstraints(httpContext, endpoint, templateValuesResult.CombinedValues))
{
return null;
result = default;
return false;
}
var url = templateBinder.BindValues(templateValuesResult.AcceptedValues);
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)
if (!templateBinder.TryBindValues(templateValuesResult.AcceptedValues, options, _globalLinkOptions, out result))
{
return false;
}
foreach (var endpoint in endpoints)
{
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);
return true;
}
private bool MatchesConstraints(
@ -260,75 +312,10 @@ namespace Microsoft.AspNetCore.Routing
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;
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;
return httpContext?.Features.Get<IRouteValuesFeature>()?.RouteValues;
}
}
}

View File

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

View File

@ -6,16 +6,18 @@ using System.Diagnostics;
using System.IO;
using System.Text;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing.Internal
{
[DebuggerDisplay("{DebuggerToString(),nq}")]
public class UriBuildingContext
{
// Holds the 'accepted' parts of the uri.
private readonly StringBuilder _uri;
// Holds the 'accepted' parts of the path.
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
// followed by other optional segments than we will just throw it away.
private readonly List<BufferValue> _buffer;
@ -27,20 +29,30 @@ namespace Microsoft.AspNetCore.Routing.Internal
public UriBuildingContext(UrlEncoder urlEncoder)
{
_urlEncoder = urlEncoder;
_uri = new StringBuilder();
_path = new StringBuilder();
_query = new StringBuilder();
_buffer = new List<BufferValue>();
Writer = new StringWriter(_uri);
PathWriter = new StringWriter(_path);
QueryWriter = new StringWriter(_query);
_lastValueOffset = -1;
BufferState = 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 UriState { get; private set; }
public TextWriter Writer { get; }
public TextWriter PathWriter { get; }
public TextWriter QueryWriter { get; }
public bool Accept(string value)
{
@ -68,6 +80,12 @@ namespace Microsoft.AspNetCore.Routing.Internal
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++)
{
if (_buffer[i].RequiresEncoding)
@ -76,29 +94,29 @@ namespace Microsoft.AspNetCore.Routing.Internal
}
else
{
_uri.Append(_buffer[i].Value);
_path.Append(_buffer[i].Value);
}
}
_buffer.Clear();
if (UriState == SegmentState.Beginning && BufferState == SegmentState.Beginning)
{
if (_uri.Length != 0)
if (_path.Length != 0)
{
_uri.Append("/");
_path.Append("/");
}
}
BufferState = SegmentState.Inside;
UriState = SegmentState.Inside;
_lastValueOffset = _uri.Length;
_lastValueOffset = _path.Length;
// Allow the first segment to have a leading slash.
// 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);
}
else
@ -112,7 +130,7 @@ namespace Microsoft.AspNetCore.Routing.Internal
public void Remove(string literal)
{
Debug.Assert(_lastValueOffset != -1, "Cannot invoke Remove more than once.");
_uri.Length = _lastValueOffset;
_path.Length = _lastValueOffset;
_lastValueOffset = -1;
}
@ -152,7 +170,7 @@ namespace Microsoft.AspNetCore.Routing.Internal
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));
}
@ -172,11 +190,17 @@ namespace Microsoft.AspNetCore.Routing.Internal
public void Clear()
{
_uri.Clear();
if (_uri.Capacity > 128)
_path.Clear();
if (_path.Capacity > 128)
{
// 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();
@ -189,18 +213,52 @@ namespace Microsoft.AspNetCore.Routing.Internal
_lastValueOffset = -1;
BufferState = 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()
{
// 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.
_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)
@ -219,7 +277,7 @@ namespace Microsoft.AspNetCore.Routing.Internal
// Just encode everything if its ok to encode slashes
if (encodeSlashes)
{
_urlEncoder.Encode(Writer, value, start, characterCount);
_urlEncoder.Encode(PathWriter, value, start, characterCount);
}
else
{
@ -227,8 +285,8 @@ namespace Microsoft.AspNetCore.Routing.Internal
int length = start + characterCount;
while ((end = value.IndexOf('/', start, characterCount)) >= 0)
{
_urlEncoder.Encode(Writer, value, start, end - start);
_uri.Append("/");
_urlEncoder.Encode(PathWriter, value, start, end - start);
_path.Append("/");
start = end + 1;
characterCount = length - start;
@ -236,14 +294,14 @@ namespace Microsoft.AspNetCore.Routing.Internal
if (end < 0 && characterCount >= 0)
{
_urlEncoder.Encode(Writer, value, start, length - start);
_urlEncoder.Encode(PathWriter, value, start, length - start);
}
}
}
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}")]
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;
}
public string Name { get; }
public string RouteName { get; }
public IReadOnlyDictionary<string, object> RequiredValues { get; }
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)
{

View File

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

View File

@ -160,12 +160,48 @@ namespace Microsoft.AspNetCore.Routing.Template
public string BindValues(RouteValueDictionary acceptedValues)
{
var context = _pool.Get();
var result = BindValues(context, acceptedValues);
_pool.Return(context);
return result;
try
{
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++)
{
@ -182,14 +218,14 @@ namespace Microsoft.AspNetCore.Routing.Template
{
if (!context.Accept(literalPart.Content))
{
return null;
return false;
}
}
else if (part is RoutePatternSeparatorPart separatorPart)
{
if (!context.Accept(separatorPart.Content))
{
return null;
return false;
}
}
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.
if (!context.Buffer(converted))
{
return null;
return false;
}
}
else
@ -237,7 +273,7 @@ namespace Microsoft.AspNetCore.Routing.Template
}
else
{
return null;
return false;
}
}
}
@ -262,26 +298,33 @@ namespace Microsoft.AspNetCore.Routing.Template
{
foreach (var value in values)
{
wroteFirst |= AddParameterToContext(context, kvp.Key, value, wroteFirst);
wroteFirst |= AddQueryKeyValueToContext(context, kvp.Key, value, wroteFirst);
}
}
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);
if (!string.IsNullOrEmpty(converted))
{
context.Writer.Write(wroteFirst ? '&' : '?');
_urlEncoder.Encode(context.Writer, key);
context.Writer.Write('=');
_urlEncoder.Encode(context.Writer, converted);
if (context.LowercaseQueryStrings)
{
key = key.ToLowerInvariant();
converted = converted.ToLowerInvariant();
}
context.QueryWriter.Write(wroteFirst ? '&' : '?');
_urlEncoder.Encode(context.QueryWriter, key);
context.QueryWriter.Write('=');
_urlEncoder.Encode(context.QueryWriter, converted);
return true;
}
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.
// 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.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Internal;
using Microsoft.AspNetCore.Routing.Matching;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.AspNetCore.Routing.TestObjects;
using Microsoft.AspNetCore.Routing.Tree;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.Options;
using Xunit;
namespace Microsoft.AspNetCore.Routing