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 DoesNotDiscardAmbientValuesData + { + get + { + // - ambient values + // - explicit values + // - required values + // - defaults + return new TheoryData + { + // link to same action on same controller + { + new { controller = "Products", action = "Edit", id = 10 }, + new { controller = "Products", action = "Edit" }, + new { area = (string)null, controller = "Products", action = "Edit", page = (string)null }, + new { area = (string)null, controller = "Products", action = "Edit", page = (string)null } + }, + + // link to same action on same controller - ignoring case + { + new { controller = "ProDUcts", action = "EDit", id = 10 }, + new { controller = "ProDUcts", action = "EDit" }, + new { area = (string)null, controller = "Products", action = "Edit", page = (string)null }, + new { area = (string)null, controller = "Products", action = "Edit", page = (string)null } + }, + + // link to same action and same controller on same area + { + new { area = "Admin", controller = "Products", action = "Edit", id = 10 }, + new { area = "Admin", controller = "Products", action = "Edit" }, + new { area = "Admin", controller = "Products", action = "Edit", page = (string)null }, + new { area = "Admin", controller = "Products", action = "Edit", page = (string)null } + }, + + // link to same action and same controller on same area + { + new { area = "Admin", controller = "Products", action = "Edit", id = 10 }, + new { controller = "Products", action = "Edit" }, + new { area = "Admin", controller = "Products", action = "Edit", page = (string)null }, + new { area = "Admin", controller = "Products", action = "Edit", page = (string)null } + }, + + // link to same action and same controller + { + new { controller = "Products", action = "Edit", id = 10 }, + new { controller = "Products", action = "Edit" }, + new { area = (string)null, controller = "Products", action = "Edit", page = (string)null }, + new { area = (string)null, controller = "Products", action = "Edit", page = (string)null } + }, + { + new { controller = "Products", action = "Edit", id = 10 }, + new { controller = "Products", action = "Edit" }, + new { area = (string)null, controller = "Products", action = "Edit", page = (string)null }, + new { area = (string)null, controller = "Products", action = "Edit", page = (string)null } + }, + { + new { controller = "Products", action = "Edit", id = 10 }, + new { controller = "Products", action = "Edit" }, + new { area = "", controller = "Products", action = "Edit", page = "" }, + new { area = "", controller = "Products", action = "Edit", page = "" } + }, + + // link to same page + { + new { page = "Products/Edit", id = 10 }, + new { page = "Products/Edit" }, + new { area = (string)null, controller = (string)null, action = (string)null, page = "Products/Edit" }, + new { area = (string)null, controller = (string)null, action = (string)null, page = "Products/Edit" } + }, + }; + } + } + + [Theory] + [MemberData(nameof(DoesNotDiscardAmbientValuesData))] + public void TryProcessTemplate_DoesNotDiscardAmbientValues_IfAllRequiredKeysMatch( + object ambientValues, + object explicitValues, + object requiredValues, + object defaults) + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint( + "Products/Edit/{id}", + requiredValues: requiredValues, + defaults: defaults); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + explicitValues: new RouteValueDictionary(explicitValues), + options: null, + out var result); + + // Assert + Assert.True(success); + Assert.Equal("/Products/Edit/10", result.path.ToUriComponent()); + Assert.Equal(string.Empty, result.query.ToUriComponent()); + } + + [Fact] + public void TryProcessTemplate_DoesNotDiscardAmbientValues_IfAllRequiredValuesMatch_ForGenericKeys() + { + // Verifying that discarding works in general usage case i.e when keys are not like controller, action etc. + + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint( + "Products/Edit/{id}", + requiredValues: new { c = "Products", a = "Edit" }, + defaults: new { c = "Products", a = "Edit" }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { c = "Products", a = "Edit", id = 10 }); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + explicitValues: new RouteValueDictionary(new { c = "Products", a = "Edit" }), + options: null, + out var result); + + // Assert + Assert.True(success); + Assert.Equal("/Products/Edit/10", result.path.ToUriComponent()); + Assert.Equal(string.Empty, result.query.ToUriComponent()); + } + + [Fact] + public void TryProcessTemplate_DiscardsAmbientValues_ForGenericKeys() + { + // Verifying that discarding works in general usage case i.e when keys are not like controller, action etc. + + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint( + "Products/Edit/{id}", + requiredValues: new { c = "Products", a = "Edit" }, + defaults: new { c = "Products", a = "Edit" }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { c = "Products", a = "Edit", id = 10 }); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + explicitValues: new RouteValueDictionary(new { c = "Products", a = "List" }), + options: null, + out var result); + + // Assert + Assert.False(success); + } + + public static TheoryData DiscardAmbientValuesData + { + get + { + // - ambient values + // - explicit values + // - required values + // - defaults + return new TheoryData + { + // link to different action on same controller + { + new { controller = "Products", action = "Edit", id = 10 }, + new { controller = "Products", action = "List" }, + new { area = (string)null, controller = "Products", action = "List", page = (string)null }, + new { area = (string)null, controller = "Products", action = "List", page = (string)null } + }, + + // link to different action on same controller and same area + { + new { area = "Customer", controller = "Products", action = "Edit", id = 10 }, + new { area = "Customer", controller = "Products", action = "List" }, + new { area = "Customer", controller = "Products", action = "List", page = (string)null }, + new { area = "Customer", controller = "Products", action = "List", page = (string)null } + }, + + // link from one area to a different one + { + new { area = "Admin", controller = "Products", action = "Edit", id = 10 }, + new { area = "Consumer", controller = "Products", action = "Edit" }, + new { area = "Consumer", controller = "Products", action = "Edit", page = (string)null }, + new { area = "Consumer", controller = "Products", action = "Edit", page = (string)null } + }, + + // link from non-area to a area one + { + new { controller = "Products", action = "Edit", id = 10 }, + new { area = "Consumer", controller = "Products", action = "Edit" }, + new { area = "Consumer", controller = "Products", action = "Edit", page = (string)null }, + new { area = "Consumer", controller = "Products", action = "Edit", page = (string)null } + }, + + // link from area to a non-area based action + { + new { area = "Admin", controller = "Products", action = "Edit", id = 10 }, + new { area = "", controller = "Products", action = "Edit" }, + new { area = "", controller = "Products", action = "Edit", page = (string)null }, + new { area = "", controller = "Products", action = "Edit", page = (string)null } + }, + + // link from controller-action to a page + { + new { controller = "Products", action = "Edit", id = 10 }, + new { page = "Products/Edit" }, + new { area = (string)null, controller = (string)null, action = (string)null, page = "Products/Edit"}, + new { area = (string)null, controller = (string)null, action = (string)null, page = "Products/Edit"} + }, + + // link from a page to controller-action + { + new { page = "Products/Edit", id = 10 }, + new { controller = "Products", action = "Edit" }, + new { area = (string)null, controller = "Products", action = "Edit", page = (string)null }, + new { area = (string)null, controller = "Products", action = "Edit", page = (string)null } + }, + + // link from one page to a different page + { + new { page = "Products/Details", id = 10 }, + new { page = "Products/Edit" }, + new { area = (string)null, controller = (string)null, action = (string)null, page = "Products/Edit" }, + new { area = (string)null, controller = (string)null, action = (string)null, page = "Products/Edit" } + }, + }; + } + } + + [Theory] + [MemberData(nameof(DiscardAmbientValuesData))] + public void TryProcessTemplate_DiscardsAmbientValues_IfAnyAmbientValue_IsDifferentThan_EndpointRequiredValues( + object ambientValues, + object explicitValues, + object requiredValues, + object defaults) + { + // Linking to a different action on the same controller + + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint( + "Products/Edit/{id}", + requiredValues: requiredValues, + defaults: defaults); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + explicitValues: new RouteValueDictionary(explicitValues), + options: null, + out var result); + + // Assert + Assert.False(success); + } + } +} diff --git a/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs index 42008ca6eb..75b93f157e 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs @@ -1,1630 +1,434 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.Routing.Constraints; -using Microsoft.AspNetCore.Routing.Internal; -using Microsoft.AspNetCore.Routing.Matching; -using Microsoft.AspNetCore.Routing.TestObjects; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.ObjectPool; -using Microsoft.Extensions.Options; -using Moq; using Xunit; namespace Microsoft.AspNetCore.Routing { - public class DefaultLinkGeneratorTest + // Tests LinkGenerator functionality using GetXyzByAddress - see tests for the extension + // methods for more E2E tests. + // + // Does not cover template processing in detail, those scenarios are validated by TemplateBinderTests + // and DefaultLinkGeneratorProcessTemplateTest + public class DefaultLinkGeneratorTest : LinkGeneratorTestBase { [Fact] - public void GetLink_Success() + public void GetPathByAddress_WithoutHttpContext_NoMatches_ReturnsNull() { // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}"); + var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); + var linkGenerator = CreateLinkGenerator(endpoint); // Act - var link = linkGenerator.GetLink(new { controller = "Home" }); + var path = linkGenerator.GetPathByAddress(0, values: null); // Assert - Assert.Equal("/Home", link); + Assert.Null(path); } [Fact] - public void GetLink_Fail_ThrowsException() + public void GetPathByAddress_WithHttpContext_NoMatches_ReturnsNull() { // Arrange - var expectedMessage = "Could not find a matching endpoint to generate a link."; - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); - var linkGenerator = CreateLinkGenerator(endpoint); + var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); - // Act & Assert - var exception = Assert.Throws( - () => linkGenerator.GetLink(new { controller = "Home" })); - Assert.Equal(expectedMessage, exception.Message); - } - - [Fact] - public void TryGetLink_Fail() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); var linkGenerator = CreateLinkGenerator(endpoint); // Act - var canGenerateLink = linkGenerator.TryGetLink( - new { controller = "Home" }, - out var link); + var path = linkGenerator.GetPathByAddress(CreateHttpContext(), 0, values: null); // Assert - Assert.False(canGenerateLink); - Assert.Null(link); + Assert.Null(path); } [Fact] - public void GetLink_MultipleEndpoints_Success() + public void GetUriByAddress_WithoutHttpContext_NoMatches_ReturnsNull() { // Arrange - var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}"); - var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); - var endpoint3 = EndpointFactory.CreateRouteEndpoint("{controller}"); - var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2, endpoint3); + var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); - // Act - var link = linkGenerator.GetLink(new { controller = "Home", action = "Index", id = "10" }); - - // Assert - Assert.Equal("/Home/Index/10", link); - } - - [Fact] - public void GetLink_MultipleEndpoints_Success2() - { - // Arrange - var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}"); - var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); - var endpoint3 = EndpointFactory.CreateRouteEndpoint("{controller}"); - var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2, endpoint3); - - // Act - var link = linkGenerator.GetLink(new { controller = "Home", action = "Index" }); - - // Assert - Assert.Equal("/Home/Index", link); - } - - [Fact] - public void GetLink_EncodesIntermediate_DefaultValues() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{p1}/{p2=a b}/{p3=foo}"); var linkGenerator = CreateLinkGenerator(endpoint); // Act - var link = linkGenerator.GetLink(new { p1 = "Home", p3 = "bar" }); + var uri = linkGenerator.GetUriByAddress(0, values: null, "http", new HostString("example.com")); // Assert - Assert.Equal("/Home/a%20b/bar", link); + Assert.Null(uri); } - [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 GetLink_EncodesValue_OfSingleAsteriskCatchAllParameter(string routeValue, string expected) + [Fact] + public void GetUriByAddress_WithHttpContext_NoMatches_ReturnsNull() { // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{*path}"); + var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); + var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Index" }); // Act - var link = linkGenerator.GetLink(httpContext, new { path = routeValue }); + var uri = linkGenerator.GetUriByAddress(CreateHttpContext(), 0, values: null); // Assert - Assert.Equal(expected, link); - } - - [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 GetLink_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 link = linkGenerator.GetLink(httpContext, new { path = routeValue }); - - // Assert - Assert.Equal(expected, link); + Assert.Null(uri); } [Fact] - public void GetLink_EncodesContentOtherThanSlashes_OfDoubleAsteriskCatchAllParameter() + public void GetPathByAddress_WithoutHttpContext_HasMatches_ReturnsFirstSuccessfulTemplateResult() { // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{**path}"); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Index" }); + var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); - // Act - var link = linkGenerator.GetLink(httpContext, new { path = "a/b b1/c c1" }); - - // Assert - Assert.Equal("/Home/Index/a/b%20b1/c%20c1", link); - } - - [Fact] - public void GetLink_EncodesValues() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Index" }); - - // Act - var link = linkGenerator.GetLink(httpContext, new { name = "name with %special #characters" }); - - // Assert - Assert.Equal("/Home/Index?name=name%20with%20%25special%20%23characters", link); - } - - [Fact] - public void GetLink_ForListOfStrings() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); - var linkGenerator = CreateLinkGenerator(endpoint); - var context = CreateHttpContext(ambientValues: new { controller = "Home", action = "Index" }); - - // Act - var link = linkGenerator.GetLink(context, new { color = new List { "red", "green", "blue" } }); - - // Assert - Assert.Equal("/Home/Index?color=red&color=green&color=blue", link); - } - - [Fact] - public void GetLink_ForListOfInts() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Index" }); - - // Act - var link = linkGenerator.GetLink(httpContext, new { items = new List { 10, 20, 30 } }); - - // Assert - Assert.Equal("/Home/Index?items=10&items=20&items=30", link); - } - - [Fact] - public void GetLink_ForList_Empty() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Index" }); - - // Act - var link = linkGenerator.GetLink(httpContext, new { color = new List { } }); - - // Assert - Assert.Equal("/Home/Index", link); - } - - [Fact] - public void GetLink_ForList_StringWorkaround() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Index" }); - - // Act - var link = linkGenerator.GetLink( - httpContext, - new { page = 1, color = new List { "red", "green", "blue" }, message = "textfortest" }); - - // Assert - Assert.Equal("/Home/Index?page=1&color=red&color=green&color=blue&message=textfortest", link); - } - - [Fact] - public void GetLink_Success_AmbientValues() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" }); - - // Act - var link = linkGenerator.GetLink(httpContext, new { action = "Index" }); - - // Assert - Assert.Equal("/Home/Index", link); - } - - [Fact] - public void GetLink_GeneratesLowercaseUrl_SetOnRouteOptions() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); - var linkGenerator = CreateLinkGenerator(new[] { endpoint }, new RouteOptions() { LowercaseUrls = true }); - var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" }); - - // Act - var link = linkGenerator.GetLink(httpContext, new { action = "Index" }); - - // Assert - Assert.Equal("/home/index", link); - } - - [Fact] - public void GetLink_GeneratesLowercaseQueryString_SetOnRouteOptions() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); - var linkGenerator = CreateLinkGenerator( - new[] { endpoint }, - new RouteOptions() { LowercaseUrls = true, LowercaseQueryStrings = true }); - var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" }); - - // Act - var link = linkGenerator.GetLink( - httpContext, - new { action = "Index", ShowStatus = "True", INFO = "DETAILED" }); - - // Assert - Assert.Equal("/home/index?showstatus=true&info=detailed", link); - } - - [Fact] - public void GetLink_GeneratesLowercaseQueryString_OnlyIfLowercaseUrlIsTrue_SetOnRouteOptions() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); - var linkGenerator = CreateLinkGenerator( - new[] { endpoint }, - new RouteOptions() { LowercaseUrls = false, LowercaseQueryStrings = true }); - var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" }); - - // Act - var link = linkGenerator.GetLink( - httpContext, - new { action = "Index", ShowStatus = "True", INFO = "DETAILED" }); - - // Assert - Assert.Equal("/Home/Index?ShowStatus=True&INFO=DETAILED", link); - } - - [Fact] - public void GetLink_AppendsTrailingSlash_SetOnRouteOptions() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); - var linkGenerator = CreateLinkGenerator( - new[] { endpoint }, - new RouteOptions() { AppendTrailingSlash = true }); - var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" }); - - // Act - var link = linkGenerator.GetLink(httpContext, new { action = "Index" }); - - // Assert - Assert.Equal("/Home/Index/", link); - } - - [Fact] - public void GetLink_GeneratesLowercaseQueryStringAndTrailingSlash_SetOnRouteOptions() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); - var linkGenerator = CreateLinkGenerator( - new[] { endpoint }, - new RouteOptions() { LowercaseUrls = true, LowercaseQueryStrings = true, AppendTrailingSlash = true }); - var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" }); - - // Act - var link = linkGenerator.GetLink( - httpContext, - new { action = "Index", ShowStatus = "True", INFO = "DETAILED" }); - - // Assert - Assert.Equal("/home/index/?showstatus=true&info=detailed", link); - } - - [Fact] - public void GetLink_LowercaseUrlSetToTrue_OnRouteOptions_OverridenByCallsiteValue() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); - var linkGenerator = CreateLinkGenerator( - new[] { endpoint }, - new RouteOptions() { LowercaseUrls = true }); - var httpContext = CreateHttpContext(ambientValues: new { controller = "HoMe" }); - - // Act - var link = linkGenerator.GetLink( - httpContext, - values: new { action = "InDex" }, - new LinkOptions - { - LowercaseUrls = false - }); - - // Assert - Assert.Equal("/HoMe/InDex", link); - } - - [Fact] - public void GetLink_LowercaseUrlSetToFalse_OnRouteOptions_OverridenByCallsiteValue() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); - var linkGenerator = CreateLinkGenerator( - new[] { endpoint }, - new RouteOptions() { LowercaseUrls = false }); - var httpContext = CreateHttpContext(ambientValues: new { controller = "HoMe" }); - - // Act - var link = linkGenerator.GetLink( - httpContext, - values: new { action = "InDex" }, - new LinkOptions - { - LowercaseUrls = true - }); - - // Assert - Assert.Equal("/home/index", link); - } - - [Fact] - public void GetLink_LowercaseUrlQueryStringsSetToTrue_OnRouteOptions_OverridenByCallsiteValue() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); - var linkGenerator = CreateLinkGenerator( - new[] { endpoint }, - new RouteOptions() { LowercaseUrls = true, LowercaseQueryStrings = true }); - var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" }); - - // Act - var link = linkGenerator.GetLink( - httpContext, - values: new { action = "Index", ShowStatus = "True", INFO = "DETAILED" }, - new LinkOptions - { - LowercaseUrls = false, - LowercaseQueryStrings = false - }); - - // Assert - Assert.Equal("/Home/Index?ShowStatus=True&INFO=DETAILED", link); - } - - [Fact] - public void GetLink_LowercaseUrlQueryStringsSetToFalse_OnRouteOptions_OverridenByCallsiteValue() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); - var linkGenerator = CreateLinkGenerator( - new[] { endpoint }, - new RouteOptions() { LowercaseUrls = false, LowercaseQueryStrings = false }); - var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" }); - - // Act - var link = linkGenerator.GetLink( - httpContext, - values: new { action = "Index", ShowStatus = "True", INFO = "DETAILED" }, - new LinkOptions - { - LowercaseUrls = true, - LowercaseQueryStrings = true - }); - - // Assert - Assert.Equal("/home/index?showstatus=true&info=detailed", link); - } - - [Fact] - public void GetLink_AppendTrailingSlashSetToFalse_OnRouteOptions_OverridenByCallsiteValue() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); - var linkGenerator = CreateLinkGenerator( - new[] { endpoint }, - new RouteOptions() { AppendTrailingSlash = false }); - var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" }); - - // Act - var link = linkGenerator.GetLink( - httpContext, - values: new { action = "Index" }, - new LinkOptions - { - AppendTrailingSlash = true - }); - - // Assert - Assert.Equal("/Home/Index/", link); - } - - [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 canGenerateLink = linkGenerator.TryGetLink( - httpContext, - new { p1 = "abcd" }, - out var link); - - // Assert - Assert.False(canGenerateLink); - } - - [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 canGenerateLink = linkGenerator.TryGetLink( - httpContext, - new { p1 = "hello", p2 = "1234" }, - out var link); - - // Assert - Assert.True(canGenerateLink); - Assert.Equal("/hello/1234", link); - } - - [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 canGenerateLink = linkGenerator.TryGetLink( - httpContext, - new { p1 = "abcd" }, - out var link); - - // Assert - Assert.False(canGenerateLink); - } - - [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 canGenerateLink = linkGenerator.TryGetLink( - httpContext, - new { p1 = "hello", p2 = "1234" }, - out var link); - - // Assert - Assert.True(canGenerateLink); - Assert.Equal("/hello/1234", link); - } - - [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 canGenerateLink = linkGenerator.TryGetLink( - httpContext, - new { p1 = "hello", p2 = "1234" }, - out var link); - - // Assert - Assert.True(canGenerateLink); - Assert.Equal("/hello/1234", link); - 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 GetLink_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 canGenerateLink = linkGenerator.TryGetLink( - httpContext, - new { action = "Store" }, - out var link); - - // Assert - Assert.True(canGenerateLink); - Assert.Equal("/slug/Home/Store", link); - 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 GetLink_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 link = linkGenerator.GetLink(httpContext, new { action = "Store" }); - - // Assert - Assert.Equal("/slug/Home/Store", link); - 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 GetLink_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 link = linkGenerator.GetLink(httpContext, new { controller = "Shopping" }); - - // Assert - Assert.Equal("/slug/Shopping", link); - 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 GetLink_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 link = linkGenerator.GetLink( - httpContext, - new { action = "Store", thirdthing = "13" }); - - // Assert - Assert.Equal("/slug/Home/Store", link); - Assert.Equal(expectedValues.OrderBy(kvp => kvp.Key), constraint.Values.OrderBy(kvp => kvp.Key)); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void GetLink_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 link = linkGenerator.GetLink( - httpContext, - new { action = "Index", controller = "Home", id = 4 }); - - // Assert - Assert.Equal("/Home/Index/4", link); - } - - [Fact] - public void GetLink_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 canGenerateLink = linkGenerator.TryGetLink( - httpContext, - new { action = "Index", controller = "Home", id = "not-an-integer" }, - out var link); - - // Assert - Assert.False(canGenerateLink); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void GetLink_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 link = linkGenerator.GetLink(httpContext, new { action = "Index", controller = "Home", id = 98 }); - - // Assert - Assert.Equal("/Home/Index/98", link); - } - - [Fact] - public void GetLink_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 link = linkGenerator.GetLink(httpContext, new { action = "Index", controller = "Home" }); - - // Assert - Assert.Equal("/Home/Index", link); - } - - [Fact] - public void GetLink_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 canGenerateLink = linkGenerator.TryGetLink( - httpContext, - new { action = "Index", controller = "Home", id = "not-an-integer" }, - out var link); - - // Assert - Assert.False(canGenerateLink); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void GetLink_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 link = linkGenerator.GetLink( - httpContext, - new { action = "Index", controller = "Home", id = 14 }); - - // Assert - Assert.Equal("/Home/Index/14", link); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void GetLink_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 canGenerateLink = linkGenerator.TryGetLink( - httpContext, - new { action = "Index", controller = "Home", id = 50 }, - out var link); - - // Assert - Assert.False(canGenerateLink); - } - - [Fact] - public void GetLink_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 link = linkGenerator.GetLink( - httpContext, - new { action = "Index", controller = "Home", name = "products" }); - - // Assert - Assert.Equal("/Home/Index/products", link); - } - - [Fact] - public void GetLink_OptionalParameter_ParameterPresentInValues() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{name?}"); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues: new { }); - - // Act - var link = linkGenerator.GetLink( - httpContext, - new { action = "Index", controller = "Home", name = "products" }); - - // Assert - Assert.Equal("/Home/Index/products", link); - } - - [Fact] - public void GetLink_OptionalParameter_ParameterNotPresentInValues() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{name?}"); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues: new { }); - - // Act - var link = linkGenerator.GetLink( - httpContext, - new { action = "Index", controller = "Home" }); - - // Assert - Assert.Equal("/Home/Index", link); - } - - [Fact] - public void GetLink_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 link = linkGenerator.GetLink( - httpContext, - new { action = "Index", controller = "Home", name = "products" }); - - // Assert - Assert.Equal("/Home/Index/products", link); - } - - [Fact] - public void GetLink_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 link = linkGenerator.GetLink( - httpContext, - new { action = "Index", controller = "Home" }); - - // Assert - Assert.Equal("/Home/Index", link); - } - - [Fact] - public void GetLink_ParameterNotPresentInTemplate_PresentInValues() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{name}"); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues: new { }); - - // Act - var link = linkGenerator.GetLink( - httpContext, - new { action = "Index", controller = "Home", name = "products", format = "json" }); - - // Assert - Assert.Equal("/Home/Index/products?format=json", link); - } - - [Fact] - public void GetLink_OptionalParameter_FollowedByDotAfterSlash_ParameterPresent() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint( - template: "{controller}/{action}/.{name?}"); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues: new { }); - - // Act - var link = linkGenerator.GetLink( - httpContext, - new { action = "Index", controller = "Home", name = "products" }); - - // Assert - Assert.Equal("/Home/Index/.products", link); - } - - [Fact] - public void GetLink_OptionalParameter_FollowedByDotAfterSlash_ParameterNotPresent() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/.{name?}"); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues: new { }); - - // Act - var link = linkGenerator.GetLink(httpContext, new { action = "Index", controller = "Home" }); - - // Assert - Assert.Equal("/Home/Index/", link); - } - - [Fact] - public void GetLink_OptionalParameter_InSimpleSegment() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{name?}"); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues: new { }); - - // Act - var link = linkGenerator.GetLink(httpContext, new { action = "Index", controller = "Home" }); - - // Assert - Assert.Equal("/Home/Index", link); - } - - [Fact] - public void GetLink_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 link = linkGenerator.GetLink(httpContext, new { }); - - // Assert - Assert.Equal("/a/15/17", link); - } - - [Fact] - public void GetLink_OptionalParameterAfterDefault_OneValueFromAmbientValues() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("a/{b=15}/{c?}"); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues: new { c = "17" }); - - // Act - var link = linkGenerator.GetLink(httpContext, new { }); - - // Assert - Assert.Equal("/a/15/17", link); - } - - [Fact] - public void GetLink_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 link = linkGenerator.GetLink(httpContext, new { }); - - // Assert - Assert.Equal("/a", link); - } - - public static TheoryData DoesNotDiscardAmbientValuesData - { - get - { - // - ambient values - // - explicit values - // - required values - // - defaults - return new TheoryData - { - // link to same action on same controller - { - new { controller = "Products", action = "Edit", id = 10 }, - new { controller = "Products", action = "Edit" }, - new { area = (string)null, controller = "Products", action = "Edit", page = (string)null }, - new { area = (string)null, controller = "Products", action = "Edit", page = (string)null } - }, - - // link to same action on same controller - ignoring case - { - new { controller = "ProDUcts", action = "EDit", id = 10 }, - new { controller = "ProDUcts", action = "EDit" }, - new { area = (string)null, controller = "Products", action = "Edit", page = (string)null }, - new { area = (string)null, controller = "Products", action = "Edit", page = (string)null } - }, - - // link to same action and same controller on same area - { - new { area = "Admin", controller = "Products", action = "Edit", id = 10 }, - new { area = "Admin", controller = "Products", action = "Edit" }, - new { area = "Admin", controller = "Products", action = "Edit", page = (string)null }, - new { area = "Admin", controller = "Products", action = "Edit", page = (string)null } - }, - - // link to same action and same controller on same area - { - new { area = "Admin", controller = "Products", action = "Edit", id = 10 }, - new { controller = "Products", action = "Edit" }, - new { area = "Admin", controller = "Products", action = "Edit", page = (string)null }, - new { area = "Admin", controller = "Products", action = "Edit", page = (string)null } - }, - - // link to same action and same controller - { - new { controller = "Products", action = "Edit", id = 10 }, - new { controller = "Products", action = "Edit" }, - new { area = (string)null, controller = "Products", action = "Edit", page = (string)null }, - new { area = (string)null, controller = "Products", action = "Edit", page = (string)null } - }, - { - new { controller = "Products", action = "Edit", id = 10 }, - new { controller = "Products", action = "Edit" }, - new { area = (string)null, controller = "Products", action = "Edit", page = (string)null }, - new { area = (string)null, controller = "Products", action = "Edit", page = (string)null } - }, - { - new { controller = "Products", action = "Edit", id = 10 }, - new { controller = "Products", action = "Edit" }, - new { area = "", controller = "Products", action = "Edit", page = "" }, - new { area = "", controller = "Products", action = "Edit", page = "" } - }, - - // link to same page - { - new { page = "Products/Edit", id = 10 }, - new { page = "Products/Edit" }, - new { area = (string)null, controller = (string)null, action = (string)null, page = "Products/Edit" }, - new { area = (string)null, controller = (string)null, action = (string)null, page = "Products/Edit" } - }, - }; - } - } - - [Theory] - [MemberData(nameof(DoesNotDiscardAmbientValuesData))] - public void TryGetLink_DoesNotDiscardAmbientValues_IfAllRequiredKeysMatch( - object ambientValues, - object explicitValues, - object requiredValues, - object defaults) - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint( - "Products/Edit/{id}", - requiredValues: requiredValues, - defaults: defaults); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues); - - // Act - var canGenerateLink = linkGenerator.TryGetLink( - httpContext, - new RouteValueDictionary(explicitValues), - out var link); - - // Assert - Assert.True(canGenerateLink); - Assert.Equal("/Products/Edit/10", link); - } - - [Fact] - public void TryGetLink_DoesNotDiscardAmbientValues_IfAllRequiredValuesMatch_ForGenericKeys() - { - // Verifying that discarding works in general usage case i.e when keys are not like controller, action etc. - - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint( - "Products/Edit/{id}", - requiredValues: new { c = "Products", a = "Edit" }, - defaults: new { c = "Products", a = "Edit" }); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues: new { c = "Products", a = "Edit", id = 10 }); - - // Act - var canGenerateLink = linkGenerator.TryGetLink( - httpContext, - new { c = "Products", a = "Edit" }, - out var link); - - // Assert - Assert.True(canGenerateLink); - Assert.Equal("/Products/Edit/10", link); - } - - [Fact] - public void TryGetLink_DiscardsAmbientValues_ForGenericKeys() - { - // Verifying that discarding works in general usage case i.e when keys are not like controller, action etc. - - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint( - "Products/Edit/{id}", - requiredValues: new { c = "Products", a = "Edit" }, - defaults: new { c = "Products", a = "Edit" }); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues: new { c = "Products", a = "Edit", id = 10 }); - - // Act - var canGenerateLink = linkGenerator.TryGetLink( - httpContext, - new { c = "Products", a = "List" }, - out var link); - - // Assert - Assert.False(canGenerateLink); - Assert.Null(link); - } - - public static TheoryData DiscardAmbientValuesData - { - get - { - // - ambient values - // - explicit values - // - required values - // - defaults - return new TheoryData - { - // link to different action on same controller - { - new { controller = "Products", action = "Edit", id = 10 }, - new { controller = "Products", action = "List" }, - new { area = (string)null, controller = "Products", action = "List", page = (string)null }, - new { area = (string)null, controller = "Products", action = "List", page = (string)null } - }, - - // link to different action on same controller and same area - { - new { area = "Customer", controller = "Products", action = "Edit", id = 10 }, - new { area = "Customer", controller = "Products", action = "List" }, - new { area = "Customer", controller = "Products", action = "List", page = (string)null }, - new { area = "Customer", controller = "Products", action = "List", page = (string)null } - }, - - // link from one area to a different one - { - new { area = "Admin", controller = "Products", action = "Edit", id = 10 }, - new { area = "Consumer", controller = "Products", action = "Edit" }, - new { area = "Consumer", controller = "Products", action = "Edit", page = (string)null }, - new { area = "Consumer", controller = "Products", action = "Edit", page = (string)null } - }, - - // link from non-area to a area one - { - new { controller = "Products", action = "Edit", id = 10 }, - new { area = "Consumer", controller = "Products", action = "Edit" }, - new { area = "Consumer", controller = "Products", action = "Edit", page = (string)null }, - new { area = "Consumer", controller = "Products", action = "Edit", page = (string)null } - }, - - // link from area to a non-area based action - { - new { area = "Admin", controller = "Products", action = "Edit", id = 10 }, - new { area = "", controller = "Products", action = "Edit" }, - new { area = "", controller = "Products", action = "Edit", page = (string)null }, - new { area = "", controller = "Products", action = "Edit", page = (string)null } - }, - - // link from controller-action to a page - { - new { controller = "Products", action = "Edit", id = 10 }, - new { page = "Products/Edit" }, - new { area = (string)null, controller = (string)null, action = (string)null, page = "Products/Edit"}, - new { area = (string)null, controller = (string)null, action = (string)null, page = "Products/Edit"} - }, - - // link from a page to controller-action - { - new { page = "Products/Edit", id = 10 }, - new { controller = "Products", action = "Edit" }, - new { area = (string)null, controller = "Products", action = "Edit", page = (string)null }, - new { area = (string)null, controller = "Products", action = "Edit", page = (string)null } - }, - - // link from one page to a different page - { - new { page = "Products/Details", id = 10 }, - new { page = "Products/Edit" }, - new { area = (string)null, controller = (string)null, action = (string)null, page = "Products/Edit" }, - new { area = (string)null, controller = (string)null, action = (string)null, page = "Products/Edit" } - }, - }; - } - } - - [Theory] - [MemberData(nameof(DiscardAmbientValuesData))] - public void TryGetLink_DiscardsAmbientValues_IfAnyAmbientValue_IsDifferentThan_EndpointRequiredValues( - object ambientValues, - object explicitValues, - object requiredValues, - object defaults) - { - // Linking to a different action on the same controller - - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint( - "Products/Edit/{id}", - requiredValues: requiredValues, - defaults: defaults); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues); - - // Act - var canGenerateLink = linkGenerator.TryGetLink( - httpContext, - new RouteValueDictionary(explicitValues), - out var link); - - // Assert - Assert.False(canGenerateLink); - Assert.Null(link); - } - - [Fact] - public void TryGetLinkByAddress_WithCustomAddress_CanGenerateLink() - { - // Arrange - var services = GetBasicServices(); - services.TryAddEnumerable( - ServiceDescriptor.Singleton, EndpointFinderByName>()); - var endpoint1 = EndpointFactory.CreateRouteEndpoint( - "Products/Details/{id}", - requiredValues: new { controller = "Products", action = "Details" }, - defaults: new { controller = "Products", action = "Details" }); - var endpoint2 = EndpointFactory.CreateRouteEndpoint( - "Customers/Details/{id}", - requiredValues: new { controller = "Customers", action = "Details" }, - defaults: new { controller = "Customers", action = "Details" }, - metadata: new NameMetadata("CustomerDetails")); - var linkGenerator = CreateLinkGenerator(new[] { endpoint1, endpoint2 }, new RouteOptions(), services); - var httpContext = CreateHttpContext(ambientValues: new { }); - - // Act - var canGenerateLink = linkGenerator.TryGetLinkByAddress( - httpContext, - address: new NameMetadata("CustomerDetails"), - values: new { id = 10 }, - out var link); - - // Assert - Assert.True(canGenerateLink); - Assert.Equal("/Customers/Details/10", link); - } - - [Fact] - public void TryGetLinkByAddress_WithCustomAddress_CanGenerateLink_RespectsLinkOptions_SuppliedAtCallSite() - { - // Arrange - var services = GetBasicServices(); - services.TryAddEnumerable( - ServiceDescriptor.Singleton, EndpointFinderByName>()); - var endpoint1 = EndpointFactory.CreateRouteEndpoint( - "Products/Details/{id}", - requiredValues: new { controller = "Products", action = "Details" }, - defaults: new { controller = "Products", action = "Details" }); - var endpoint2 = EndpointFactory.CreateRouteEndpoint( - "Customers/Details/{id}", - requiredValues: new { controller = "Customers", action = "Details" }, - defaults: new { controller = "Customers", action = "Details" }, - metadata: new NameMetadata("CustomerDetails")); - var linkGenerator = CreateLinkGenerator(new[] { endpoint1, endpoint2 }, new RouteOptions(), services); - var httpContext = CreateHttpContext(ambientValues: new { }); - - // Act - var canGenerateLink = linkGenerator.TryGetLinkByAddress( - httpContext, - address: new NameMetadata("CustomerDetails"), - values: new { id = 10 }, - new LinkOptions - { - LowercaseUrls = true - }, - out var link); - - // Assert - Assert.True(canGenerateLink); - Assert.Equal("/customers/details/10", link); - } - - [Fact] - public void GetTemplate_ByRouteValues_ReturnsTemplate() - { - // Arrange - var endpoint1 = EndpointFactory.CreateRouteEndpoint( - "Product/Edit/{id}", - requiredValues: new { controller = "Product", action = "Edit", area = (string)null, page = (string)null }, - defaults: new { controller = "Product", action = "Edit", area = (string)null, page = (string)null }); - var linkGenerator = CreateLinkGenerator(endpoint1); - var values = new RouteValueDictionary(new { controller = "Product", action = "Edit" }); - - // Act - var template = linkGenerator.GetTemplate(values); - - // Assert - var defaultTemplate = Assert.IsType(template); - Assert.Same(linkGenerator, defaultTemplate.LinkGenerator); - Assert.Equal(new[] { endpoint1 }, defaultTemplate.Endpoints); - Assert.Equal(values, defaultTemplate.EarlierExplicitValues); - Assert.Null(defaultTemplate.HttpContext); - Assert.Empty(defaultTemplate.AmbientValues); - } - - [Fact] - public void GetTemplate_ByRouteName_ReturnsTemplate() - { - // Arrange - var endpoint1 = EndpointFactory.CreateRouteEndpoint( - "Product/Edit/{id}", - defaults: new { controller = "Product", action = "Edit", area = (string)null, page = (string)null }, - metadata: new RouteValuesAddressMetadata( - "EditProduct", - new RouteValueDictionary(new { controller = "Product", action = "Edit", area = (string)null, page = (string)null }))); - var linkGenerator = CreateLinkGenerator(endpoint1); - - // Act - var template = linkGenerator.GetTemplate("EditProduct", values: new { }); - - // Assert - var defaultTemplate = Assert.IsType(template); - Assert.Same(linkGenerator, defaultTemplate.LinkGenerator); - Assert.Equal(new[] { endpoint1 }, defaultTemplate.Endpoints); - Assert.Empty(defaultTemplate.EarlierExplicitValues); - Assert.Null(defaultTemplate.HttpContext); - Assert.Empty(defaultTemplate.AmbientValues); - } - - [Fact] - public void GetTemplate_ByRouteName_ReturnsTemplate_WithMultipleEndpoints() - { - // Arrange - var endpoint1 = EndpointFactory.CreateRouteEndpoint( - "Product/Edit/{id}", - defaults: new { controller = "Product", action = "Edit", area = (string)null, page = (string)null }, - metadata: new RouteValuesAddressMetadata( - "default", - new RouteValueDictionary(new { controller = "Product", action = "Edit", area = (string)null, page = (string)null }))); - var endpoint2 = EndpointFactory.CreateRouteEndpoint( - "Product/Details/{id}", - defaults: new { controller = "Product", action = "Edit", area = (string)null, page = (string)null }, - metadata: new RouteValuesAddressMetadata( - "default", - new RouteValueDictionary(new { controller = "Product", action = "Edit", area = (string)null, page = (string)null }))); var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); // Act - var template = linkGenerator.GetTemplate("default", values: new { }); + var path = linkGenerator.GetPathByAddress(1, values: new RouteValueDictionary(new { controller = "Home", action = "Index", })); // Assert - var defaultTemplate = Assert.IsType(template); - Assert.Same(linkGenerator, defaultTemplate.LinkGenerator); - Assert.Equal(new[] { endpoint1, endpoint2 }, defaultTemplate.Endpoints); - Assert.Empty(defaultTemplate.EarlierExplicitValues); - Assert.Null(defaultTemplate.HttpContext); - Assert.Empty(defaultTemplate.AmbientValues); + Assert.Equal("/Home/Index", path); } [Fact] - public void GetTemplateByAddress_ByCustomAddress_ReturnsTemplate() + public void GetPathByAddress_WithHttpContext_HasMatches_ReturnsFirstSuccessfulTemplateResult() { // Arrange - var services = GetBasicServices(); - services.TryAddEnumerable( - ServiceDescriptor.Singleton, EndpointFinderByName>()); - var endpoint1 = EndpointFactory.CreateRouteEndpoint( - "Product/Edit/{id}", - requiredValues: new { controller = "Product", action = "Edit", area = (string)null, page = (string)null }, - defaults: new { controller = "Product", action = "Edit", area = (string)null, page = (string)null }); - var endpoint2 = EndpointFactory.CreateRouteEndpoint( - "Customers/Details/{id}", - requiredValues: new { controller = "Customers", action = "Details" }, - defaults: new { controller = "Customers", action = "Details" }, - metadata: new NameMetadata("CustomerDetails")); - var linkGenerator = CreateLinkGenerator(new[] { endpoint1, endpoint2 }, new RouteOptions(), services); + var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); + + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); // Act - var template = linkGenerator.GetTemplateByAddress(new NameMetadata("CustomerDetails")); + var path = linkGenerator.GetPathByAddress(CreateHttpContext(), 1, values: new RouteValueDictionary(new { controller = "Home", action = "Index", })); // Assert - var defaultTemplate = Assert.IsType(template); - Assert.Same(linkGenerator, defaultTemplate.LinkGenerator); - Assert.Equal(new[] { endpoint2 }, defaultTemplate.Endpoints); - Assert.Empty(defaultTemplate.EarlierExplicitValues); - Assert.Null(defaultTemplate.HttpContext); - Assert.Empty(defaultTemplate.AmbientValues); + Assert.Equal("/Home/Index", path); } [Fact] - public void MakeUrl_Honors_LinkOptions() + public void GetUriByAddress_WithoutHttpContext_HasMatches_ReturnsFirstSuccessfulTemplateResult() { // Arrange - var services = GetBasicServices(); - services.TryAddEnumerable( - ServiceDescriptor.Singleton, EndpointFinderByName>()); - var endpoint1 = EndpointFactory.CreateRouteEndpoint( - "Product/Edit/{id}", - requiredValues: new { controller = "Product", action = "Edit", area = (string)null, page = (string)null }, - defaults: new { controller = "Product", action = "Edit", area = (string)null, page = (string)null }); - var endpoint2 = EndpointFactory.CreateRouteEndpoint( - "Customers/Details/{id}", - requiredValues: new { controller = "Customers", action = "Details" }, - defaults: new { controller = "Customers", action = "Details" }, - metadata: new NameMetadata("CustomerDetails")); - var linkGenerator = CreateLinkGenerator(new[] { endpoint1, endpoint2 }, new RouteOptions(), services); + var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); - // Act1 - var template = linkGenerator.GetTemplateByAddress(new NameMetadata("CustomerDetails")); + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); - // Assert1 - Assert.NotNull(template); + // Act + var path = linkGenerator.GetUriByAddress( + 1, + values: new RouteValueDictionary(new { controller = "Home", action = "Index", }), + "http", + new HostString("example.com")); - // Act2 - var link = template.MakeUrl(new { id = 10 }, new LinkOptions { LowercaseUrls = true }); - - // Assert2 - Assert.Equal("/customers/details/10", link); - - // Act3 - link = template.MakeUrl(new { id = 25 }); - - // Assert3 - Assert.Equal("/Customers/Details/25", link); + // Assert + Assert.Equal("http://example.com/Home/Index", path); } [Fact] - public void MakeUrl_GeneratesLink_WithExtraRouteValues() + public void GetUriByAddress_WithHttpContext_HasMatches_ReturnsFirstSuccessfulTemplateResult() { // Arrange - var endpoint1 = EndpointFactory.CreateRouteEndpoint( - "Product/Edit/{id}", - requiredValues: new { controller = "Product", action = "Edit", area = (string)null, page = (string)null }, - defaults: new { controller = "Product", action = "Edit", area = (string)null, page = (string)null }); - var linkGenerator = CreateLinkGenerator(endpoint1); + var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); - // Act1 - var template = linkGenerator.GetTemplate( - values: new { controller = "Product", action = "Edit", foo = "bar" }); + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); - // Assert1 + var httpContext = CreateHttpContext(); + httpContext.Request.Scheme = "http"; + httpContext.Request.Host = new HostString("example.com"); + + // Act + var uri = linkGenerator.GetUriByAddress(httpContext, 1, values: new RouteValueDictionary(new { controller = "Home", action = "Index", })); + + // Assert + Assert.Equal("http://example.com/Home/Index", uri); + } + + [Fact] + public void GetPathByAddress_WithoutHttpContext_WithLinkOptions() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); + + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + + // Act + var path = linkGenerator.GetPathByAddress( + 1, + values: new RouteValueDictionary(new { controller = "Home", action = "Index", }), + options: new LinkOptions() { AppendTrailingSlash = true, }); + + // Assert + Assert.Equal("/Home/Index/", path); + } + + [Fact] + public void GetPathByAddress_WithHttpContext_WithLinkOptions() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); + + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + + // Act + var path = linkGenerator.GetPathByAddress( + CreateHttpContext(), + 1, + values: new RouteValueDictionary(new { controller = "Home", action = "Index", }), + options: new LinkOptions() { AppendTrailingSlash = true, }); + + // Assert + Assert.Equal("/Home/Index/", path); + } + + [Fact] + public void GetUriByAddress_WithoutHttpContext_WithLinkOptions() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); + + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + + // Act + var path = linkGenerator.GetUriByAddress( + 1, + values: new RouteValueDictionary(new { controller = "Home", action = "Index", }), + "http", + new HostString("example.com"), + options: new LinkOptions() { AppendTrailingSlash = true, }); + + // Assert + Assert.Equal("http://example.com/Home/Index/", path); + } + + [Fact] + public void GetUriByAddress_WithHttpContext_WithLinkOptions() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); + + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + + var httpContext = CreateHttpContext(); + httpContext.Request.Scheme = "http"; + httpContext.Request.Host = new HostString("example.com"); + + // Act + var uri = linkGenerator.GetUriByAddress( + httpContext, + 1, + values: new RouteValueDictionary(new { controller = "Home", action = "Index", }), + options: new LinkOptions() { AppendTrailingSlash = true, }); + + // Assert + Assert.Equal("http://example.com/Home/Index/", uri); + } + + // Includes characters that need to be encoded + [Fact] + public void GetPathByAddress_WithoutHttpContext_WithPathBaseAndFragment() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); + + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + + // Act + var path = linkGenerator.GetPathByAddress( + 1, + values: new RouteValueDictionary(new { controller = "Home", action = "In?dex", query = "some?query" }), + new PathString("/Foo/Bar?encodeme?"), + new FragmentString("#Fragment?")); + + // Assert + Assert.Equal("/Foo/Bar%3Fencodeme%3F/Home/In%3Fdex?query=some%3Fquery#Fragment?", path); + } + + // Includes characters that need to be encoded + [Fact] + public void GetPathByAddress_WithHttpContext_WithPathBaseAndFragment() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); + + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + + var httpContext = CreateHttpContext(); + httpContext.Request.PathBase = new PathString("/Foo/Bar?encodeme?"); + + // Act + var path = linkGenerator.GetPathByAddress( + httpContext, + 1, + values: new RouteValueDictionary(new { controller = "Home", action = "In?dex", query = "some?query" }), + new FragmentString("#Fragment?")); + + // Assert + Assert.Equal("/Foo/Bar%3Fencodeme%3F/Home/In%3Fdex?query=some%3Fquery#Fragment?", path); + } + + // Includes characters that need to be encoded + [Fact] + public void GetUriByAddress_WithoutHttpContext_WithPathBaseAndFragment() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); + + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + + // Act + var path = linkGenerator.GetUriByAddress( + 1, + 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?")); + + // Assert + Assert.Equal("http://example.com/Foo/Bar%3Fencodeme%3F/Home/In%3Fdex?query=some%3Fquery#Fragment?", path); + } + + // Includes characters that need to be encoded + [Fact] + public void GetUriByAddress_WithHttpContext_WithPathBaseAndFragment() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); + + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + + var httpContext = CreateHttpContext(); + httpContext.Request.Scheme = "http"; + httpContext.Request.Host = new HostString("example.com"); + httpContext.Request.PathBase = new PathString("/Foo/Bar?encodeme?"); + + // Act + var uri = linkGenerator.GetUriByAddress( + httpContext, + 1, + values: new RouteValueDictionary(new { controller = "Home", action = "In?dex", query = "some?query" }), + new FragmentString("#Fragment?")); + + // Assert + Assert.Equal("http://example.com/Foo/Bar%3Fencodeme%3F/Home/In%3Fdex?query=some%3Fquery#Fragment?", uri); + } + + [Fact] + public void GetPathByAddress_WithHttpContext_IncludesAmbientValues() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); + + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + + var httpContext = CreateHttpContext(new { controller = "Home", }); + httpContext.Request.Scheme = "http"; + httpContext.Request.Host = new HostString("example.com"); + + // Act + var uri = linkGenerator.GetPathByAddress(httpContext, 1, values: new RouteValueDictionary(new { action = "Index", })); + + // Assert + Assert.Equal("/Home/Index", uri); + } + + [Fact] + public void GetUriByAddress_WithHttpContext_IncludesAmbientValues() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); + + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + + var httpContext = CreateHttpContext(new { controller = "Home", }); + httpContext.Request.Scheme = "http"; + httpContext.Request.Host = new HostString("example.com"); + + // Act + var uri = linkGenerator.GetUriByAddress(httpContext, 1, values: new RouteValueDictionary(new { action = "Index", })); + + // Assert + Assert.Equal("http://example.com/Home/Index", uri); + } + + [Fact] + public void GetTemplateByAddress_WithNoMatch_ReturnsNull() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); + + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + + // Act + var template = linkGenerator.GetTemplateByAddress(address: 0); + + // Assert + Assert.Null(template); + } + + [Fact] + public void GetTemplateByAddress_WithMatch_ReturnsTemplate() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); + + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + + // Act + var template = linkGenerator.GetTemplateByAddress(address: 1); + + // Assert Assert.NotNull(template); - - // Act2 - var link = template.MakeUrl(new { id = 10 }); - - // Assert2 - Assert.Equal("/Product/Edit/10?foo=bar", link); - - // Act3 - link = template.MakeUrl(new { id = 25, foo = "boo" }); - - // Assert3 - Assert.Equal("/Product/Edit/25?foo=boo", link); + Assert.Collection( + Assert.IsType(template).Endpoints, + e => Assert.Same(endpoint1, e), + e => Assert.Same(endpoint2, e)); } - private LinkGenerator CreateLinkGenerator(params Endpoint[] endpoints) + protected override void AddAdditionalServices(IServiceCollection services) { - return CreateLinkGenerator(endpoints, routeOptions: null); + services.AddSingleton, IntEndpointFinder>(); } - private LinkGenerator CreateLinkGenerator( - Endpoint[] endpoints, - RouteOptions routeOptions, - ServiceCollection services = null) - { - if (services == null) - { - services = GetBasicServices(); - } - - if (endpoints != null || endpoints.Length > 0) - { - services.Configure(o => - { - o.DataSources.Add(new DefaultEndpointDataSource(endpoints)); - }); - } - - routeOptions = routeOptions ?? new RouteOptions(); - var options = Options.Create(routeOptions); - var serviceProvider = services.BuildServiceProvider(); - - return new DefaultLinkGenerator( - new DefaultParameterPolicyFactory(options, serviceProvider), - new DefaultObjectPool(new UriBuilderContextPooledObjectPolicy()), - options, - NullLogger.Instance, - serviceProvider); - } - - private HttpContext CreateHttpContext(object ambientValues) - { - var httpContext = new DefaultHttpContext(); - - var feature = new EndpointFeature - { - RouteValues = new RouteValueDictionary(ambientValues) - }; - httpContext.Features.Set(feature); - httpContext.Features.Set(feature); - return httpContext; - } - - private ServiceCollection GetBasicServices() - { - var services = new ServiceCollection(); - services.AddSingleton(); - services.AddOptions(); - services.AddRouting(); - services.AddLogging(); - return services; - } - - private class EndpointFinderByName : IEndpointFinder + private class IntEndpointFinder : IEndpointFinder { private readonly CompositeEndpointDataSource _dataSource; - public EndpointFinderByName(CompositeEndpointDataSource dataSource) + public IntEndpointFinder(CompositeEndpointDataSource dataSource) { _dataSource = dataSource; } - public IEnumerable FindEndpoints(INameMetadata address) + public IEnumerable FindEndpoints(int address) { - var endpoint = _dataSource.Endpoints.SingleOrDefault(e => - { - var nameMetadata = e.Metadata.GetMetadata(); - return nameMetadata != null && string.Equals(address.Name, nameMetadata.Name); - }); - return new[] { endpoint }; + return _dataSource.Endpoints.Where(e => e.Metadata.GetMetadata().Value == address); } } - private interface INameMetadata + private class IntMetadata { - string Name { get; } - } - - private class NameMetadata : INameMetadata - { - public NameMetadata(string name) + public IntMetadata(int value) { - Name = name; + Value = value; } - public string Name { get; } + public int Value { get; } } } } diff --git a/test/Microsoft.AspNetCore.Routing.Tests/LinkGeneratorRouteValuesAddressExtensionsTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/LinkGeneratorRouteValuesAddressExtensionsTest.cs new file mode 100644 index 0000000000..d4ccf080d0 --- /dev/null +++ b/test/Microsoft.AspNetCore.Routing.Tests/LinkGeneratorRouteValuesAddressExtensionsTest.cs @@ -0,0 +1,128 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Http; +using Xunit; + +namespace Microsoft.AspNetCore.Routing +{ + // Integration tests for GetXyzByRouteValues. These are basic because important behavioral details + // are covered elsewhere. + // + // Does not cover template processing in detail, those scenarios are validated by TemplateBinderTests + // and DefaultLinkGeneratorProcessTemplateTest + // + // Does not cover the RouteValueBasedEndpointFinder in detail. see RouteValueBasedEndpointFinderTest + public class LinkGeneratorRouteValuesAddressExtensionsTest : LinkGeneratorTestBase + { + [Fact] + public void GetPathByRouteValues_WithoutHttpContext_WithPathBaseAndFragment() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint( + "{controller}/{action}/{id}", + metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "In?dex", })) }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint( + "{controller}/{action}/{id?}", + metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "In?dex", })) }); + + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + + // Act + var path = linkGenerator.GetPathByRouteValues( + routeName: null, + values: new RouteValueDictionary(new { controller = "Home", action = "In?dex", query = "some?query" }), + new PathString("/Foo/Bar?encodeme?"), + new FragmentString("#Fragment?"), + new LinkOptions() { AppendTrailingSlash = true, }); + + // Assert + Assert.Equal("/Foo/Bar%3Fencodeme%3F/Home/In%3Fdex/?query=some%3Fquery#Fragment?", path); + } + + [Fact] + public void GetPathByRouteValues_WithHttpContext_WithPathBaseAndFragment() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint( + "{controller}/{action}/{id}", + metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "In?dex", })) }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint( + "{controller}/{action}/{id?}", + metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "In?dex", })) }); + + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + + var httpContext = CreateHttpContext(); + httpContext.Request.PathBase = new PathString("/Foo/Bar?encodeme?"); + + // Act + var path = linkGenerator.GetPathByRouteValues( + httpContext, + routeName: null, + values: new RouteValueDictionary(new { controller = "Home", action = "In?dex", query = "some?query" }), + new FragmentString("#Fragment?"), + new LinkOptions() { AppendTrailingSlash = true, }); + + // Assert + Assert.Equal("/Foo/Bar%3Fencodeme%3F/Home/In%3Fdex/?query=some%3Fquery#Fragment?", path); + } + + [Fact] + public void GetUriByRouteValues_WithoutHttpContext_WithPathBaseAndFragment() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint( + "{controller}/{action}/{id}", + metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "In?dex", })) }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint( + "{controller}/{action}/{id?}", + metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "In?dex", })) }); + + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + + // Act + var path = linkGenerator.GetUriByRouteValues( + routeName: null, + values: new RouteValueDictionary(new { controller = "Home", action = "In?dex", query = "some?query" }), + "http", + new HostString("example.com"), + new PathString("/Foo/Bar?encodeme?"), + new FragmentString("#Fragment?"), + new LinkOptions() { AppendTrailingSlash = true, }); + + // Assert + Assert.Equal("http://example.com/Foo/Bar%3Fencodeme%3F/Home/In%3Fdex/?query=some%3Fquery#Fragment?", path); + } + + [Fact] + public void GetUri_WithHttpContext_WithPathBaseAndFragment() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint( + "{controller}/{action}/{id}", + metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "In?dex", })) }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint( + "{controller}/{action}/{id?}", + metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "In?dex", })) }); + + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + + var httpContext = CreateHttpContext(); + httpContext.Request.Scheme = "http"; + httpContext.Request.Host = new HostString("example.com"); + httpContext.Request.PathBase = new PathString("/Foo/Bar?encodeme?"); + + // Act + var uri = linkGenerator.GetUriByRouteValues( + httpContext, + routeName: null, + values: new RouteValueDictionary(new { controller = "Home", action = "In?dex", query = "some?query" }), + new FragmentString("#Fragment?"), + new LinkOptions() { AppendTrailingSlash = true, }); + + // Assert + Assert.Equal("http://example.com/Foo/Bar%3Fencodeme%3F/Home/In%3Fdex/?query=some%3Fquery#Fragment?", uri); + } + } +} diff --git a/test/Microsoft.AspNetCore.Routing.Tests/LinkGeneratorTestBase.cs b/test/Microsoft.AspNetCore.Routing.Tests/LinkGeneratorTestBase.cs new file mode 100644 index 0000000000..4864467409 --- /dev/null +++ b/test/Microsoft.AspNetCore.Routing.Tests/LinkGeneratorTestBase.cs @@ -0,0 +1,77 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Routing.Internal; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Routing +{ + public abstract class LinkGeneratorTestBase + { + protected HttpContext CreateHttpContext(object ambientValues = null) + { + var httpContext = new DefaultHttpContext(); + + var feature = new EndpointFeature + { + RouteValues = new RouteValueDictionary(ambientValues) + }; + + httpContext.Features.Set(feature); + httpContext.Features.Set(feature); + return httpContext; + } + + private ServiceCollection GetBasicServices() + { + var services = new ServiceCollection(); + services.AddSingleton(); + services.AddOptions(); + services.AddRouting(); + services.AddLogging(); + return services; + } + + protected virtual void AddAdditionalServices(IServiceCollection services) + { + } + + private protected DefaultLinkGenerator CreateLinkGenerator(params Endpoint[] endpoints) + { + return CreateLinkGenerator(routeOptions: null, services: null, endpoints); + } + + private protected DefaultLinkGenerator CreateLinkGenerator(RouteOptions routeOptions = null, IServiceCollection services = null, params Endpoint[] endpoints) + { + if (services == null) + { + services = GetBasicServices(); + AddAdditionalServices(services); + } + + if (endpoints != null || endpoints.Length > 0) + { + services.Configure(o => + { + o.DataSources.Add(new DefaultEndpointDataSource(endpoints)); + }); + } + + routeOptions = routeOptions ?? new RouteOptions(); + var options = Options.Create(routeOptions); + var serviceProvider = services.BuildServiceProvider(); + + return new DefaultLinkGenerator( + new DefaultParameterPolicyFactory(options, serviceProvider), + new DefaultObjectPool(new UriBuilderContextPooledObjectPolicy()), + options, + NullLogger.Instance, + serviceProvider); + } + } +} diff --git a/test/Microsoft.AspNetCore.Routing.Tests/RouteValueBasedEndpointFinderTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/RouteValueBasedEndpointFinderTest.cs index 5b475247bb..c3c1f644e8 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/RouteValueBasedEndpointFinderTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/RouteValueBasedEndpointFinderTest.cs @@ -1,18 +1,14 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Internal; -using Microsoft.AspNetCore.Routing.Matching; using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Routing.TestObjects; using Microsoft.AspNetCore.Routing.Tree; using Microsoft.Extensions.ObjectPool; -using Microsoft.Extensions.Options; using Xunit; namespace Microsoft.AspNetCore.Routing