diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelper.cs b/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelper.cs index 21d37a36ce..1cf815cb41 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelper.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelper.cs @@ -15,6 +15,10 @@ namespace Microsoft.AspNetCore.Mvc.Routing /// public class UrlHelper : IUrlHelper { + + // Perf: Share the StringBuilder object across multiple calls of GenerateURL for this UrlHelper + private StringBuilder _stringBuilder; + /// /// Initializes a new instance of the class using the specified action context and /// action selector. @@ -201,6 +205,16 @@ namespace Microsoft.AspNetCore.Mvc.Routing }); } + private StringBuilder GetStringBuilder() + { + if(_stringBuilder == null) + { + _stringBuilder = new StringBuilder(); + } + + return _stringBuilder; + } + /// /// Generates the URL using the specified components. /// @@ -219,29 +233,38 @@ namespace Microsoft.AspNetCore.Mvc.Routing // VirtualPathData.VirtualPath returns string.Empty instead of null. Debug.Assert(pathData.VirtualPath != null); - var builder = new StringBuilder(); - if (string.IsNullOrEmpty(protocol) && string.IsNullOrEmpty(host)) + var builder = GetStringBuilder(); + try { - AppendPathAndFragment(builder, pathData, fragment); - // We're returning a partial URL (just path + query + fragment), but we still want it to be rooted. - if (builder.Length == 0 || builder[0] != '/') + if (string.IsNullOrEmpty(protocol) && string.IsNullOrEmpty(host)) { - builder.Insert(0, '/'); + AppendPathAndFragment(builder, pathData, fragment); + // 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) ? HttpContext.Request.Host.Value : host; + builder.Append(host); + AppendPathAndFragment(builder, pathData, fragment); + } + + var path = builder.ToString(); + return path; } - else + finally { - protocol = string.IsNullOrEmpty(protocol) ? "http" : protocol; - builder.Append(protocol); - - builder.Append("://"); - - host = string.IsNullOrEmpty(host) ? HttpContext.Request.Host.Value : host; - builder.Append(host); - AppendPathAndFragment(builder, pathData, fragment); + // Clear the StringBuilder so that it can reused for the next call. + builder.Clear(); } - - return builder.ToString(); } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelperFactory.cs b/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelperFactory.cs index e90f140ee4..9c8962f15f 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelperFactory.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelperFactory.cs @@ -1,6 +1,10 @@ // 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; +using Microsoft.AspNetCore.Mvc.Core; + namespace Microsoft.AspNetCore.Mvc.Routing { /// @@ -11,7 +15,31 @@ namespace Microsoft.AspNetCore.Mvc.Routing /// public IUrlHelper GetUrlHelper(ActionContext context) { - return new UrlHelper(context); + var httpContext = context.HttpContext; + + if (httpContext == null) + { + throw new ArgumentException(Resources.FormatPropertyOfTypeCannotBeNull( + nameof(ActionContext.HttpContext), + nameof(ActionContext))); + } + + if (httpContext.Items == null) + { + throw new ArgumentException(Resources.FormatPropertyOfTypeCannotBeNull( + nameof(HttpContext.Items), + nameof(HttpContext))); + } + + // Perf: Create only one UrlHelper per context + var urlHelper = httpContext.Items[typeof(IUrlHelper)] as IUrlHelper; + if (urlHelper == null) + { + urlHelper = new UrlHelper(context); + httpContext.Items[typeof(IUrlHelper)] = urlHelper; + } + + return urlHelper; } } } diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/LocalRedirectResultTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/LocalRedirectResultTest.cs index 11a7e8d06e..1d2113b6e5 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/LocalRedirectResultTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/LocalRedirectResultTest.cs @@ -3,6 +3,7 @@ using System; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Internal; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Routing; @@ -120,6 +121,8 @@ namespace Microsoft.AspNetCore.Mvc .Returns(response); httpContext.SetupGet(o => o.RequestServices) .Returns(serviceProvider); + httpContext.SetupGet(o => o.Items) + .Returns(new ItemsDictionary()); httpContext.Setup(o => o.Request.PathBase) .Returns(new PathString(appRoot)); diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/RedirectResultTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/RedirectResultTest.cs index 11c743eff2..bede2f5978 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/RedirectResultTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/RedirectResultTest.cs @@ -3,6 +3,7 @@ using System; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Internal; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Routing; @@ -128,6 +129,8 @@ namespace Microsoft.AspNetCore.Mvc .Returns(response); httpContext.SetupGet(o => o.RequestServices) .Returns(serviceProvider); + httpContext.SetupGet(o => o.Items) + .Returns(new ItemsDictionary()); httpContext.Setup(o => o.Request.PathBase) .Returns(new PathString(appRoot));