diff --git a/src/Microsoft.AspNetCore.Routing.Abstractions/LinkGenerationTemplate.cs b/src/Microsoft.AspNetCore.Routing.Abstractions/LinkGenerationTemplate.cs new file mode 100644 index 0000000000..ec3736228a --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing.Abstractions/LinkGenerationTemplate.cs @@ -0,0 +1,29 @@ +// 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. + +namespace Microsoft.AspNetCore.Routing +{ + /// + /// Defines a contract to generate a URL from a template. + /// + public abstract class LinkGenerationTemplate + { + /// + /// Generates a URL with an absolute path from the specified route values. + /// + /// An object that contains route values. + /// The generated URL. + public string MakeUrl(object values) + { + return MakeUrl(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 abstract string MakeUrl(object values, LinkOptions options); + } +} \ 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 f4101a609a..35b9456bf6 100644 --- a/src/Microsoft.AspNetCore.Routing.Abstractions/LinkGenerator.cs +++ b/src/Microsoft.AspNetCore.Routing.Abstractions/LinkGenerator.cs @@ -6,78 +6,191 @@ using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Routing { + /// + /// Defines a contract to generate URLs to endpoints. + /// public abstract class LinkGenerator { + /// + /// Generates a URL with an absolute path from the specified route 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)) @@ -88,6 +201,16 @@ namespace Microsoft.AspNetCore.Routing 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, @@ -95,51 +218,119 @@ namespace Microsoft.AspNetCore.Routing 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(address, httpContext: null, values, options: null); + 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(address, httpContext: null, values, 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(address, httpContext: null, values, options, out link); + return TryGetLinkByAddress(httpContext: null, address, values, options, out link); } - public string GetLinkByAddress(TAddress address, HttpContext httpContext, object values) + /// + /// 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(address, httpContext, values, options: null); + 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( - TAddress address, HttpContext httpContext, + TAddress address, object values, out string link) { - return TryGetLinkByAddress(address, httpContext, values, options: null, out link); + return TryGetLinkByAddress(httpContext, 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. + /// The associated with current request. + /// An object that contains route values. + /// The . + /// The generated URL. public string GetLinkByAddress( - TAddress address, HttpContext httpContext, + TAddress address, object values, LinkOptions options) { - if (TryGetLinkByAddress(address, httpContext, values, options, out var link)) + if (TryGetLinkByAddress(httpContext, address, values, options, out var link)) { return link; } @@ -147,11 +338,126 @@ namespace Microsoft.AspNetCore.Routing throw new InvalidOperationException("Could not find a matching endpoint to generate a 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>'. + /// 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 . + /// The generated URL. + /// true if a URL was generated successfully; otherwise, false. public abstract bool TryGetLinkByAddress( - TAddress address, HttpContext httpContext, + TAddress address, object values, LinkOptions options, out string link); + + /// + /// 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. + /// + /// + /// An object that contains route values. These values are used to lookup endpoint(s). + /// + /// + /// If an endpoint(s) was found successfully, then this returns a template object representing that, + /// null otherwise. + /// + 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); } } diff --git a/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerationTemplate.cs b/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerationTemplate.cs new file mode 100644 index 0000000000..2b432db71f --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerationTemplate.cs @@ -0,0 +1,62 @@ +// 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 Microsoft.AspNetCore.Routing.Matching; + +namespace Microsoft.AspNetCore.Routing +{ + internal class DefaultLinkGenerationTemplate : LinkGenerationTemplate + { + public DefaultLinkGenerationTemplate( + DefaultLinkGenerator linkGenerator, + IEnumerable endpoints, + HttpContext httpContext, + RouteValueDictionary explicitValues, + RouteValueDictionary ambientValues) + { + LinkGenerator = linkGenerator; + Endpoints = endpoints; + HttpContext = httpContext; + EarlierExplicitValues = explicitValues; + AmbientValues = ambientValues; + } + + internal DefaultLinkGenerator LinkGenerator { get; } + + internal IEnumerable Endpoints { get; } + + internal HttpContext HttpContext { get; } + + internal RouteValueDictionary EarlierExplicitValues { get; } + + internal RouteValueDictionary AmbientValues { get; } + + public override string MakeUrl(object values, LinkOptions options) + { + var currentValues = new RouteValueDictionary(values); + var mergedValuesDictionary = new RouteValueDictionary(EarlierExplicitValues); + + foreach (var kvp in currentValues) + { + mergedValuesDictionary[kvp.Key] = kvp.Value; + } + + foreach (var endpoint in Endpoints) + { + var link = LinkGenerator.MakeLink( + HttpContext, + endpoint, + AmbientValues, + mergedValuesDictionary, + options); + if (link != null) + { + return link; + } + } + return null; + } + } +} diff --git a/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerator.cs b/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerator.cs index 13d2869d32..d56e7092ea 100644 --- a/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerator.cs +++ b/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerator.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; using System.Linq; using System.Text.Encodings.Web; using Microsoft.AspNetCore.Http; @@ -54,21 +55,78 @@ namespace Microsoft.AspNetCore.Routing } public override bool TryGetLinkByAddress( - TAddress address, HttpContext httpContext, + TAddress address, object values, LinkOptions options, out string link) { return TryGetLinkByAddressInternal( - address, httpContext, + address, explicitValues: values, ambientValues: GetAmbientValues(httpContext), options, out link); } + public override LinkGenerationTemplate GetTemplate(HttpContext httpContext, string routeName, object values) + { + var ambientValues = GetAmbientValues(httpContext); + var explicitValues = new RouteValueDictionary(values); + + return GetTemplateInternal( + httpContext, + new RouteValuesAddress + { + RouteName = routeName, + ExplicitValues = explicitValues, + AmbientValues = ambientValues + }, + ambientValues, + explicitValues, + values); + } + + public override LinkGenerationTemplate GetTemplateByAddress( + HttpContext httpContext, + TAddress address) + { + return GetTemplateInternal(httpContext, address, values: null); + } + + internal string MakeLink( + HttpContext httpContext, + MatcherEndpoint endpoint, + RouteValueDictionary ambientValues, + RouteValueDictionary explicitValues, + LinkOptions options) + { + var templateBinder = new TemplateBinder( + UrlEncoder.Default, + _uriBuildingContextPool, + new RouteTemplate(endpoint.RoutePattern), + new RouteValueDictionary(endpoint.RoutePattern.Defaults)); + + var templateValuesResult = templateBinder.GetValues( + ambientValues: ambientValues, + explicitValues: explicitValues, + requiredKeys: endpoint.RequiredValues.Keys); + if (templateValuesResult == null) + { + // We're missing one of the required values for this route. + return null; + } + + if (!MatchesConstraints(httpContext, endpoint, templateValuesResult.CombinedValues)) + { + return null; + } + + var url = templateBinder.BindValues(templateValuesResult.AcceptedValues); + return Normalize(url, options); + } + private bool TryGetLinkByRouteValues( HttpContext httpContext, string routeName, @@ -86,8 +144,8 @@ namespace Microsoft.AspNetCore.Routing }; return TryGetLinkByAddressInternal( - address, httpContext, + address, explicitValues: values, ambientValues: ambientValues, options, @@ -95,8 +153,8 @@ namespace Microsoft.AspNetCore.Routing } private bool TryGetLinkByAddressInternal( - TAddress address, HttpContext httpContext, + TAddress address, object explicitValues, RouteValueDictionary ambientValues, LinkOptions options, @@ -104,22 +162,21 @@ namespace Microsoft.AspNetCore.Routing { link = null; - var endpointFinder = _serviceProvider.GetRequiredService>(); - var endpoints = endpointFinder.FindEndpoints(address); + var endpoints = FindEndpoints(address); if (endpoints == null) { return false; } - var matcherEndpoints = endpoints.OfType(); - if (!matcherEndpoints.Any()) + foreach (var endpoint in endpoints) { - return false; - } + link = MakeLink( + httpContext, + endpoint, + ambientValues, + new RouteValueDictionary(explicitValues), + options); - foreach (var endpoint in matcherEndpoints) - { - link = GetLink(endpoint); if (link != null) { return true; @@ -127,33 +184,49 @@ namespace Microsoft.AspNetCore.Routing } return false; + } - string GetLink(MatcherEndpoint endpoint) + private LinkGenerationTemplate GetTemplateInternal( + HttpContext httpContext, + TAddress address, + object values) + { + var endpoints = FindEndpoints(address); + if (endpoints == null) { - var templateBinder = new TemplateBinder( - UrlEncoder.Default, - _uriBuildingContextPool, - new RouteTemplate(endpoint.RoutePattern), - new RouteValueDictionary(endpoint.RoutePattern.Defaults)); - - var templateValuesResult = templateBinder.GetValues( - ambientValues: ambientValues, - explicitValues: new RouteValueDictionary(explicitValues), - requiredKeys: endpoint.RequiredValues.Keys); - if (templateValuesResult == null) - { - // We're missing one of the required values for this route. - return null; - } - - if (!MatchesConstraints(httpContext, endpoint, templateValuesResult.CombinedValues)) - { - return null; - } - - var url = templateBinder.BindValues(templateValuesResult.AcceptedValues); - return Normalize(url, options); + 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); } private bool MatchesConstraints( @@ -234,7 +307,25 @@ namespace Microsoft.AspNetCore.Routing return feature.Values; } } - return null; + return new RouteValueDictionary(); + } + + private IEnumerable FindEndpoints(TAddress address) + { + var finder = _serviceProvider.GetRequiredService>(); + var endpoints = finder.FindEndpoints(address); + if (endpoints == null) + { + return null; + } + + var matcherEndpoints = endpoints.OfType(); + if (!matcherEndpoints.Any()) + { + return null; + } + + return matcherEndpoints; } } } diff --git a/src/Microsoft.AspNetCore.Routing/IEndpointFinderOfT.cs b/src/Microsoft.AspNetCore.Routing/IEndpointFinderOfT.cs index ba450f1c7b..618220a6e9 100644 --- a/src/Microsoft.AspNetCore.Routing/IEndpointFinderOfT.cs +++ b/src/Microsoft.AspNetCore.Routing/IEndpointFinderOfT.cs @@ -5,8 +5,17 @@ using System.Collections.Generic; namespace Microsoft.AspNetCore.Routing { + /// + /// Defines a contract to find endpoints based on the supplied lookup information. + /// + /// The address type to look up endpoints. public interface IEndpointFinder { + /// + /// Finds endpoints based on the supplied lookup information. + /// + /// The information used to look up endpoints. + /// A collection of . IEnumerable FindEndpoints(TAddress address); } } diff --git a/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs index 1063db6819..c226e3edd6 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs @@ -1217,7 +1217,7 @@ namespace Microsoft.AspNetCore.Routing } [Fact] - public void TryGetLink_WithCustomAddress_CanGenerateLink() + public void TryGetLinkByAddress_WithCustomAddress_CanGenerateLink() { // Arrange var services = GetBasicServices(); @@ -1237,8 +1237,8 @@ namespace Microsoft.AspNetCore.Routing // Act var canGenerateLink = linkGenerator.TryGetLinkByAddress( - address: new NameMetadata("CustomerDetails"), httpContext, + address: new NameMetadata("CustomerDetails"), values: new { id = 10 }, out var link); @@ -1248,7 +1248,7 @@ namespace Microsoft.AspNetCore.Routing } [Fact] - public void TryGetLink_WithCustomAddress_CanGenerateLink_RespectsLinkOptions_SuppliedAtCallSite() + public void TryGetLinkByAddress_WithCustomAddress_CanGenerateLink_RespectsLinkOptions_SuppliedAtCallSite() { // Arrange var services = GetBasicServices(); @@ -1268,8 +1268,8 @@ namespace Microsoft.AspNetCore.Routing // Act var canGenerateLink = linkGenerator.TryGetLinkByAddress( - address: new NameMetadata("CustomerDetails"), httpContext, + address: new NameMetadata("CustomerDetails"), values: new { id = 10 }, new LinkOptions { @@ -1282,6 +1282,177 @@ namespace Microsoft.AspNetCore.Routing Assert.Equal("/customers/details/10", link); } + [Fact] + public void GetTemplate_ByRouteValues_ReturnsTemplate() + { + // Arrange + var endpoint1 = EndpointFactory.CreateMatcherEndpoint( + "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.CreateMatcherEndpoint( + "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 }, + metadata: new RouteNameMetadata("EditProduct")); + 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.CreateMatcherEndpoint( + "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 }, + metadata: new RouteNameMetadata("default")); + var endpoint2 = EndpointFactory.CreateMatcherEndpoint( + "Product/Details/{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 }, + metadata: new RouteNameMetadata("default")); + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + + // Act + var template = linkGenerator.GetTemplate("default", values: new { }); + + // 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); + } + + [Fact] + public void GetTemplateByAddress_ByCustomAddress_ReturnsTemplate() + { + // Arrange + var services = GetBasicServices(); + services.TryAddEnumerable( + ServiceDescriptor.Singleton, EndpointFinderByName>()); + var endpoint1 = EndpointFactory.CreateMatcherEndpoint( + "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.CreateMatcherEndpoint( + "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); + + // Act + var template = linkGenerator.GetTemplateByAddress(new NameMetadata("CustomerDetails")); + + // 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); + } + + [Fact] + public void MakeUrl_Honors_LinkOptions() + { + // Arrange + var services = GetBasicServices(); + services.TryAddEnumerable( + ServiceDescriptor.Singleton, EndpointFinderByName>()); + var endpoint1 = EndpointFactory.CreateMatcherEndpoint( + "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.CreateMatcherEndpoint( + "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); + + // Act1 + var template = linkGenerator.GetTemplateByAddress(new NameMetadata("CustomerDetails")); + + // Assert1 + Assert.NotNull(template); + + // 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); + } + + [Fact] + public void MakeUrl_GeneratesLink_WithExtraRouteValues() + { + // Arrange + var endpoint1 = EndpointFactory.CreateMatcherEndpoint( + "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); + + // Act1 + var template = linkGenerator.GetTemplate( + values: new { controller = "Product", action = "Edit", foo = "bar" }); + + // Assert1 + 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); + } + private LinkGenerator CreateLinkGenerator(params Endpoint[] endpoints) { return CreateLinkGenerator(endpoints, routeOptions: null); @@ -1365,5 +1536,14 @@ namespace Microsoft.AspNetCore.Routing } public string Name { get; } } + + private class RouteNameMetadata : IRouteNameMetadata + { + public RouteNameMetadata(string name) + { + Name = name; + } + public string Name { get; } + } } }