diff --git a/benchmarks/Microsoft.AspNetCore.Routing.Performance/LinkGeneration/LinkGenerationGithubBenchmark.cs b/benchmarks/Microsoft.AspNetCore.Routing.Performance/LinkGeneration/LinkGenerationGithubBenchmark.cs
index b7309a159e..f2b8df799f 100644
--- a/benchmarks/Microsoft.AspNetCore.Routing.Performance/LinkGeneration/LinkGenerationGithubBenchmark.cs
+++ b/benchmarks/Microsoft.AspNetCore.Routing.Performance/LinkGeneration/LinkGenerationGithubBenchmark.cs
@@ -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);
}
diff --git a/benchmarks/Microsoft.AspNetCore.Routing.Performance/LinkGeneration/SingleRouteWithConstraintsBenchmark.cs b/benchmarks/Microsoft.AspNetCore.Routing.Performance/LinkGeneration/SingleRouteWithConstraintsBenchmark.cs
index 271263bc7f..204966e46d 100644
--- a/benchmarks/Microsoft.AspNetCore.Routing.Performance/LinkGeneration/SingleRouteWithConstraintsBenchmark.cs
+++ b/benchmarks/Microsoft.AspNetCore.Routing.Performance/LinkGeneration/SingleRouteWithConstraintsBenchmark.cs
@@ -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);
}
diff --git a/benchmarks/Microsoft.AspNetCore.Routing.Performance/LinkGeneration/SingleRouteWithNoParametersBenchmark.cs b/benchmarks/Microsoft.AspNetCore.Routing.Performance/LinkGeneration/SingleRouteWithNoParametersBenchmark.cs
index 0b2e30dc64..6b46a5acfe 100644
--- a/benchmarks/Microsoft.AspNetCore.Routing.Performance/LinkGeneration/SingleRouteWithNoParametersBenchmark.cs
+++ b/benchmarks/Microsoft.AspNetCore.Routing.Performance/LinkGeneration/SingleRouteWithNoParametersBenchmark.cs
@@ -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);
}
diff --git a/benchmarks/Microsoft.AspNetCore.Routing.Performance/LinkGeneration/SingleRouteWithParametersBenchmark.cs b/benchmarks/Microsoft.AspNetCore.Routing.Performance/LinkGeneration/SingleRouteWithParametersBenchmark.cs
index 6ca5d74fbe..6037924e9a 100644
--- a/benchmarks/Microsoft.AspNetCore.Routing.Performance/LinkGeneration/SingleRouteWithParametersBenchmark.cs
+++ b/benchmarks/Microsoft.AspNetCore.Routing.Performance/LinkGeneration/SingleRouteWithParametersBenchmark.cs
@@ -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);
}
diff --git a/samples/RoutingSample.Web/UseEndpointRoutingStartup.cs b/samples/RoutingSample.Web/UseEndpointRoutingStartup.cs
index 49262142dd..d600b905a9 100644
--- a/samples/RoutingSample.Web/UseEndpointRoutingStartup.cs
+++ b/samples/RoutingSample.Web/UseEndpointRoutingStartup.cs
@@ -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"),
});
diff --git a/src/Microsoft.AspNetCore.Routing.Abstractions/LinkGenerationTemplate.cs b/src/Microsoft.AspNetCore.Routing.Abstractions/LinkGenerationTemplate.cs
index ec3736228a..82da67cd47 100644
--- a/src/Microsoft.AspNetCore.Routing.Abstractions/LinkGenerationTemplate.cs
+++ b/src/Microsoft.AspNetCore.Routing.Abstractions/LinkGenerationTemplate.cs
@@ -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
{
///
/// Defines a contract to generate a URL from a template.
///
+ ///
+ /// A can be created from
+ /// by supplying an address value which has matching endpoints. The returned
+ /// will be bound to the endpoints matching the address that was originally provided.
+ ///
public abstract class LinkGenerationTemplate
{
///
- /// Generates a URL with an absolute path from the specified route values.
+ /// Generates a URI with an absolute path based on the provided values.
///
- /// An object that contains route values.
- /// The generated URL.
- public string MakeUrl(object values)
- {
- return MakeUrl(values, options: null);
- }
+ /// The associated with the current request.
+ /// The route values. Used to expand parameters in the route template. Optional.
+ /// An optional URI fragment. Appended to the resulting URI.
+ ///
+ /// An optional . Settings on provided object override the settings with matching
+ /// names from RouteOptions.
+ ///
+ /// A URI with an absolute path, or null.
+ public abstract string GetPath(
+ HttpContext httpContext,
+ object values,
+ FragmentString fragment = default,
+ LinkOptions options = default);
///
- /// 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.
///
- /// An object that contains route values.
- /// The .
- /// The generated URL.
- public abstract string MakeUrl(object values, LinkOptions options);
+ /// The route values. Used to expand parameters in the route template. Optional.
+ /// An optional URI path base. Prepended to the path in the resulting URI.
+ /// An optional URI fragment. Appended to the resulting URI.
+ ///
+ /// An optional . Settings on provided object override the settings with matching
+ /// names from RouteOptions.
+ ///
+ /// A URI with an absolute path, or null.
+ public abstract string GetPath(
+ object values,
+ PathString pathBase = default,
+ FragmentString fragment = default,
+ LinkOptions options = default);
+
+ ///
+ /// Generates an absolute URI based on the provided values.
+ ///
+ /// The associated with the current request.
+ /// The route values. Used to expand parameters in the route template. Optional.
+ /// An optional URI fragment. Appended to the resulting URI.
+ ///
+ /// An optional . Settings on provided object override the settings with matching
+ /// names from RouteOptions.
+ ///
+ /// A URI with an absolute path, or null.
+ public abstract string GetUri(
+ HttpContext httpContext,
+ object values,
+ FragmentString fragment = default,
+ LinkOptions options = default);
+
+ ///
+ /// Generates an absolute URI based on the provided values.
+ ///
+ /// The route values. Used to expand parameters in the route template. Optional.
+ /// The URI scheme, applied to the resulting URI.
+ /// The URI host/authority, applied to the resulting URI.
+ /// An optional URI path base. Prepended to the path in the resulting URI.
+ /// An optional URI fragment. Appended to the resulting URI.
+ ///
+ /// An optional . Settings on provided object override the settings with matching
+ /// names from RouteOptions.
+ ///
+ /// An absolute URI, or null.
+ public abstract string GetUri(
+ object values,
+ string scheme,
+ HostString host,
+ PathString pathBase = default,
+ FragmentString fragment = default,
+ LinkOptions options = default);
}
}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Routing.Abstractions/LinkGenerator.cs b/src/Microsoft.AspNetCore.Routing.Abstractions/LinkGenerator.cs
index 35b9456bf6..e8659b3577 100644
--- a/src/Microsoft.AspNetCore.Routing.Abstractions/LinkGenerator.cs
+++ b/src/Microsoft.AspNetCore.Routing.Abstractions/LinkGenerator.cs
@@ -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
{
///
- /// Defines a contract to generate URLs to endpoints.
+ /// Defines a contract to generate absolute and related URIs based on endpoint routing.
///
+ ///
+ ///
+ /// 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 RoutePattern 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.
+ ///
+ ///
+ /// The methods provided by the type are general infrastructure, and support
+ /// the standard link generator functionality for any type of address. The most convenient way to use
+ /// is through extension methods that perform operations for a specific
+ /// address type.
+ ///
+ ///
public abstract class LinkGenerator
{
///
- /// Generates a URL with an absolute path from the specified route values.
+ /// Generates a URI with an absolute path based on the provided values.
///
- /// An object that contains route values.
- /// The generated URL.
- public string GetLink(object values)
- {
- return GetLink(httpContext: null, routeName: null, values, options: null);
- }
-
- ///
- /// Generates a URL with an absolute path from the specified route values and link options.
- ///
- /// An object that contains route values.
- /// The .
- /// The generated URL.
- public string GetLink(object values, LinkOptions options)
- {
- return GetLink(httpContext: null, routeName: null, values, options);
- }
-
- ///
- /// Generates a URL with an absolute path from the specified route values.
- /// A return value indicates whether the operation succeeded.
- ///
- /// An object that contains route values.
- /// The generated URL.
- /// true if a URL was generated successfully; otherwise, false.
- public bool TryGetLink(object values, out string link)
- {
- return TryGetLink(httpContext: null, routeName: null, values, options: null, out link);
- }
-
- ///
- /// Generates a URL with an absolute path from the specified route values and link options.
- /// A return value indicates whether the operation succeeded.
- ///
- /// An object that contains route values.
- /// The .
- /// The generated URL.
- /// true if a URL was generated successfully; otherwise, false.
- public bool TryGetLink(object values, LinkOptions options, out string link)
- {
- return TryGetLink(httpContext: null, routeName: null, values, options, out link);
- }
-
- ///
- /// Generates a URL with an absolute path from the specified route values.
- ///
- /// The associated with current request.
- /// An object that contains route values.
- /// The generated URL.
- public string GetLink(HttpContext httpContext, object values)
- {
- return GetLink(httpContext, routeName: null, values, options: null);
- }
-
- ///
- /// Generates a URL with an absolute path from the specified route values.
- /// A return value indicates whether the operation succeeded.
- ///
- /// The associated with current request.
- /// An object that contains route values.
- /// The generated URL.
- /// true if a URL was generated successfully; otherwise, false.
- public bool TryGetLink(HttpContext httpContext, object values, out string link)
- {
- return TryGetLink(httpContext, routeName: null, values, options: null, out link);
- }
-
- ///
- /// Generates a URL with an absolute path from the specified route values and link options.
- ///
- /// The associated with current request.
- /// An object that contains route values.
- /// The .
- /// The generated URL.
- public string GetLink(HttpContext httpContext, object values, LinkOptions options)
- {
- return GetLink(httpContext, routeName: null, values, options);
- }
-
- ///
- /// Generates a URL with an absolute path from the specified route values and link options.
- /// A return value indicates whether the operation succeeded.
- ///
- /// The associated with current request.
- /// An object that contains route values.
- /// The .
- /// The generated URL.
- /// true if a URL was generated successfully; otherwise, false.
- public bool TryGetLink(HttpContext httpContext, object values, LinkOptions options, out string link)
- {
- return TryGetLink(httpContext, routeName: null, values, options, out link);
- }
-
- ///
- /// Generates a URL with an absolute path from the specified route name and route values.
- ///
- /// The name of the route to generate the URL to.
- /// An object that contains route values.
- /// The generated URL.
- public string GetLink(string routeName, object values)
- {
- return GetLink(httpContext: null, routeName, values, options: null);
- }
-
- ///
- /// Generates a URL with an absolute path from the specified route name and route values.
- /// A return value indicates whether the operation succeeded.
- ///
- /// The name of the route to generate the URL to.
- /// An object that contains route values.
- /// The generated URL.
- /// true if a URL was generated successfully; otherwise, false.
- public bool TryGetLink(string routeName, object values, out string link)
- {
- return TryGetLink(httpContext: null, routeName, values, options: null, out link);
- }
-
- ///
- /// Generates a URL with an absolute path from the specified route name and route values.
- ///
- /// The name of the route to generate the URL to.
- /// An object that contains route values.
- /// The .
- /// The generated URL.
- public string GetLink(string routeName, object values, LinkOptions options)
- {
- return GetLink(httpContext: null, routeName, values, options);
- }
-
- ///
- /// 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.
- ///
- /// The name of the route to generate the URL to.
- /// An object that contains route values.
- /// The .
- /// The generated URL.
- /// true if a URL was generated successfully; otherwise, false.
- public bool TryGetLink(string routeName, object values, LinkOptions options, out string link)
- {
- return TryGetLink(httpContext: null, routeName, values, options, out link);
- }
-
- ///
- /// Generates a URL with an absolute path from the specified route name and route values.
- ///
- /// The name of the route to generate the URL to.
- /// The associated with current request.
- /// An object that contains route values.
- /// The generated URL.
- public string GetLink(HttpContext httpContext, string routeName, object values)
- {
- return GetLink(httpContext, routeName, values, options: null);
- }
-
- ///
- /// Generates a URL with an absolute path from the specified route name and route values.
- /// A return value indicates whether the operation succeeded.
- ///
- /// The associated with current request.
- /// The name of the route to generate the URL to.
- /// An object that contains route values.
- /// The generated URL.
- /// true if a URL was generated successfully; otherwise, false.
- public bool TryGetLink(HttpContext httpContext, string routeName, object values, out string link)
- {
- return TryGetLink(httpContext, routeName, values, options: null, out link);
- }
-
- ///
- /// Generates a URL with an absolute path from the specified route name, route values and link options.
- ///
- /// The name of the route to generate the URL to.
- /// The associated with current request.
- /// An object that contains route values.
- /// The .
- /// The generated URL.
- 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.");
- }
-
- ///
- /// 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.
- ///
- /// The associated with current request.
- /// The name of the route to generate the URL to.
- /// An object that contains route values.
- /// The .
- /// The generated URL.
- /// true if a URL was generated successfully; otherwise, false.
- public abstract bool TryGetLink(
- HttpContext httpContext,
- string routeName,
- object values,
- LinkOptions options,
- out string link);
-
- ///
- /// 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<TAddress>'.
- ///
- /// The address type to look up endpoints.
- /// The information used to look up endpoints for generating a URL.
- /// An object that contains route values.
- /// The generated URL.
- public string GetLinkByAddress(TAddress address, object values)
- {
- return GetLinkByAddress(httpContext: null, address, values, options: null);
- }
-
- ///
- /// 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<TAddress>'.
- /// A return value indicates whether the operation succeeded.
- ///
- /// The address type to look up endpoints.
- /// The information used to look up endpoints for generating a URL.
- /// An object that contains route values.
- /// The generated URL.
- /// true if a URL was generated successfully; otherwise, false.
- public bool TryGetLinkByAddress(TAddress address, object values, out string link)
- {
- return TryGetLinkByAddress(address, values, options: null, out link);
- }
-
- ///
- /// 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<TAddress>'.
- ///
- /// The address type to look up endpoints.
- /// The information used to look up endpoints for generating a URL.
- /// An object that contains route values.
- /// The .
- /// The generated URL.
- public string GetLinkByAddress(TAddress address, object values, LinkOptions options)
- {
- return GetLinkByAddress(httpContext: null, address, values, options);
- }
-
- ///
- /// 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<TAddress>'.
- /// A return value indicates whether the operation succeeded.
- ///
- /// The address type to look up endpoints.
- /// The information used to look up endpoints for generating a URL.
- /// An object that contains route values.
- /// The .
- /// The generated URL.
- /// true if a URL was generated successfully; otherwise, false.
- public bool TryGetLinkByAddress(
- TAddress address,
- object values,
- LinkOptions options,
- out string link)
- {
- return TryGetLinkByAddress(httpContext: null, address, values, options, out link);
- }
-
- ///
- /// 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<TAddress>'.
- ///
- /// The address type to look up endpoints.
- /// The information used to look up endpoints for generating a URL.
- /// The associated with current request.
- /// An object that contains route values.
- /// The generated URL.
- public string GetLinkByAddress(HttpContext httpContext, TAddress address, object values)
- {
- return GetLinkByAddress(httpContext, address, values, options: null);
- }
-
- ///
- /// 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<TAddress>'.
- /// A return value indicates whether the operation succeeded.
- ///
- /// The address type to look up endpoints.
- /// The information used to look up endpoints for generating a URL.
- /// The associated with current request.
- /// An object that contains route values.
- /// The generated URL.
- /// true if a URL was generated successfully; otherwise, false.
- public bool TryGetLinkByAddress(
+ /// The address type.
+ /// The associated with the current request.
+ /// The address value. Used to resolve endpoints.
+ /// The route values. Used to expand parameters in the route template. Optional.
+ /// An optional URI fragment. Appended to the resulting URI.
+ ///
+ /// An optional . Settings on provided object override the settings with matching
+ /// names from RouteOptions.
+ ///
+ /// A URI with an absolute path, or null.
+ public abstract string GetPathByAddress(
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);
///
- /// 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<TAddress>'.
+ /// Generates a URI with an absolute path based on the provided values.
///
- /// The address type to look up endpoints.
- /// The information used to look up endpoints for generating a URL.
- /// The associated with current request.
- /// An object that contains route values.
- /// The .
- /// The generated URL.
- public string GetLinkByAddress(
+ /// The address type.
+ /// The address value. Used to resolve endpoints.
+ /// The route values. Used to expand parameters in the route template. Optional.
+ /// An optional URI path base. Prepended to the path in the resulting URI.
+ /// An optional URI fragment. Appended to the resulting URI.
+ ///
+ /// An optional . Settings on provided object override the settings with matching
+ /// names from RouteOptions.
+ ///
+ /// A URI with an absolute path, or null.
+ public abstract string GetPathByAddress(
+ TAddress address,
+ RouteValueDictionary values,
+ PathString pathBase = default,
+ FragmentString fragment = default,
+ LinkOptions options = default);
+
+ ///
+ /// Generates an absolute URI based on the provided values.
+ ///
+ /// The address type.
+ /// The associated with the current request.
+ /// The address value. Used to resolve endpoints.
+ /// The route values. Used to expand parameters in the route template. Optional.
+ /// An optional URI fragment. Appended to the resulting URI.
+ ///
+ /// An optional . Settings on provided object override the settings with matching
+ /// names from RouteOptions.
+ ///
+ /// A URI with an absolute path, or null.
+ public abstract string GetUriByAddress(
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);
///
- /// 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<TAddress>'.
- /// A return value indicates whether the operation succeeded.
+ /// Generates an absolute URI based on the provided values.
///
- /// The address type to look up endpoints.
- /// The information used to look up endpoints for generating a URL.
- /// The associated with current request.
- /// An object that contains route values.
- /// The .
- /// The generated URL.
- /// true if a URL was generated successfully; otherwise, false.
- public abstract bool TryGetLinkByAddress(
- HttpContext httpContext,
+ /// The address type.
+ /// The address value. Used to resolve endpoints.
+ /// The route values. Used to expand parameters in the route template. Optional.
+ /// The URI scheme, applied to the resulting URI.
+ /// The URI host/authority, applied to the resulting URI.
+ /// An optional URI path base. Prepended to the path in the resulting URI.
+ /// An optional URI fragment. Appended to the resulting URI.
+ ///
+ /// An optional . Settings on provided object override the settings with matching
+ /// names from RouteOptions.
+ ///
+ /// An absolute URI, or null.
+ public abstract string GetUriByAddress(
TAddress address,
- object values,
- LinkOptions options,
- out string link);
+ RouteValueDictionary values,
+ string scheme,
+ HostString host,
+ PathString pathBase = default,
+ FragmentString fragment = default,
+ LinkOptions options = default);
///
- /// Gets a 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 api.
+ /// Gets a based on the provided .
///
- ///
- /// An object that contains route values. These values are used to lookup endpoint(s).
- ///
+ /// The address type.
+ /// The address value. Used to resolve endpoints.
///
- /// If an endpoint(s) was found successfully, then this returns a template object representing that,
- /// null otherwise.
+ /// A if one or more endpoints matching the address can be found, otherwise null.
///
- public LinkGenerationTemplate GetTemplate(object values)
- {
- return GetTemplate(httpContext: null, routeName: null, values);
- }
-
- ///
- /// Gets a 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 api.
- ///
- /// The name of the route to generate the URL to.
- ///
- /// An object that contains route values. These values are used to lookup for endpoint(s).
- ///
- ///
- /// If an endpoint(s) was found successfully, then this returns a template object representing that,
- /// null otherwise.
- ///
- public LinkGenerationTemplate GetTemplate(string routeName, object values)
- {
- return GetTemplate(httpContext: null, routeName, values);
- }
-
- ///
- /// Gets a 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 api.
- ///
- /// The associated with current request.
- ///
- /// An object that contains route values. These values are used to lookup for endpoint(s).
- ///
- ///
- /// If an endpoint(s) was found successfully, then this returns a template object representing that,
- /// null otherwise.
- ///
- public LinkGenerationTemplate GetTemplate(HttpContext httpContext, object values)
- {
- return GetTemplate(httpContext, routeName: null, values);
- }
-
- ///
- /// Gets a 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 api.
- ///
- /// The associated with current request.
- /// The name of the route to generate the URL to.
- ///
- /// An object that contains route values. These values are used to lookup for endpoint(s).
- ///
- ///
- /// If an endpoint(s) was found successfully, then this returns a template object representing that,
- /// null otherwise.
- ///
- public abstract LinkGenerationTemplate GetTemplate(HttpContext httpContext, string routeName, object values);
-
- ///
- /// Gets a 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 api.
- /// The lookup information is used to find endpoints using a registered 'IEndpointFinder<TAddress>'.
- ///
- /// The address type to look up endpoints.
- /// The information used to look up endpoints for creating a template.
- ///
- /// If an endpoint(s) was found successfully, then this returns a template object representing that,
- /// null otherwise.
- ///
- public LinkGenerationTemplate GetTemplateByAddress(TAddress address)
- {
- return GetTemplateByAddress(httpContext: null, address);
- }
-
- ///
- /// Gets a 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 api.
- /// The lookup information is used to find endpoints using a registered 'IEndpointFinder<TAddress>'.
- ///
- /// The address type to look up endpoints.
- /// The information used to look up endpoints for creating a template.
- /// The associated with current request.
- ///
- /// If an endpoint(s) was found successfully, then this returns a template object representing that,
- /// null otherwise.
- ///
- public abstract LinkGenerationTemplate GetTemplateByAddress(
- HttpContext httpContext,
- TAddress address);
+ public abstract LinkGenerationTemplate GetTemplateByAddress(TAddress address);
}
}
diff --git a/src/Microsoft.AspNetCore.Routing/CompositeEndpointDataSource.cs b/src/Microsoft.AspNetCore.Routing/CompositeEndpointDataSource.cs
index 991a324afb..36794aeacc 100644
--- a/src/Microsoft.AspNetCore.Routing/CompositeEndpointDataSource.cs
+++ b/src/Microsoft.AspNetCore.Routing/CompositeEndpointDataSource.cs
@@ -142,7 +142,7 @@ namespace Microsoft.AspNetCore.Routing
sb.Append(" }");
var routeValuesAddressMetadata = routeEndpoint.Metadata.GetMetadata();
sb.Append(", Route Name: ");
- sb.Append(routeValuesAddressMetadata?.Name);
+ sb.Append(routeValuesAddressMetadata?.RouteName);
if (routeValuesAddressMetadata?.RequiredValues != null)
{
sb.Append(", Required Values: new { ");
diff --git a/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerationTemplate.cs b/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerationTemplate.cs
index 01d4c54267..b1beb67b1b 100644
--- a/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerationTemplate.cs
+++ b/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerationTemplate.cs
@@ -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 endpoints,
- HttpContext httpContext,
- RouteValueDictionary explicitValues,
- RouteValueDictionary ambientValues)
+ public DefaultLinkGenerationTemplate(DefaultLinkGenerator linkGenerator, List endpoints)
{
LinkGenerator = linkGenerator;
Endpoints = endpoints;
- HttpContext = httpContext;
- EarlierExplicitValues = explicitValues;
- AmbientValues = ambientValues;
}
- internal DefaultLinkGenerator LinkGenerator { get; }
+ public DefaultLinkGenerator LinkGenerator { get; }
- internal IEnumerable Endpoints { get; }
+ public List 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);
}
}
}
diff --git a/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerator.cs b/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerator.cs
index b60f25eb25..df52926a02 100644
--- a/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerator.cs
+++ b/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerator.cs
@@ -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 _uriBuildingContextPool;
private readonly ILogger _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(
+ public override string GetPathByAddress(
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 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(
+ public override string GetUriByAddress(
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 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 address)
+ {
+ var endpoints = GetEndpoints(address);
+ if (endpoints.Count == 0)
+ {
+ return null;
+ }
+
+ return new DefaultLinkGenerationTemplate(this, endpoints);
+ }
+
+ private List GetEndpoints(TAddress address)
+ {
+ var addressingScheme = _serviceProvider.GetRequiredService>();
+ return addressingScheme.FindEndpoints(address).OfType().ToList();
+ }
+
+ // Also called from DefaultLinkGenerationTemplate
+ public string GetPathByEndpoints(
+ List 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 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(
- 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(
- 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(
- 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();
- if (feature != null)
- {
- return feature.RouteValues;
- }
- }
- return new RouteValueDictionary();
- }
-
- private IEnumerable FindEndpoints(TAddress address)
- {
- var finder = _serviceProvider.GetRequiredService>();
- var endpoints = finder.FindEndpoints(address);
- if (endpoints == null)
- {
- return null;
- }
-
- var routeEndpoints = endpoints.OfType();
- if (!routeEndpoints.Any())
- {
- return null;
- }
-
- return routeEndpoints;
+ return httpContext?.Features.Get()?.RouteValues;
}
}
}
diff --git a/src/Microsoft.AspNetCore.Routing/IRouteValuesAddressMetadata.cs b/src/Microsoft.AspNetCore.Routing/IRouteValuesAddressMetadata.cs
index 0d60e5db92..1e771c27d8 100644
--- a/src/Microsoft.AspNetCore.Routing/IRouteValuesAddressMetadata.cs
+++ b/src/Microsoft.AspNetCore.Routing/IRouteValuesAddressMetadata.cs
@@ -7,7 +7,7 @@ namespace Microsoft.AspNetCore.Routing
{
public interface IRouteValuesAddressMetadata
{
- string Name { get; }
+ string RouteName { get; }
IReadOnlyDictionary RequiredValues { get; }
}
}
diff --git a/src/Microsoft.AspNetCore.Routing/Internal/UriBuildingContext.cs b/src/Microsoft.AspNetCore.Routing/Internal/UriBuildingContext.cs
index 3b7c3b743d..2b5c506af6 100644
--- a/src/Microsoft.AspNetCore.Routing/Internal/UriBuildingContext.cs
+++ b/src/Microsoft.AspNetCore.Routing/Internal/UriBuildingContext.cs
@@ -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 _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();
- 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));
}
}
}
diff --git a/src/Microsoft.AspNetCore.Routing/LinkGeneratorRouteValuesAddressExtensions.cs b/src/Microsoft.AspNetCore.Routing/LinkGeneratorRouteValuesAddressExtensions.cs
new file mode 100644
index 0000000000..0e2f8a663c
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Routing/LinkGeneratorRouteValuesAddressExtensions.cs
@@ -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
+{
+ ///
+ /// Extension methods for using with .
+ ///
+ 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(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(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(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(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,
+ };
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Routing/RouteValuesAddressMetadata.cs b/src/Microsoft.AspNetCore.Routing/RouteValuesAddressMetadata.cs
index 6d110a70d6..a65c48101a 100644
--- a/src/Microsoft.AspNetCore.Routing/RouteValuesAddressMetadata.cs
+++ b/src/Microsoft.AspNetCore.Routing/RouteValuesAddressMetadata.cs
@@ -11,19 +11,19 @@ namespace Microsoft.AspNetCore.Routing
[DebuggerDisplay("{DebuggerToString(),nq}")]
public sealed class RouteValuesAddressMetadata : IRouteValuesAddressMetadata
{
- public RouteValuesAddressMetadata(string name, IReadOnlyDictionary requiredValues)
+ public RouteValuesAddressMetadata(string routeName, IReadOnlyDictionary requiredValues)
{
- Name = name;
+ RouteName = routeName;
RequiredValues = requiredValues;
}
- public string Name { get; }
+ public string RouteName { get; }
public IReadOnlyDictionary RequiredValues { get; }
internal string DebuggerToString()
{
- return $"Name: {Name} - Required values: {string.Join(", ", FormatValues(RequiredValues))}";
+ return $"Name: {RouteName} - Required values: {string.Join(", ", FormatValues(RequiredValues))}";
IEnumerable FormatValues(IEnumerable> values)
{
diff --git a/src/Microsoft.AspNetCore.Routing/RouteValuesBasedEndpointFinder.cs b/src/Microsoft.AspNetCore.Routing/RouteValuesBasedEndpointFinder.cs
index a55e1b7274..3ce63cea56 100644
--- a/src/Microsoft.AspNetCore.Routing/RouteValuesBasedEndpointFinder.cs
+++ b/src/Microsoft.AspNetCore.Routing/RouteValuesBasedEndpointFinder.cs
@@ -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;
diff --git a/src/Microsoft.AspNetCore.Routing/Template/TemplateBinder.cs b/src/Microsoft.AspNetCore.Routing/Template/TemplateBinder.cs
index c96f540133..7579e27232 100644
--- a/src/Microsoft.AspNetCore.Routing/Template/TemplateBinder.cs
+++ b/src/Microsoft.AspNetCore.Routing/Template/TemplateBinder.cs
@@ -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;
diff --git a/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGenerationTemplateTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGenerationTemplateTest.cs
new file mode 100644
index 0000000000..f76eec2f75
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGenerationTemplateTest.cs
@@ -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() { 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() { 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() { 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() { 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() { 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() { 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);
+ }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorProcessTemplateTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorProcessTemplateTest.cs
new file mode 100644
index 0000000000..ea202ff786
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorProcessTemplateTest.cs
@@ -0,0 +1,1503 @@
+// 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 System.Linq;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing.Constraints;
+using Microsoft.AspNetCore.Routing.TestObjects;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing
+{
+ // Detailed coverage for how DefaultLinkGenerator processes templates
+ public class DefaultLinkGeneratorProcessTemplateTest : LinkGeneratorTestBase
+ {
+ [Fact]
+ public void TryProcessTemplate_EncodesIntermediate_DefaultValues()
+ {
+ // Arrange
+ var endpoint = EndpointFactory.CreateRouteEndpoint("{p1}/{p2=a b}/{p3=foo}");
+ var linkGenerator = CreateLinkGenerator(endpoint);
+
+ // Act
+ var success = linkGenerator.TryProcessTemplate(
+ httpContext: null,
+ endpoint: endpoint,
+ ambientValues: null,
+ explicitValues: new RouteValueDictionary(new { p1 = "Home", p3 = "bar", }),
+ options: null,
+ out var result);
+
+ // Assert
+ Assert.True(success);
+ Assert.Equal("/Home/a%20b/bar", result.path.ToUriComponent());
+ Assert.Equal(string.Empty, result.query.ToUriComponent());
+ }
+
+ [Theory]
+ [InlineData("a/b/c", "/Home/Index/a%2Fb%2Fc")]
+ [InlineData("a/b b1/c c1", "/Home/Index/a%2Fb%20b1%2Fc%20c1")]
+ public void TryProcessTemplate_EncodesValue_OfSingleAsteriskCatchAllParameter(string routeValue, string expected)
+ {
+ // Arrange
+ var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{*path}");
+ var linkGenerator = CreateLinkGenerator(endpoint);
+ var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Index" });
+
+ // Act
+ var success = linkGenerator.TryProcessTemplate(
+ httpContext: httpContext,
+ endpoint: endpoint,
+ ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext),
+ explicitValues: new RouteValueDictionary(new { path = routeValue, }),
+ options: null,
+ out var result);
+
+ // Assert
+ Assert.True(success);
+ Assert.Equal(expected, result.path.ToUriComponent());
+ Assert.Equal(string.Empty, result.query.ToUriComponent());
+ }
+
+ [Theory]
+ [InlineData("/", "/Home/Index//")]
+ [InlineData("a", "/Home/Index/a")]
+ [InlineData("a/", "/Home/Index/a/")]
+ [InlineData("a/b", "/Home/Index/a/b")]
+ [InlineData("a/b/c", "/Home/Index/a/b/c")]
+ [InlineData("a/b/cc", "/Home/Index/a/b/cc")]
+ [InlineData("a/b/c/", "/Home/Index/a/b/c/")]
+ [InlineData("a/b/c//", "/Home/Index/a/b/c//")]
+ [InlineData("a//b//c", "/Home/Index/a//b//c")]
+ public void TryProcessTemplate_DoesNotEncodeSlashes_OfDoubleAsteriskCatchAllParameter(string routeValue, string expected)
+ {
+ // Arrange
+ var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{**path}");
+ var linkGenerator = CreateLinkGenerator(endpoint);
+ var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Index" });
+
+ // Act
+ var success = linkGenerator.TryProcessTemplate(
+ httpContext: httpContext,
+ endpoint: endpoint,
+ ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext),
+ explicitValues: new RouteValueDictionary(new { path = routeValue, }),
+ options: null,
+ out var result);
+
+ // Assert
+ Assert.True(success);
+ Assert.Equal(expected, result.path.ToUriComponent());
+ Assert.Equal(string.Empty, result.query.ToUriComponent());
+ }
+
+ [Fact]
+ public void TryProcessTemplate_EncodesContentOtherThanSlashes_OfDoubleAsteriskCatchAllParameter()
+ {
+ // Arrange
+ var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{**path}");
+ var linkGenerator = CreateLinkGenerator(endpoint);
+ var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Index" });
+
+ // Act
+ var success = linkGenerator.TryProcessTemplate(
+ httpContext: httpContext,
+ endpoint: endpoint,
+ ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext),
+ explicitValues: new RouteValueDictionary(new { path = "a/b b1/c c1" }),
+ options: null,
+ out var result);
+
+ // Assert
+ Assert.True(success);
+ Assert.Equal("/Home/Index/a/b%20b1/c%20c1", result.path.ToUriComponent());
+ Assert.Equal(string.Empty, result.query.ToUriComponent());
+ }
+
+ [Fact]
+ public void TryProcessTemplate_EncodesValues()
+ {
+ // Arrange
+ var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}");
+ var linkGenerator = CreateLinkGenerator(endpoint);
+ var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Index" });
+
+ // Act
+ var success = linkGenerator.TryProcessTemplate(
+ httpContext: httpContext,
+ endpoint: endpoint,
+ ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext),
+ explicitValues: new RouteValueDictionary(new { name = "name with %special #characters" }),
+ options: null,
+ out var result);
+
+ // Assert
+ Assert.True(success);
+ Assert.Equal("/Home/Index", result.path.ToUriComponent());
+ Assert.Equal("?name=name%20with%20%25special%20%23characters", result.query.ToUriComponent());
+ }
+
+ [Fact]
+ public void TryProcessTemplate_ForListOfStrings()
+ {
+ // Arrange
+ var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}");
+ var linkGenerator = CreateLinkGenerator(endpoint);
+ var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Index" });
+
+ // Act
+ var success = linkGenerator.TryProcessTemplate(
+ httpContext: httpContext,
+ endpoint: endpoint,
+ ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext),
+ explicitValues: new RouteValueDictionary(new { color = new List { "red", "green", "blue" } }),
+ options: null,
+ out var result);
+
+ // Assert
+ Assert.True(success);
+ Assert.Equal("/Home/Index", result.path.ToUriComponent());
+ Assert.Equal("?color=red&color=green&color=blue", result.query.ToUriComponent());
+ }
+
+ [Fact]
+ public void TryProcessTemplate_ForListOfInts()
+ {
+ // Arrange
+ var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}");
+ var linkGenerator = CreateLinkGenerator(endpoint);
+ var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Index" });
+
+ // Act
+ var success = linkGenerator.TryProcessTemplate(
+ httpContext: httpContext,
+ endpoint: endpoint,
+ ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext),
+ explicitValues: new RouteValueDictionary(new { items = new List { 10, 20, 30 } }),
+ options: null,
+ out var result);
+
+ // Assert
+ Assert.True(success);
+ Assert.Equal("/Home/Index", result.path.ToUriComponent());
+ Assert.Equal("?items=10&items=20&items=30", result.query.ToUriComponent());
+ }
+
+ [Fact]
+ public void TryProcessTemplate_ForList_Empty()
+ {
+ // Arrange
+ var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}");
+ var linkGenerator = CreateLinkGenerator(endpoint);
+ var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Index" });
+
+ // Act
+ var success = linkGenerator.TryProcessTemplate(
+ httpContext: httpContext,
+ endpoint: endpoint,
+ ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext),
+ explicitValues: new RouteValueDictionary(new { color = new List { } }),
+ options: null,
+ out var result);
+
+ // Assert
+ Assert.True(success);
+ Assert.Equal("/Home/Index", result.path.ToUriComponent());
+ Assert.Equal(string.Empty, result.query.ToUriComponent());
+ }
+
+ [Fact]
+ public void TryProcessTemplate_ForList_StringWorkaround()
+ {
+ // Arrange
+ var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}");
+ var linkGenerator = CreateLinkGenerator(endpoint);
+ var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Index" });
+
+ // Act
+ var success = linkGenerator.TryProcessTemplate(
+ httpContext: httpContext,
+ endpoint: endpoint,
+ ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext),
+ explicitValues: new RouteValueDictionary(new { page = 1, color = new List { "red", "green", "blue" }, message = "textfortest" }),
+ options: null,
+ out var result);
+
+ // Assert
+ Assert.True(success);
+ Assert.Equal("/Home/Index", result.path.ToUriComponent());
+ Assert.Equal("?page=1&color=red&color=green&color=blue&message=textfortest", result.query.ToUriComponent());
+ }
+
+ [Fact]
+ public void TryProcessTemplate_Success_AmbientValues()
+ {
+ // Arrange
+ var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}");
+ var linkGenerator = CreateLinkGenerator(endpoint);
+ var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" });
+
+ // Act
+ var success = linkGenerator.TryProcessTemplate(
+ httpContext: httpContext,
+ endpoint: endpoint,
+ ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext),
+ explicitValues: new RouteValueDictionary(new { action = "Index" }),
+ options: null,
+ out var result);
+
+ // Assert
+ Assert.True(success);
+ Assert.Equal("/Home/Index", result.path.ToUriComponent());
+ Assert.Equal(string.Empty, result.query.ToUriComponent());
+ }
+
+ [Fact]
+ public void TryProcessTemplate_GeneratesLowercaseUrl_SetOnRouteOptions()
+ {
+ // Arrange
+ var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}");
+ var linkGenerator = CreateLinkGenerator(new RouteOptions() { LowercaseUrls = true }, endpoints: new[] { endpoint, });
+ var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" });
+
+ // Act
+ var success = linkGenerator.TryProcessTemplate(
+ httpContext: httpContext,
+ endpoint: endpoint,
+ ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext),
+ explicitValues: new RouteValueDictionary(new { action = "Index" }),
+ options: null,
+ out var result);
+
+ // Assert
+ Assert.True(success);
+ Assert.Equal("/home/index", result.path.ToUriComponent());
+ Assert.Equal(string.Empty, result.query.ToUriComponent());
+ }
+
+ [Fact]
+ public void TryProcessTemplate_GeneratesLowercaseQueryString_SetOnRouteOptions()
+ {
+ // Arrange
+ var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}");
+ var linkGenerator = CreateLinkGenerator(
+ new RouteOptions() { LowercaseUrls = true, LowercaseQueryStrings = true },
+ endpoints: new[] { endpoint, });
+ var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" });
+
+ // Act
+ var success = linkGenerator.TryProcessTemplate(
+ httpContext: httpContext,
+ endpoint: endpoint,
+ ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext),
+ explicitValues: new RouteValueDictionary(new { action = "Index", ShowStatus = "True", INFO = "DETAILED" }),
+ options: null,
+ out var result);
+
+ // Assert
+ Assert.True(success);
+ Assert.Equal("/home/index", result.path.ToUriComponent());
+ Assert.Equal("?showstatus=true&info=detailed", result.query.ToUriComponent());
+ }
+
+ [Fact]
+ public void TryProcessTemplate_AppendsTrailingSlash_SetOnRouteOptions()
+ {
+ // Arrange
+ var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}");
+ var linkGenerator = CreateLinkGenerator(
+ new RouteOptions() { AppendTrailingSlash = true },
+ endpoints: new[] { endpoint });
+ var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" });
+
+ // Act
+ var success = linkGenerator.TryProcessTemplate(
+ httpContext: httpContext,
+ endpoint: endpoint,
+ ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext),
+ explicitValues: new RouteValueDictionary(new { action = "Index" }),
+ options: null,
+ out var result);
+
+ // Assert
+ Assert.True(success);
+ Assert.Equal("/Home/Index/", result.path.ToUriComponent());
+ Assert.Equal(string.Empty, result.query.ToUriComponent());
+ }
+
+ [Fact]
+ public void TryProcessTemplate_GeneratesLowercaseQueryStringAndTrailingSlash_SetOnRouteOptions()
+ {
+ // Arrange
+ var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}");
+ var linkGenerator = CreateLinkGenerator(
+ new RouteOptions() { LowercaseUrls = true, LowercaseQueryStrings = true, AppendTrailingSlash = true },
+ endpoints: new[] { endpoint });
+ var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" });
+
+ // Act
+ var success = linkGenerator.TryProcessTemplate(
+ httpContext: httpContext,
+ endpoint: endpoint,
+ ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext),
+ explicitValues: new RouteValueDictionary(new { action = "Index", ShowStatus = "True", INFO = "DETAILED" }),
+ options: null,
+ out var result);
+
+ // Assert
+ Assert.True(success);
+ Assert.Equal("/home/index/", result.path.ToUriComponent());
+ Assert.Equal("?showstatus=true&info=detailed", result.query.ToUriComponent());
+ }
+
+ [Fact]
+ public void TryProcessTemplate_LowercaseUrlSetToTrue_OnRouteOptions_OverridenByCallsiteValue()
+ {
+ // Arrange
+ var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}");
+ var linkGenerator = CreateLinkGenerator(
+ new RouteOptions() { LowercaseUrls = true },
+ endpoints: new[] { endpoint });
+ var httpContext = CreateHttpContext(ambientValues: new { controller = "HoMe" });
+
+ // Act
+ var success = linkGenerator.TryProcessTemplate(
+ httpContext: httpContext,
+ endpoint: endpoint,
+ ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext),
+ explicitValues: new RouteValueDictionary(new { action = "InDex" }),
+ options: new LinkOptions
+ {
+ LowercaseUrls = false
+ },
+ out var result);
+
+
+ // Assert
+ Assert.True(success);
+ Assert.Equal("/HoMe/InDex", result.path.ToUriComponent());
+ Assert.Equal(string.Empty, result.query.ToUriComponent());
+ }
+
+ [Fact]
+ public void TryProcessTemplate_LowercaseUrlSetToFalse_OnRouteOptions_OverridenByCallsiteValue()
+ {
+ // Arrange
+ var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}");
+ var linkGenerator = CreateLinkGenerator(
+ new RouteOptions() { LowercaseUrls = false },
+ endpoints: new[] { endpoint });
+ var httpContext = CreateHttpContext(ambientValues: new { controller = "HoMe" });
+
+ // Act
+ var success = linkGenerator.TryProcessTemplate(
+ httpContext: httpContext,
+ endpoint: endpoint,
+ ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext),
+ explicitValues: new RouteValueDictionary(new { action = "InDex" }),
+ options: new LinkOptions()
+ {
+ LowercaseUrls = true
+ },
+ out var result);
+
+ // Assert
+ Assert.True(success);
+ Assert.Equal("/home/index", result.path.ToUriComponent());
+ Assert.Equal(string.Empty, result.query.ToUriComponent());
+ }
+
+ [Fact]
+ public void TryProcessTemplate_LowercaseUrlQueryStringsSetToTrue_OnRouteOptions_OverridenByCallsiteValue()
+ {
+ // Arrange
+ var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}");
+ var linkGenerator = CreateLinkGenerator(
+ new RouteOptions() { LowercaseUrls = true, LowercaseQueryStrings = true },
+ endpoints: new[] { endpoint });
+ var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" });
+
+ // Act
+ var success = linkGenerator.TryProcessTemplate(
+ httpContext: httpContext,
+ endpoint: endpoint,
+ ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext),
+ explicitValues: new RouteValueDictionary(new { action = "Index", ShowStatus = "True", INFO = "DETAILED" }),
+ options: new LinkOptions
+ {
+ LowercaseUrls = false,
+ LowercaseQueryStrings = false
+ },
+ out var result);
+
+ // Assert
+ Assert.True(success);
+ Assert.Equal("/Home/Index", result.path.ToUriComponent());
+ Assert.Equal("?ShowStatus=True&INFO=DETAILED", result.query.ToUriComponent());
+ }
+
+ [Fact]
+ public void TryProcessTemplate_LowercaseUrlQueryStringsSetToFalse_OnRouteOptions_OverridenByCallsiteValue()
+ {
+ // Arrange
+ var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}");
+ var linkGenerator = CreateLinkGenerator(
+ new RouteOptions() { LowercaseUrls = false, LowercaseQueryStrings = false },
+ endpoints: new[] { endpoint });
+ var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" });
+
+ // Act
+ var success = linkGenerator.TryProcessTemplate(
+ httpContext: httpContext,
+ endpoint: endpoint,
+ ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext),
+ explicitValues: new RouteValueDictionary(new { action = "Index", ShowStatus = "True", INFO = "DETAILED" }),
+ options: new LinkOptions()
+ {
+ LowercaseUrls = true,
+ LowercaseQueryStrings = true,
+ },
+ out var result);
+
+ // Assert
+ Assert.True(success);
+ Assert.Equal("/home/index", result.path.ToUriComponent());
+ Assert.Equal("?showstatus=true&info=detailed", result.query.ToUriComponent());
+ }
+
+ [Fact]
+ public void TryProcessTemplate_AppendTrailingSlashSetToFalse_OnRouteOptions_OverridenByCallsiteValue()
+ {
+ // Arrange
+ var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}");
+ var linkGenerator = CreateLinkGenerator(
+ new RouteOptions() { AppendTrailingSlash = false },
+ endpoints: new[] { endpoint });
+ var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" });
+
+ // Act
+ var success = linkGenerator.TryProcessTemplate(
+ httpContext: httpContext,
+ endpoint: endpoint,
+ ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext),
+ explicitValues: new RouteValueDictionary(new { action = "Index" }),
+ options: new LinkOptions() { AppendTrailingSlash = true, },
+ out var result);
+
+ // Assert
+ Assert.True(success);
+ Assert.Equal("/Home/Index/", result.path.ToUriComponent());
+ Assert.Equal(string.Empty, result.query.ToUriComponent());
+ }
+
+ [Fact]
+ public void RouteGenerationRejectsConstraints()
+ {
+ // Arrange
+ var endpoint = EndpointFactory.CreateRouteEndpoint(
+ "{p1}/{p2}",
+ defaults: new { p2 = "catchall" },
+ constraints: new { p2 = "\\d{4}" });
+ var linkGenerator = CreateLinkGenerator(endpoint);
+ var httpContext = CreateHttpContext(ambientValues: new { });
+
+ // Act
+ var success = linkGenerator.TryProcessTemplate(
+ httpContext: httpContext,
+ endpoint: endpoint,
+ ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext),
+ explicitValues: new RouteValueDictionary(new { p1 = "abcd" }),
+ options: null,
+ out var result);
+
+ // Assert
+ Assert.False(success);
+ }
+
+ [Fact]
+ public void RouteGenerationAcceptsConstraints()
+ {
+ // Arrange
+ var endpoint = EndpointFactory.CreateRouteEndpoint(
+ "{p1}/{p2}",
+ defaults: new { p2 = "catchall" },
+ constraints: new { p2 = new RegexRouteConstraint("\\d{4}"), });
+ var linkGenerator = CreateLinkGenerator(endpoint);
+ var httpContext = CreateHttpContext(ambientValues: new { });
+
+ // Act
+ var success = linkGenerator.TryProcessTemplate(
+ httpContext: httpContext,
+ endpoint: endpoint,
+ ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext),
+ explicitValues: new RouteValueDictionary(new { p1 = "hello", p2 = "1234" }),
+ options: null,
+ out var result);
+
+ // Assert
+ Assert.True(success);
+ Assert.Equal("/hello/1234", result.path.ToUriComponent());
+ Assert.Equal(string.Empty, result.query.ToUriComponent());
+ }
+
+ [Fact]
+ public void RouteWithCatchAllRejectsConstraints()
+ {
+ // Arrange
+ var endpoint = EndpointFactory.CreateRouteEndpoint(
+ "{p1}/{*p2}",
+ defaults: new { p2 = "catchall" },
+ constraints: new { p2 = new RegexRouteConstraint("\\d{4}") });
+ var linkGenerator = CreateLinkGenerator(endpoint);
+ var httpContext = CreateHttpContext(ambientValues: new { });
+
+ // Act
+ var success = linkGenerator.TryProcessTemplate(
+ httpContext: httpContext,
+ endpoint: endpoint,
+ ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext),
+ explicitValues: new RouteValueDictionary(new { p1 = "abcd" }),
+ options: null,
+ out var result);
+
+ // Assert
+ Assert.False(success);
+ }
+
+ [Fact]
+ public void RouteWithCatchAllAcceptsConstraints()
+ {
+ // Arrange
+ var endpoint = EndpointFactory.CreateRouteEndpoint(
+ "{p1}/{*p2}",
+ defaults: new { p2 = "catchall" },
+ constraints: new { p2 = new RegexRouteConstraint("\\d{4}") });
+ var linkGenerator = CreateLinkGenerator(endpoint);
+ var httpContext = CreateHttpContext(ambientValues: new { });
+
+ // Act
+ var success = linkGenerator.TryProcessTemplate(
+ httpContext: httpContext,
+ endpoint: endpoint,
+ ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext),
+ explicitValues: new RouteValueDictionary(new { p1 = "hello", p2 = "1234" }),
+ options: null,
+ out var result);
+
+ // Assert
+ Assert.True(success);
+ Assert.Equal("/hello/1234", result.path.ToUriComponent());
+ Assert.Equal(string.Empty, result.query.ToUriComponent());
+ }
+
+ [Fact]
+ public void GetLinkWithNonParameterConstraintReturnsUrlWithoutQueryString()
+ {
+ // Arrange
+ var target = new Mock();
+ target
+ .Setup(
+ e => e.Match(
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny()))
+ .Returns(true)
+ .Verifiable();
+ var endpoint = EndpointFactory.CreateRouteEndpoint(
+ "{p1}/{p2}",
+ defaults: new { p2 = "catchall" },
+ constraints: new { p2 = target.Object });
+ var linkGenerator = CreateLinkGenerator(endpoint);
+ var httpContext = CreateHttpContext(ambientValues: new { });
+
+ // Act
+ var success = linkGenerator.TryProcessTemplate(
+ httpContext: httpContext,
+ endpoint: endpoint,
+ ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext),
+ explicitValues: new RouteValueDictionary(new { p1 = "hello", p2 = "1234" }),
+ options: null,
+ out var result);
+
+ // Assert
+ Assert.True(success);
+ Assert.Equal("/hello/1234", result.path.ToUriComponent());
+ Assert.Equal(string.Empty, result.query.ToUriComponent());
+
+ target.VerifyAll();
+ }
+
+
+ // Any ambient values from the current request should be visible to constraint, even
+ // if they have nothing to do with the route generating a link
+ [Fact]
+ public void TryProcessTemplate_ConstraintsSeeAmbientValues()
+ {
+ // Arrange
+ var constraint = new CapturingConstraint();
+ var endpoint = EndpointFactory.CreateRouteEndpoint(
+ template: "slug/Home/Store",
+ defaults: new { controller = "Home", action = "Store" },
+ constraints: new { c = constraint });
+ var linkGenerator = CreateLinkGenerator(endpoint);
+ var httpContext = CreateHttpContext(
+ ambientValues: new { controller = "Home", action = "Blog", extra = "42" });
+ var expectedValues = new RouteValueDictionary(
+ new { controller = "Home", action = "Store", extra = "42" });
+
+ // Act
+ var success = linkGenerator.TryProcessTemplate(
+ httpContext: httpContext,
+ endpoint: endpoint,
+ ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext),
+ explicitValues: new RouteValueDictionary(new { action = "Store" }),
+ options: null,
+ out var result);
+
+ // Assert
+ Assert.True(success);
+ Assert.Equal("/slug/Home/Store", result.path.ToUriComponent());
+ Assert.Equal(string.Empty, result.query.ToUriComponent());
+
+ Assert.Equal(expectedValues.OrderBy(kvp => kvp.Key), constraint.Values.OrderBy(kvp => kvp.Key));
+ }
+
+ // Non-parameter default values from the routing generating a link are not in the 'values'
+ // collection when constraints are processed.
+ [Fact]
+ public void TryProcessTemplate_ConstraintsDontSeeDefaults_WhenTheyArentParameters()
+ {
+ // Arrange
+ var constraint = new CapturingConstraint();
+ var endpoint = EndpointFactory.CreateRouteEndpoint(
+ template: "slug/Home/Store",
+ defaults: new { controller = "Home", action = "Store", otherthing = "17" },
+ constraints: new { c = constraint });
+ var linkGenerator = CreateLinkGenerator(endpoint);
+ var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Blog" });
+ var expectedValues = new RouteValueDictionary(
+ new { controller = "Home", action = "Store" });
+
+ // Act
+ var success = linkGenerator.TryProcessTemplate(
+ httpContext: httpContext,
+ endpoint: endpoint,
+ ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext),
+ explicitValues: new RouteValueDictionary(new { action = "Store" }),
+ options: null,
+ out var result);
+
+ // Assert
+ Assert.True(success);
+ Assert.Equal("/slug/Home/Store", result.path.ToUriComponent());
+ Assert.Equal(string.Empty, result.query.ToUriComponent());
+
+ Assert.Equal(expectedValues.OrderBy(kvp => kvp.Key), constraint.Values.OrderBy(kvp => kvp.Key));
+ }
+
+ // Default values are visible to the constraint when they are used to fill a parameter.
+ [Fact]
+ public void TryProcessTemplate_ConstraintsSeesDefault_WhenThereItsAParamter()
+ {
+ // Arrange
+ var constraint = new CapturingConstraint();
+ var endpoint = EndpointFactory.CreateRouteEndpoint(
+ template: "slug/{controller}/{action}",
+ defaults: new { action = "Index" },
+ constraints: new { c = constraint, });
+ var linkGenerator = CreateLinkGenerator(endpoint);
+ var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Blog" });
+ var expectedValues = new RouteValueDictionary(
+ new { controller = "Shopping", action = "Index" });
+
+ // Act
+ var success = linkGenerator.TryProcessTemplate(
+ httpContext: httpContext,
+ endpoint: endpoint,
+ ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext),
+ explicitValues: new RouteValueDictionary(new { controller = "Shopping" }),
+ options: null,
+ out var result);
+
+ // Assert
+ Assert.True(success);
+ Assert.Equal("/slug/Shopping", result.path.ToUriComponent());
+ Assert.Equal(string.Empty, result.query.ToUriComponent());
+ Assert.Equal(expectedValues, constraint.Values);
+ }
+
+ // Default values from the routing generating a link are in the 'values' collection when
+ // constraints are processed - IFF they are specified as values or ambient values.
+ [Fact]
+ public void TryProcessTemplate_ConstraintsSeeDefaults_IfTheyAreSpecifiedOrAmbient()
+ {
+ // Arrange
+ var constraint = new CapturingConstraint();
+ var endpoint = EndpointFactory.CreateRouteEndpoint(
+ template: "slug/Home/Store",
+ defaults: new { controller = "Home", action = "Store", otherthing = "17", thirdthing = "13" },
+ constraints: new { c = constraint, });
+ var linkGenerator = CreateLinkGenerator(endpoint);
+ var httpContext = CreateHttpContext(
+ ambientValues: new { controller = "Home", action = "Blog", otherthing = "17" });
+
+ var expectedValues = new RouteValueDictionary(
+ new { controller = "Home", action = "Store", otherthing = "17", thirdthing = "13" });
+
+ // Act
+ var success = linkGenerator.TryProcessTemplate(
+ httpContext: httpContext,
+ endpoint: endpoint,
+ ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext),
+ explicitValues: new RouteValueDictionary(new { action = "Store", thirdthing = "13" }),
+ options: null,
+ out var result);
+
+ // Assert
+ Assert.True(success);
+ Assert.Equal("/slug/Home/Store", result.path.ToUriComponent());
+ Assert.Equal(string.Empty, result.query.ToUriComponent());
+
+ Assert.Equal(expectedValues.OrderBy(kvp => kvp.Key), constraint.Values.OrderBy(kvp => kvp.Key));
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void TryProcessTemplate_InlineConstraints_Success(bool hasHttpContext)
+ {
+ // Arrange
+ var endpoint = EndpointFactory.CreateRouteEndpoint(
+ template: "Home/Index/{id:int}",
+ defaults: new { controller = "Home", action = "Index" },
+ constraints: new { });
+ var linkGenerator = CreateLinkGenerator(endpoint);
+ var httpContext = hasHttpContext ? CreateHttpContext(new { }) : null;
+
+ // Act
+ var success = linkGenerator.TryProcessTemplate(
+ httpContext: httpContext,
+ endpoint: endpoint,
+ ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext),
+ explicitValues: new RouteValueDictionary(new { action = "Index", controller = "Home", id = 4 }),
+ options: null,
+ out var result);
+
+ // Assert
+ Assert.True(success);
+ Assert.Equal("/Home/Index/4", result.path.ToUriComponent());
+ Assert.Equal(string.Empty, result.query.ToUriComponent());
+ }
+
+ [Fact]
+ public void TryProcessTemplate_InlineConstraints_NonMatchingvalue()
+ {
+ // Arrange
+ var endpoint = EndpointFactory.CreateRouteEndpoint(
+ template: "Home/Index/{id}",
+ defaults: new { controller = "Home", action = "Index" },
+ constraints: new { id = "int" });
+ var linkGenerator = CreateLinkGenerator(endpoint);
+ var httpContext = CreateHttpContext(ambientValues: new { });
+
+ // Act
+ var success = linkGenerator.TryProcessTemplate(
+ httpContext: httpContext,
+ endpoint: endpoint,
+ ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext),
+ explicitValues: new RouteValueDictionary(new { action = "Index", controller = "Home", id = "not-an-integer" }),
+ options: null,
+ out var result);
+
+ // Assert
+ Assert.False(success);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void TryProcessTemplate_InlineConstraints_OptionalParameter_ValuePresent(bool hasHttpContext)
+ {
+ // Arrange
+ var endpoint = EndpointFactory.CreateRouteEndpoint(
+ template: "Home/Index/{id:int?}",
+ defaults: new { controller = "Home", action = "Index" },
+ constraints: new { });
+ var linkGenerator = CreateLinkGenerator(endpoint);
+ var httpContext = hasHttpContext ? CreateHttpContext(new { }) : null;
+
+ // Act
+ var success = linkGenerator.TryProcessTemplate(
+ httpContext: httpContext,
+ endpoint: endpoint,
+ ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext),
+ explicitValues: new RouteValueDictionary(new { action = "Index", controller = "Home", id = 98 }),
+ options: null,
+ out var result);
+
+ // Assert
+ Assert.True(success);
+ Assert.Equal("/Home/Index/98", result.path.ToUriComponent());
+ Assert.Equal(string.Empty, result.query.ToUriComponent());
+ }
+
+ [Fact]
+ public void TryProcessTemplate_InlineConstraints_OptionalParameter_ValueNotPresent()
+ {
+ // Arrange
+ var endpoint = EndpointFactory.CreateRouteEndpoint(
+ template: "Home/Index/{id?}",
+ defaults: new { controller = "Home", action = "Index" },
+ constraints: new { id = "int" });
+ var linkGenerator = CreateLinkGenerator(endpoint);
+ var httpContext = CreateHttpContext(ambientValues: new { });
+
+ // Act
+ var success = linkGenerator.TryProcessTemplate(
+ httpContext: httpContext,
+ endpoint: endpoint,
+ ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext),
+ explicitValues: new RouteValueDictionary(new { action = "Index", controller = "Home" }),
+ options: null,
+ out var result);
+
+ // Assert
+ Assert.True(success);
+ Assert.Equal("/Home/Index", result.path.ToUriComponent());
+ Assert.Equal(string.Empty, result.query.ToUriComponent());
+ }
+
+ [Fact]
+ public void TryProcessTemplate_InlineConstraints_OptionalParameter_ValuePresent_ConstraintFails()
+ {
+ // Arrange
+ var endpoint = EndpointFactory.CreateRouteEndpoint(
+ template: "Home/Index/{id?}",
+ defaults: new { controller = "Home", action = "Index" },
+ constraints: new { id = "int" });
+ var linkGenerator = CreateLinkGenerator(endpoint);
+ var httpContext = CreateHttpContext(ambientValues: new { });
+
+ // Act
+ var success = linkGenerator.TryProcessTemplate(
+ httpContext: httpContext,
+ endpoint: endpoint,
+ ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext),
+ explicitValues: new RouteValueDictionary(new { action = "Index", controller = "Home", id = "not-an-integer" }),
+ options: null,
+ out var result);
+
+ // Assert
+ Assert.False(success);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void TryProcessTemplate_InlineConstraints_MultipleInlineConstraints(bool hasHttpContext)
+ {
+ // Arrange
+ var endpoint = EndpointFactory.CreateRouteEndpoint(
+ template: "Home/Index/{id:int:range(1,20)}",
+ defaults: new { controller = "Home", action = "Index" },
+ constraints: new { });
+ var linkGenerator = CreateLinkGenerator(endpoint);
+ var httpContext = hasHttpContext ? CreateHttpContext(new { }) : null;
+
+ // Act
+ var success = linkGenerator.TryProcessTemplate(
+ httpContext: httpContext,
+ endpoint: endpoint,
+ ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext),
+ explicitValues: new RouteValueDictionary(new { action = "Index", controller = "Home", id = 14 }),
+ options: null,
+ out var result);
+
+ // Assert
+ Assert.True(success);
+ Assert.Equal("/Home/Index/14", result.path.ToUriComponent());
+ Assert.Equal(string.Empty, result.query.ToUriComponent());
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void TryProcessTemplate_InlineConstraints_CompositeInlineConstraint_Fails(bool hasHttpContext)
+ {
+ // Arrange
+ var endpoint = EndpointFactory.CreateRouteEndpoint(
+ template: "Home/Index/{id:int:range(1,20)}",
+ defaults: new { controller = "Home", action = "Index" },
+ constraints: new { });
+ var linkGenerator = CreateLinkGenerator(endpoint);
+ var httpContext = hasHttpContext ? CreateHttpContext(new { }) : null;
+
+ // Act
+ var success = linkGenerator.TryProcessTemplate(
+ httpContext: httpContext,
+ endpoint: endpoint,
+ ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext),
+ explicitValues: new RouteValueDictionary(new { action = "Index", controller = "Home", id = 50 }),
+ options: null,
+ out var result);
+
+ // Assert
+ Assert.False(success);
+ }
+
+ [Fact]
+ public void TryProcessTemplate_InlineConstraints_CompositeConstraint_FromConstructor()
+ {
+ // Arrange
+ var constraint = new MaxLengthRouteConstraint(20);
+ var endpoint = EndpointFactory.CreateRouteEndpoint(
+ template: "Home/Index/{name}",
+ defaults: new { controller = "Home", action = "Index" },
+ constraints: new { name = constraint });
+ var linkGenerator = CreateLinkGenerator(endpoint);
+ var httpContext = CreateHttpContext(ambientValues: new { });
+
+ // Act
+ var success = linkGenerator.TryProcessTemplate(
+ httpContext: httpContext,
+ endpoint: endpoint,
+ ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext),
+ explicitValues: new RouteValueDictionary(new { action = "Index", controller = "Home", name = "products" }),
+ options: null,
+ out var result);
+
+ // Assert
+ Assert.True(success);
+ Assert.Equal("/Home/Index/products", result.path.ToUriComponent());
+ Assert.Equal(string.Empty, result.query.ToUriComponent());
+ }
+
+ [Fact]
+ public void TryProcessTemplate_OptionalParameter_ParameterPresentInValues()
+ {
+ // Arrange
+ var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{name?}");
+ var linkGenerator = CreateLinkGenerator(endpoint);
+ var httpContext = CreateHttpContext(ambientValues: new { });
+
+ // Act
+ var success = linkGenerator.TryProcessTemplate(
+ httpContext: httpContext,
+ endpoint: endpoint,
+ ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext),
+ explicitValues: new RouteValueDictionary(new { action = "Index", controller = "Home", name = "products" }),
+ options: null,
+ out var result);
+
+ // Assert
+ Assert.True(success);
+ Assert.Equal("/Home/Index/products", result.path.ToUriComponent());
+ Assert.Equal(string.Empty, result.query.ToUriComponent());
+ }
+
+ [Fact]
+ public void TryProcessTemplate_OptionalParameter_ParameterNotPresentInValues()
+ {
+ // Arrange
+ var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{name?}");
+ var linkGenerator = CreateLinkGenerator(endpoint);
+ var httpContext = CreateHttpContext(ambientValues: new { });
+
+ // Act
+ var success = linkGenerator.TryProcessTemplate(
+ httpContext: httpContext,
+ endpoint: endpoint,
+ ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext),
+ explicitValues: new RouteValueDictionary(new { action = "Index", controller = "Home" }),
+ options: null,
+ out var result);
+
+ // Assert
+ Assert.True(success);
+ Assert.Equal("/Home/Index", result.path.ToUriComponent());
+ Assert.Equal(string.Empty, result.query.ToUriComponent());
+ }
+
+ [Fact]
+ public void TryProcessTemplate_OptionalParameter_ParameterPresentInValuesAndDefaults()
+ {
+ // Arrange
+ var endpoint = EndpointFactory.CreateRouteEndpoint(
+ template: "{controller}/{action}/{name}",
+ defaults: new { name = "default-products" });
+ var linkGenerator = CreateLinkGenerator(endpoint);
+ var httpContext = CreateHttpContext(ambientValues: new { });
+
+ // Act
+ var success = linkGenerator.TryProcessTemplate(
+ httpContext: httpContext,
+ endpoint: endpoint,
+ ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext),
+ explicitValues: new RouteValueDictionary(new { action = "Index", controller = "Home", name = "products" }),
+ options: null,
+ out var result);
+
+ // Assert
+ Assert.True(success);
+ Assert.Equal("/Home/Index/products", result.path.ToUriComponent());
+ Assert.Equal(string.Empty, result.query.ToUriComponent());
+ }
+
+ [Fact]
+ public void TryProcessTemplate_OptionalParameter_ParameterNotPresentInValues_PresentInDefaults()
+ {
+ // Arrange
+ var endpoint = EndpointFactory.CreateRouteEndpoint(
+ template: "{controller}/{action}/{name}",
+ defaults: new { name = "products" });
+ var linkGenerator = CreateLinkGenerator(endpoint);
+ var httpContext = CreateHttpContext(ambientValues: new { });
+
+ // Act
+ var success = linkGenerator.TryProcessTemplate(
+ httpContext: httpContext,
+ endpoint: endpoint,
+ ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext),
+ explicitValues: new RouteValueDictionary(new { action = "Index", controller = "Home" }),
+ options: null,
+ out var result);
+
+ // Assert
+ Assert.True(success);
+ Assert.Equal("/Home/Index", result.path.ToUriComponent());
+ Assert.Equal(string.Empty, result.query.ToUriComponent());
+ }
+
+ [Fact]
+ public void TryProcessTemplate_ParameterNotPresentInTemplate_PresentInValues()
+ {
+ // Arrange
+ var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{name}");
+ var linkGenerator = CreateLinkGenerator(endpoint);
+ var httpContext = CreateHttpContext(ambientValues: new { });
+
+ // Act
+ var success = linkGenerator.TryProcessTemplate(
+ httpContext: httpContext,
+ endpoint: endpoint,
+ ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext),
+ explicitValues: new RouteValueDictionary(new { action = "Index", controller = "Home", name = "products", format = "json" }),
+ options: null,
+ out var result);
+
+ // Assert
+ Assert.True(success);
+ Assert.Equal("/Home/Index/products", result.path.ToUriComponent());
+ Assert.Equal("?format=json", result.query.ToUriComponent());
+ }
+
+ [Fact]
+ public void TryProcessTemplate_OptionalParameter_FollowedByDotAfterSlash_ParameterPresent()
+ {
+ // Arrange
+ var endpoint = EndpointFactory.CreateRouteEndpoint(
+ template: "{controller}/{action}/.{name?}");
+ var linkGenerator = CreateLinkGenerator(endpoint);
+ var httpContext = CreateHttpContext(ambientValues: new { });
+
+ // Act
+ var success = linkGenerator.TryProcessTemplate(
+ httpContext: httpContext,
+ endpoint: endpoint,
+ ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext),
+ explicitValues: new RouteValueDictionary(new { action = "Index", controller = "Home", name = "products" }),
+ options: null,
+ out var result);
+
+ // Assert
+ Assert.True(success);
+ Assert.Equal("/Home/Index/.products", result.path.ToUriComponent());
+ Assert.Equal(string.Empty, result.query.ToUriComponent());
+ }
+
+ [Fact]
+ public void TryProcessTemplate_OptionalParameter_FollowedByDotAfterSlash_ParameterNotPresent()
+ {
+ // Arrange
+ var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/.{name?}");
+ var linkGenerator = CreateLinkGenerator(endpoint);
+ var httpContext = CreateHttpContext(ambientValues: new { });
+
+ // Act
+ var success = linkGenerator.TryProcessTemplate(
+ httpContext: httpContext,
+ endpoint: endpoint,
+ ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext),
+ explicitValues: new RouteValueDictionary(new { action = "Index", controller = "Home" }),
+ options: null,
+ out var result);
+
+
+ Assert.True(success);
+ Assert.Equal("/Home/Index/", result.path.ToUriComponent());
+ Assert.Equal(string.Empty, result.query.ToUriComponent());
+ }
+
+ [Fact]
+ public void TryProcessTemplate_OptionalParameter_InSimpleSegment()
+ {
+ // Arrange
+ var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{name?}");
+ var linkGenerator = CreateLinkGenerator(endpoint);
+ var httpContext = CreateHttpContext(ambientValues: new { });
+
+ // Act
+ var success = linkGenerator.TryProcessTemplate(
+ httpContext: httpContext,
+ endpoint: endpoint,
+ ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext),
+ explicitValues: new RouteValueDictionary(new { action = "Index", controller = "Home" }),
+ options: null,
+ out var result);
+
+ // Assert
+ Assert.True(success);
+ Assert.Equal("/Home/Index", result.path.ToUriComponent());
+ Assert.Equal(string.Empty, result.query.ToUriComponent());
+ }
+
+ [Fact]
+ public void TryProcessTemplate_TwoOptionalParameters_OneValueFromAmbientValues()
+ {
+ // Arrange
+ var endpoint = EndpointFactory.CreateRouteEndpoint("a/{b=15}/{c?}/{d?}");
+ var linkGenerator = CreateLinkGenerator(endpoint);
+ var httpContext = CreateHttpContext(ambientValues: new { c = "17" });
+
+ // Act
+ var success = linkGenerator.TryProcessTemplate(
+ httpContext: httpContext,
+ endpoint: endpoint,
+ ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext),
+ explicitValues: new RouteValueDictionary(new { }),
+ options: null,
+ out var result);
+
+ // Assert
+ Assert.True(success);
+ Assert.Equal("/a/15/17", result.path.ToUriComponent());
+ Assert.Equal(string.Empty, result.query.ToUriComponent());
+ }
+
+ [Fact]
+ public void TryProcessTemplate_OptionalParameterAfterDefault_OneValueFromAmbientValues()
+ {
+ // Arrange
+ var endpoint = EndpointFactory.CreateRouteEndpoint("a/{b=15}/{c?}");
+ var linkGenerator = CreateLinkGenerator(endpoint);
+ var httpContext = CreateHttpContext(ambientValues: new { c = "17" });
+
+ // Act
+ var success = linkGenerator.TryProcessTemplate(
+ httpContext: httpContext,
+ endpoint: endpoint,
+ ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext),
+ explicitValues: new RouteValueDictionary(new { }),
+ options: null,
+ out var result);
+
+ // Assert
+ Assert.True(success);
+ Assert.Equal("/a/15/17", result.path.ToUriComponent());
+ Assert.Equal(string.Empty, result.query.ToUriComponent());
+ }
+
+ [Fact]
+ public void TryProcessTemplate_TwoOptionalParametersAfterDefault_LastValueFromAmbientValues()
+ {
+ // Arrange
+ var endpoint = EndpointFactory.CreateRouteEndpoint("a/{b=15}/{c?}/{d?}");
+ var linkGenerator = CreateLinkGenerator(endpoint);
+ var httpContext = CreateHttpContext(ambientValues: new { d = "17" });
+
+ // Act
+ var success = linkGenerator.TryProcessTemplate(
+ httpContext: httpContext,
+ endpoint: endpoint,
+ ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext),
+ explicitValues: new RouteValueDictionary(new { }),
+ options: null,
+ out var result);
+
+ // Assert
+ Assert.True(success);
+ Assert.Equal("/a", result.path.ToUriComponent());
+ Assert.Equal(string.Empty, result.query.ToUriComponent());
+ }
+
+ public static TheoryData