diff --git a/build/dependencies.props b/build/dependencies.props index 712c6521e9..4de4f3a0fd 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -48,8 +48,8 @@ 2.2.0-preview2-35143 2.2.0-preview2-35143 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 + 2.2.0-a-preview3-link-generator-16951 + 2.2.0-a-preview3-link-generator-16951 2.2.0-preview2-35143 2.2.0-preview2-35143 2.2.0-preview2-35143 diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Routing/EndpointRoutingUrlHelper.cs b/src/Microsoft.AspNetCore.Mvc.Core/Routing/EndpointRoutingUrlHelper.cs index 8ddd49c2f1..adc94a7d8e 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Routing/EndpointRoutingUrlHelper.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Routing/EndpointRoutingUrlHelper.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 Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Logging; @@ -51,46 +52,41 @@ namespace Microsoft.AspNetCore.Mvc.Routing throw new ArgumentNullException(nameof(urlActionContext)); } - var valuesDictionary = GetValuesDictionary(urlActionContext.Values); + var values = GetValuesDictionary(urlActionContext.Values); if (urlActionContext.Action == null) { - if (!valuesDictionary.ContainsKey("action") && + if (!values.ContainsKey("action") && AmbientValues.TryGetValue("action", out var action)) { - valuesDictionary["action"] = action; + values["action"] = action; } } else { - valuesDictionary["action"] = urlActionContext.Action; + values["action"] = urlActionContext.Action; } if (urlActionContext.Controller == null) { - if (!valuesDictionary.ContainsKey("controller") && + if (!values.ContainsKey("controller") && AmbientValues.TryGetValue("controller", out var controller)) { - valuesDictionary["controller"] = controller; + values["controller"] = controller; } } else { - valuesDictionary["controller"] = urlActionContext.Controller; + values["controller"] = urlActionContext.Controller; } - var successfullyGeneratedLink = _linkGenerator.TryGetLink( + + var path = _linkGenerator.GetPathByRouteValues( ActionContext.HttpContext, - valuesDictionary, - out var link); - if (!successfullyGeneratedLink) - { - //TODO: log here - - return null; - } - - return GenerateUrl(urlActionContext.Protocol, urlActionContext.Host, link, urlActionContext.Fragment); + routeName: null, + values, + new FragmentString(urlActionContext.Fragment == null ? null : "#" + urlActionContext.Fragment)); + return GenerateUrl(urlActionContext.Protocol, urlActionContext.Host, path); } /// @@ -101,20 +97,12 @@ namespace Microsoft.AspNetCore.Mvc.Routing throw new ArgumentNullException(nameof(routeContext)); } - var valuesDictionary = routeContext.Values as RouteValueDictionary ?? GetValuesDictionary(routeContext.Values); - - var successfullyGeneratedLink = _linkGenerator.TryGetLink( + var path = _linkGenerator.GetPathByRouteValues( ActionContext.HttpContext, routeContext.RouteName, - valuesDictionary, - out var link); - - if (!successfullyGeneratedLink) - { - return null; - } - - return GenerateUrl(routeContext.Protocol, routeContext.Host, link, routeContext.Fragment); + routeContext.Values, + new FragmentString(routeContext.Fragment == null ? null : "#" + routeContext.Fragment)); + return GenerateUrl(routeContext.Protocol, routeContext.Host, path); } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelperBase.cs b/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelperBase.cs index 730323f33b..a68c9c988b 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelperBase.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelperBase.cs @@ -201,6 +201,68 @@ namespace Microsoft.AspNetCore.Mvc.Routing } } + /// + /// Generates a URI from the provided components. + /// + /// The URI scheme/protocol. + /// The URI host. + /// The URI path and remaining portions (path, query, and fragment). + /// + /// An absolute URI if the or is specified, otherwise generates a + /// URI with an absolute path. + /// + protected string GenerateUrl(string protocol, string host, string path) + { + // This method is similar to GenerateUrl, but it's used for EndpointRouting. It ignores pathbase and fragment + // because those have already been incorporated. + if (path == null) + { + return null; + } + + // Perf: In most of the common cases, GenerateUrl is called with a null protocol, host and fragment. + // In such cases, we might not need to build any URL as the url generated is mostly same as the virtual path available in pathData. + // For such common cases, this FastGenerateUrl method saves a string allocation per GenerateUrl call. + string url; + if (TryFastGenerateUrl(protocol, host, path, fragment: null, out url)) + { + return url; + } + + var builder = GetStringBuilder(); + try + { + if (string.IsNullOrEmpty(protocol) && string.IsNullOrEmpty(host)) + { + AppendPathAndFragment(builder, pathBase: null, path, fragment: null); + + // We're returning a partial URL (just path + query + fragment), but we still want it to be rooted. + if (builder.Length == 0 || builder[0] != '/') + { + builder.Insert(0, '/'); + } + } + else + { + protocol = string.IsNullOrEmpty(protocol) ? "http" : protocol; + builder.Append(protocol); + + builder.Append("://"); + + host = string.IsNullOrEmpty(host) ? ActionContext.HttpContext.Request.Host.Value : host; + builder.Append(host); + AppendPathAndFragment(builder, pathBase: null, path, fragment: null); + } + + return builder.ToString(); + } + finally + { + // Clear the StringBuilder so that it can reused for the next call. + builder.Clear(); + } + } + // for unit testing internal static void AppendPathAndFragment(StringBuilder builder, PathString pathBase, string virtualPath, string fragment) { diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/MvcEndpointDataSourceTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/MvcEndpointDataSourceTests.cs index a6f92ddc2c..e15b8225e0 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/MvcEndpointDataSourceTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/MvcEndpointDataSourceTests.cs @@ -377,7 +377,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal var matcherEndpoint = Assert.IsType(endpoint); var routeValuesAddressNameMetadata = matcherEndpoint.Metadata.GetMetadata(); Assert.NotNull(routeValuesAddressNameMetadata); - Assert.Equal(string.Empty, routeValuesAddressNameMetadata.Name); + Assert.Equal(string.Empty, routeValuesAddressNameMetadata.RouteName); } [Fact] @@ -402,7 +402,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal var matcherEndpoint = Assert.IsType(ep); var routeValuesAddressMetadata = matcherEndpoint.Metadata.GetMetadata(); Assert.NotNull(routeValuesAddressMetadata); - Assert.Equal("namedRoute", routeValuesAddressMetadata.Name); + Assert.Equal("namedRoute", routeValuesAddressMetadata.RouteName); Assert.Equal("named/Home/Index/{id?}", matcherEndpoint.RoutePattern.RawText); }, (ep) => @@ -410,7 +410,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal var matcherEndpoint = Assert.IsType(ep); var routeValuesAddressMetadata = matcherEndpoint.Metadata.GetMetadata(); Assert.NotNull(routeValuesAddressMetadata); - Assert.Equal("namedRoute", routeValuesAddressMetadata.Name); + Assert.Equal("namedRoute", routeValuesAddressMetadata.RouteName); Assert.Equal("named/Products/Details/{id?}", matcherEndpoint.RoutePattern.RawText); }); }