diff --git a/src/Microsoft.AspNetCore.Routing/DependencyInjection/RoutingServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.Routing/DependencyInjection/RoutingServiceCollectionExtensions.cs index d2ab2f127b..6037f8fa45 100644 --- a/src/Microsoft.AspNetCore.Routing/DependencyInjection/RoutingServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.Routing/DependencyInjection/RoutingServiceCollectionExtensions.cs @@ -68,8 +68,9 @@ namespace Microsoft.Extensions.DependencyInjection services.TryAddSingleton(); // Link generation related services - services.TryAddSingleton, RouteValuesBasedEndpointFinder>(); services.TryAddSingleton(); + services.TryAddSingleton, EndpointNameEndpointFinder>(); + services.TryAddSingleton, RouteValuesBasedEndpointFinder>(); // // Endpoint Selection diff --git a/src/Microsoft.AspNetCore.Routing/EndpointNameEndpointFinder.cs b/src/Microsoft.AspNetCore.Routing/EndpointNameEndpointFinder.cs new file mode 100644 index 0000000000..404ffde04b --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/EndpointNameEndpointFinder.cs @@ -0,0 +1,107 @@ +// 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.Text; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Routing +{ + internal class EndpointNameEndpointFinder : IEndpointFinder + { + private readonly DataSourceDependentCache> _cache; + + public EndpointNameEndpointFinder(CompositeEndpointDataSource dataSource) + { + _cache = new DataSourceDependentCache>(dataSource, Initialize); + } + + // Internal for tests + internal Dictionary Entries => _cache.EnsureInitialized(); + + public IEnumerable FindEndpoints(string address) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + + // Capture the current value of the cache + var entries = Entries; + + entries.TryGetValue(address, out var result); + return result ?? Array.Empty(); + } + + private static Dictionary Initialize(IReadOnlyList endpoints) + { + // Collect duplicates as we go, blow up on startup if we find any. + var hasDuplicates = false; + + var entries = new Dictionary(StringComparer.Ordinal); + for (var i = 0; i < endpoints.Count; i++) + { + var endpoint = endpoints[i]; + + var endpointName = GetEndpointName(endpoint); + if (endpointName == null) + { + continue; + } + + if (!entries.TryGetValue(endpointName, out var existing)) + { + // This isn't a duplicate (so far) + entries[endpointName] = new[] { endpoint }; + continue; + } + + // Ok this is a duplicate, because we have two endpoints with the same name. Bail out, because we + // are just going to throw, we don't need to finish collecting data. + hasDuplicates = true; + break; + } + + if (!hasDuplicates) + { + // No duplicates, success! + return entries; + } + + // OK we need to report some duplicates. + var duplicates = endpoints + .GroupBy(e => GetEndpointName(e)) + .Where(g => g.Key != null) + .Where(g => g.Count() > 1); + + var builder = new StringBuilder(); + builder.AppendLine(Resources.DuplicateEndpointNameHeader); + + foreach (var group in duplicates) + { + builder.AppendLine(); + builder.AppendLine(Resources.FormatDuplicateEndpointNameEntry(group.Key)); + + foreach (var endpoint in group) + { + builder.AppendLine(endpoint.DisplayName); + } + } + + throw new InvalidOperationException(builder.ToString()); + + string GetEndpointName(Endpoint endpoint) + { + if (endpoint.Metadata.GetMetadata() != null) + { + // Skip anything that's suppressed for linking. + return null; + } + + return endpoint.Metadata.GetMetadata()?.EndpointName; + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Routing/EndpointNameMetadata.cs b/src/Microsoft.AspNetCore.Routing/EndpointNameMetadata.cs new file mode 100644 index 0000000000..0b417f6722 --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/EndpointNameMetadata.cs @@ -0,0 +1,33 @@ +// 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 +{ + /// + /// Specifies an endpoint name in . + /// + /// + /// Endpoint names must be unique within an application, and can be used to unambiguously + /// identify a desired endpoint for URI generation using . + /// + public class EndpointNameMetadata : IEndpointNameMetadata + { + public EndpointNameMetadata(string endpointName) + { + if (endpointName == null) + { + throw new ArgumentNullException(nameof(endpointName)); + } + + EndpointName = endpointName; + } + + /// + /// Gets the endpoint name. + /// + public string EndpointName { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Routing/IEndpointNameMetadata.cs b/src/Microsoft.AspNetCore.Routing/IEndpointNameMetadata.cs new file mode 100644 index 0000000000..e2e3b85fda --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/IEndpointNameMetadata.cs @@ -0,0 +1,22 @@ +// 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 use to specify an endpoint name in . + /// + /// + /// Endpoint names must be unique within an application, and can be used to unambiguously + /// identify a desired endpoint for URI generation using . + /// + public interface IEndpointNameMetadata + { + /// + /// Gets the endpoint name. + /// + string EndpointName { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Routing/LinkGeneratorEndpointNameAddressExtensions.cs b/src/Microsoft.AspNetCore.Routing/LinkGeneratorEndpointNameAddressExtensions.cs new file mode 100644 index 0000000000..ab65be757e --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/LinkGeneratorEndpointNameAddressExtensions.cs @@ -0,0 +1,177 @@ +// 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 and endpoint name. + /// + public static class LinkGeneratorEndpointNameAddressExtensions + { + /// + /// Generates a URI with an absolute path based on the provided values. + /// + /// The . + /// The associated with the current request. + /// The endpoint name. 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 static string GetPathByName( + this LinkGenerator generator, + HttpContext httpContext, + string endpointName, + object values, + FragmentString fragment = default, + LinkOptions options = default) + { + if (generator == null) + { + throw new ArgumentNullException(nameof(generator)); + } + + if (endpointName == null) + { + throw new ArgumentNullException(nameof(endpointName)); + } + + return generator.GetPathByAddress(httpContext, endpointName, new RouteValueDictionary(values), fragment, options); + } + + /// + /// Generates a URI with an absolute path based on the provided values. + /// + /// The . + /// The endpoint name. 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 static string GetPathByName( + this LinkGenerator generator, + string endpointName, + object values, + PathString pathBase = default, + FragmentString fragment = default, + LinkOptions options = default) + { + if (generator == null) + { + throw new ArgumentNullException(nameof(generator)); + } + + if (endpointName == null) + { + throw new ArgumentNullException(nameof(endpointName)); + } + + return generator.GetPathByAddress(endpointName, new RouteValueDictionary(values), pathBase, fragment, options); + } + + /// + /// Generates an absolute URI based on the provided values. + /// + /// The . + /// The associated with the current request. + /// The endpoint name. 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 static string GetUriByName( + this LinkGenerator generator, + HttpContext httpContext, + string endpointName, + object values, + FragmentString fragment = default, + LinkOptions options = default) + { + if (generator == null) + { + throw new ArgumentNullException(nameof(generator)); + } + + if (endpointName == null) + { + throw new ArgumentNullException(nameof(endpointName)); + } + + return generator.GetUriByAddress(httpContext, endpointName, new RouteValueDictionary(values), fragment, options); + } + + /// + /// Generates an absolute URI based on the provided values. + /// + /// The . + /// The endpoint name. 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 static string GetUriByName( + this LinkGenerator generator, + string endpointName, + object values, + string scheme, + HostString host, + PathString pathBase = default, + FragmentString fragment = default, + LinkOptions options = default) + { + if (generator == null) + { + throw new ArgumentNullException(nameof(generator)); + } + + if (endpointName == null) + { + throw new ArgumentNullException(nameof(endpointName)); + } + + return generator.GetUriByAddress(endpointName, new RouteValueDictionary(values), scheme, host, pathBase, fragment, options); + } + + /// + /// Gets a based on the provided . + /// + /// The . + /// The endpoint name. Used to resolve endpoints. Optional. + /// + /// A if one or more endpoints matching the address can be found, otherwise null. + /// + public static LinkGenerationTemplate GetTemplateByName(this LinkGenerator generator, string endpointName) + { + if (generator == null) + { + throw new ArgumentNullException(nameof(generator)); + } + + if (endpointName == null) + { + throw new ArgumentNullException(nameof(endpointName)); + } + + return generator.GetTemplateByAddress(endpointName); + } + } +} diff --git a/src/Microsoft.AspNetCore.Routing/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Routing/Properties/Resources.Designer.cs index 47c12ae404..72372c4a89 100644 --- a/src/Microsoft.AspNetCore.Routing/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Routing/Properties/Resources.Designer.cs @@ -459,7 +459,7 @@ namespace Microsoft.AspNetCore.Routing => string.Format(CultureInfo.CurrentCulture, GetString("ConstraintMustBeStringOrConstraint"), p0, p1, p2); /// - /// Invalid constraint '{0}'. A constraint must be of type 'string', '{1}', or '{2}'. + /// Invalid constraint '{0}'. A constraint must be of type 'string' or '{1}'. /// internal static string RoutePattern_InvalidConstraintReference { @@ -514,6 +514,34 @@ namespace Microsoft.AspNetCore.Routing internal static string FormatRoutePattern_InvalidStringConstraintReference(object p0, object p1, object p2, object p3) => string.Format(CultureInfo.CurrentCulture, GetString("RoutePattern_InvalidStringConstraintReference"), p0, p1, p2, p3); + /// + /// Endpoints with endpoint name '{0}': + /// + internal static string DuplicateEndpointNameEntry + { + get => GetString("DuplicateEndpointNameEntry"); + } + + /// + /// Endpoints with endpoint name '{0}': + /// + internal static string FormatDuplicateEndpointNameEntry(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("DuplicateEndpointNameEntry"), p0); + + /// + /// The following endpoints with a duplicate endpoint name were found. + /// + internal static string DuplicateEndpointNameHeader + { + get => GetString("DuplicateEndpointNameHeader"); + } + + /// + /// The following endpoints with a duplicate endpoint name were found. + /// + internal static string FormatDuplicateEndpointNameHeader() + => GetString("DuplicateEndpointNameHeader"); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNetCore.Routing/Resources.resx b/src/Microsoft.AspNetCore.Routing/Resources.resx index c192330770..6ce5d8ff00 100644 --- a/src/Microsoft.AspNetCore.Routing/Resources.resx +++ b/src/Microsoft.AspNetCore.Routing/Resources.resx @@ -225,4 +225,10 @@ Invalid constraint type '{0}' registered as '{1}'. A constraint type must either implement '{2}', or inherit from '{3}'. + + Endpoints with endpoint name '{0}': + + + The following endpoints with a duplicate endpoint name were found. + \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Routing.Tests/EndpointNameEndpointFinderTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/EndpointNameEndpointFinderTest.cs new file mode 100644 index 0000000000..9165dce257 --- /dev/null +++ b/test/Microsoft.AspNetCore.Routing.Tests/EndpointNameEndpointFinderTest.cs @@ -0,0 +1,184 @@ +// 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.Linq; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing.TestObjects; +using Xunit; + +namespace Microsoft.AspNetCore.Routing +{ + public class EndpointNameEndpointFinderTest + { + [Fact] + public void EndpointFinder_Match_ReturnsMatchingEndpoint() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint( + "/a", + metadata: new object[] { new EndpointNameMetadata("name1"), }); + + var endpoint2 = EndpointFactory.CreateRouteEndpoint( + "/b", + metadata: new object[] { new EndpointNameMetadata("name2"), }); + + var finder = CreateEndpointFinder(endpoint1, endpoint2); + + // Act + var endpoints = finder.FindEndpoints("name2"); + + // Assert + Assert.Collection( + endpoints, + e => Assert.Same(endpoint2, e)); + } + + [Fact] + public void EndpointFinder_NoMatch_ReturnsEmptyCollection() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint( + "/a", + metadata: new object[] { new EndpointNameMetadata("name1"), new SuppressLinkGenerationMetadata(), }); + + var finder = CreateEndpointFinder(endpoint); + + // Act + var endpoints = finder.FindEndpoints("name2"); + + // Assert + Assert.Empty(endpoints); + } + + [Fact] + public void EndpointFinder_NoMatch_CaseSensitive() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint( + "/a", + metadata: new object[] { new EndpointNameMetadata("name1"), new SuppressLinkGenerationMetadata(), }); + + var finder = CreateEndpointFinder(endpoint); + + // Act + var endpoints = finder.FindEndpoints("NAME1"); + + // Assert + Assert.Empty(endpoints); + } + + [Fact] + public void EndpointFinder_UpdatesWhenDataSourceChanges() + { + var endpoint1 = EndpointFactory.CreateRouteEndpoint( + "/a", + metadata: new object[] { new EndpointNameMetadata("name1"), }); + var dynamicDataSource = new DynamicEndpointDataSource(new[] { endpoint1 }); + + // Act 1 + var finder = CreateEndpointFinder(dynamicDataSource); + + // Assert 1 + var match = Assert.Single(finder.Entries); + Assert.Same(endpoint1, match.Value.Single()); + + // Arrange 2 + var endpoint2 = EndpointFactory.CreateRouteEndpoint( + "/b", + metadata: new object[] { new EndpointNameMetadata("name2"), }); + + // Act 2 + // Trigger change + dynamicDataSource.AddEndpoint(endpoint2); + + // Assert 2 + Assert.Collection( + finder.Entries.OrderBy(kvp => kvp.Key), + (m) => + { + Assert.Same(endpoint1, m.Value.Single()); + }, + (m) => + { + Assert.Same(endpoint2, m.Value.Single()); + }); + } + + [Fact] + public void EndpointFinder_IgnoresEndpointsWithSuppressLinkGeneration() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint( + "/a", + metadata: new object[] { new EndpointNameMetadata("name1"), new SuppressLinkGenerationMetadata(), }); + + // Act + var finder = CreateEndpointFinder(endpoint); + + // Assert + Assert.Empty(finder.Entries); + } + + [Fact] + public void EndpointFinder_IgnoresEndpointsWithoutEndpointName() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint( + "/a", + metadata: new object[] { }); + + // Act + var finder = CreateEndpointFinder(endpoint); + + // Assert + Assert.Empty(finder.Entries); + } + + [Fact] + public void EndpointFinder_ThrowsExceptionForDuplicateEndpoints() + { + // Arrange + var endpoints = new Endpoint[] + { + EndpointFactory.CreateRouteEndpoint("/a", displayName: "a", metadata: new object[] { new EndpointNameMetadata("name1"), }), + EndpointFactory.CreateRouteEndpoint("/b", displayName: "b", metadata: new object[] { new EndpointNameMetadata("name1"), }), + EndpointFactory.CreateRouteEndpoint("/c", displayName: "c", metadata: new object[] { new EndpointNameMetadata("name1"), }), + + //// Not a duplicate + EndpointFactory.CreateRouteEndpoint("/d", displayName: "d", metadata: new object[] { new EndpointNameMetadata("NAME1"), }), + + EndpointFactory.CreateRouteEndpoint("/e", displayName: "e", metadata: new object[] { new EndpointNameMetadata("name2"), }), + EndpointFactory.CreateRouteEndpoint("/f", displayName: "f", metadata: new object[] { new EndpointNameMetadata("name2"), }), + }; + + var finder = CreateEndpointFinder(endpoints); + + // Act + var ex = Assert.Throws(() => finder.FindEndpoints("any name")); + + // Assert + Assert.Equal(@"The following endpoints with a duplicate endpoint name were found. + +Endpoints with endpoint name 'name1': +a +b +c + +Endpoints with endpoint name 'name2': +e +f +", ex.Message); + } + + private EndpointNameEndpointFinder CreateEndpointFinder(params Endpoint[] endpoints) + { + return CreateEndpointFinder(new DefaultEndpointDataSource(endpoints)); + } + + private EndpointNameEndpointFinder CreateEndpointFinder(params EndpointDataSource[] dataSources) + { + return new EndpointNameEndpointFinder(new CompositeEndpointDataSource(dataSources)); + } + } +} diff --git a/test/Microsoft.AspNetCore.Routing.Tests/LinkGeneratorEndpointNameExtensionsTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/LinkGeneratorEndpointNameExtensionsTest.cs new file mode 100644 index 0000000000..0b57b598c7 --- /dev/null +++ b/test/Microsoft.AspNetCore.Routing.Tests/LinkGeneratorEndpointNameExtensionsTest.cs @@ -0,0 +1,140 @@ +// 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.Linq; +using Microsoft.AspNetCore.Http; +using Xunit; + +namespace Microsoft.AspNetCore.Routing +{ + // Integration tests for GetXyzByName. 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 EndpointNameEndpointFinder in detail. see EndpointNameEndpointFinderTest + public class LinkGeneratorEndpointNameExtensionsTest : LinkGeneratorTestBase + { + [Fact] + public void GetPathByName_WithoutHttpContext_WithPathBaseAndFragment() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint("some-endpoint/{p}", metadata: new[] { new EndpointNameMetadata("name1"), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("some#-other-endpoint/{p}", metadata: new[] { new EndpointNameMetadata("name2"), }); + + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + + var values = new { p = "In?dex", query = "some?query", }; + + // Act + var path = linkGenerator.GetPathByName( + endpointName: "name2", + values, + new PathString("/Foo/Bar?encodeme?"), + new FragmentString("#Fragment?"), + new LinkOptions() { AppendTrailingSlash = true, }); + + // Assert + Assert.Equal("/Foo/Bar%3Fencodeme%3F/some%23-other-endpoint/In%3Fdex/?query=some%3Fquery#Fragment?", path); + } + + [Fact] + public void GetPathByName_WithHttpContext_WithPathBaseAndFragment() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint("some-endpoint/{p}", metadata: new[] { new EndpointNameMetadata("name1"), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("some#-other-endpoint/{p}", metadata: new[] { new EndpointNameMetadata("name2"), }); + + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + + var httpContext = CreateHttpContext(); + httpContext.Request.PathBase = new PathString("/Foo/Bar?encodeme?"); + + var values = new { p = "In?dex", query = "some?query", }; + + // Act + var path = linkGenerator.GetPathByName( + httpContext, + endpointName: "name2", + values, + new FragmentString("#Fragment?"), + new LinkOptions() { AppendTrailingSlash = true, }); + + // Assert + Assert.Equal("/Foo/Bar%3Fencodeme%3F/some%23-other-endpoint/In%3Fdex/?query=some%3Fquery#Fragment?", path); + } + + [Fact] + public void GetUriByRouteValues_WithoutHttpContext_WithPathBaseAndFragment() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint("some-endpoint/{p}", metadata: new[] { new EndpointNameMetadata("name1"), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("some#-other-endpoint/{p}", metadata: new[] { new EndpointNameMetadata("name2"), }); + + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + + var values = new { p = "In?dex", query = "some?query", }; + + // Act + var path = linkGenerator.GetUriByName( + endpointName: "name2", + values, + "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/some%23-other-endpoint/In%3Fdex/?query=some%3Fquery#Fragment?", path); + } + + [Fact] + public void GetUriByName_WithHttpContext_WithPathBaseAndFragment() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint("some-endpoint/{p}", metadata: new[] { new EndpointNameMetadata("name1"), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("some#-other-endpoint/{p}", metadata: new[] { new EndpointNameMetadata("name2"), }); + + 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?"); + + var values = new { p = "In?dex", query = "some?query", }; + + // Act + var uri = linkGenerator.GetUriByName( + httpContext, + endpointName: "name2", + values, + new FragmentString("#Fragment?"), + new LinkOptions() { AppendTrailingSlash = true, }); + + // Assert + Assert.Equal("http://example.com/Foo/Bar%3Fencodeme%3F/some%23-other-endpoint/In%3Fdex/?query=some%3Fquery#Fragment?", uri); + } + + [Fact] + public void GetTemplateByName_CreatesTemplate() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint("some-endpoint/{p}", metadata: new[] { new EndpointNameMetadata("name1"), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("some#-other-endpoint/{p}", metadata: new[] { new EndpointNameMetadata("name2"), }); + + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + + // Act + var template = linkGenerator.GetTemplateByName(endpointName: "name2"); + + // Assert + Assert.NotNull(template); + Assert.Collection( + Assert.IsType(template).Endpoints.Cast().OrderBy(e => e.RoutePattern.RawText), + e => Assert.Same(endpoint2, e)); + } + } +} diff --git a/test/Microsoft.AspNetCore.Routing.Tests/LinkGeneratorRouteValuesAddressExtensionsTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/LinkGeneratorRouteValuesAddressExtensionsTest.cs index d4ccf080d0..0d3d3d5a50 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/LinkGeneratorRouteValuesAddressExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/LinkGeneratorRouteValuesAddressExtensionsTest.cs @@ -1,6 +1,7 @@ // 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.Linq; using Microsoft.AspNetCore.Http; using Xunit; @@ -96,7 +97,7 @@ namespace Microsoft.AspNetCore.Routing } [Fact] - public void GetUri_WithHttpContext_WithPathBaseAndFragment() + public void GetUriByRouteValues_WithHttpContext_WithPathBaseAndFragment() { // Arrange var endpoint1 = EndpointFactory.CreateRouteEndpoint( @@ -124,5 +125,31 @@ namespace Microsoft.AspNetCore.Routing // Assert Assert.Equal("http://example.com/Foo/Bar%3Fencodeme%3F/Home/In%3Fdex/?query=some%3Fquery#Fragment?", uri); } + + [Fact] + public void GetTemplateByRouteValues_CreatesTemplate() + { + // 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 template = linkGenerator.GetTemplateByRouteValues( + routeName: null, + values: new RouteValueDictionary(new { controller = "Home", action = "In?dex", query = "some?query" })); + + // Assert + Assert.NotNull(template); + Assert.Collection( + Assert.IsType(template).Endpoints.Cast().OrderBy(e => e.RoutePattern.RawText), + e => Assert.Same(endpoint2, e), + e => Assert.Same(endpoint1, e)); + } } }