Modify UrlHelper to use a single StringBuilder

This commit is contained in:
Pranav K 2016-01-06 12:47:36 -08:00
parent aa5a4d4af2
commit e078259547
2 changed files with 100 additions and 84 deletions

View File

@ -19,9 +19,7 @@ namespace Microsoft.AspNet.Mvc.Routing
/// Initializes a new instance of the <see cref="UrlHelper"/> class using the specified action context and /// Initializes a new instance of the <see cref="UrlHelper"/> class using the specified action context and
/// action selector. /// action selector.
/// </summary> /// </summary>
/// <param name="actionContext"> /// <param name="actionContext">The <see cref="Mvc.ActionContext"/> for the current request.</param>
/// The <see cref="Mvc.ActionContext"/> for the current request.
/// </param>
public UrlHelper(ActionContext actionContext) public UrlHelper(ActionContext actionContext)
{ {
if (actionContext == null) if (actionContext == null)
@ -79,13 +77,8 @@ namespace Microsoft.AspNet.Mvc.Routing
valuesDictionary["controller"] = actionContext.Controller; valuesDictionary["controller"] = actionContext.Controller;
} }
var path = GeneratePathFromRoute(routeName: null, values: valuesDictionary); var virtualPathData = GetVirtualPathData(routeName: null, values: valuesDictionary);
if (path == null) return GenerateUrl(actionContext.Protocol, actionContext.Host, virtualPathData, actionContext.Fragment);
{
return null;
}
return GenerateUrl(actionContext.Protocol, actionContext.Host, path, actionContext.Fragment);
} }
/// <inheritdoc /> /// <inheritdoc />
@ -110,61 +103,53 @@ namespace Microsoft.AspNet.Mvc.Routing
} }
var valuesDictionary = new RouteValueDictionary(routeContext.Values); var valuesDictionary = new RouteValueDictionary(routeContext.Values);
var virtualPathData = GetVirtualPathData(routeContext.RouteName, valuesDictionary);
var path = GeneratePathFromRoute(routeContext.RouteName, valuesDictionary); return GenerateUrl(routeContext.Protocol, routeContext.Host, virtualPathData, routeContext.Fragment);
if (path == null)
{
return null;
}
return GenerateUrl(routeContext.Protocol, routeContext.Host, path, routeContext.Fragment);
} }
/// <summary> /// <summary>
/// Generates the absolute path of the url for the specified route values by /// Gets the <see cref="VirtualPathData"/> for the specified route values by using the specified route name.
/// using the specified route name.
/// </summary> /// </summary>
/// <param name="routeName">The name of the route that is used to generate the URL.</param> /// <param name="routeName">The name of the route that is used to generate the <see cref="VirtualPathData"/>.
/// </param>
/// <param name="values">A dictionary that contains the parameters for a route.</param> /// <param name="values">A dictionary that contains the parameters for a route.</param>
/// <returns>The absolute path of the URL.</returns> /// <returns>The <see cref="VirtualPathData"/>.</returns>
protected virtual string GeneratePathFromRoute(string routeName, RouteValueDictionary values) protected virtual VirtualPathData GetVirtualPathData(string routeName, RouteValueDictionary values)
{ {
var context = new VirtualPathContext(HttpContext, AmbientValues, values, routeName); var context = new VirtualPathContext(HttpContext, AmbientValues, values, routeName);
var pathData = Router.GetVirtualPath(context); return Router.GetVirtualPath(context);
if (pathData == null) }
{
return null;
}
// VirtualPathData.VirtualPath returns string.Empty for null. // Internal for unit testing.
Debug.Assert(pathData.VirtualPath != null); internal void AppendPathAndFragment(StringBuilder builder, VirtualPathData pathData, string fragment)
{
var pathBase = HttpContext.Request.PathBase; var pathBase = HttpContext.Request.PathBase;
if (!pathBase.HasValue) if (!pathBase.HasValue)
{ {
if (pathData.VirtualPath.Length == 0) if (pathData.VirtualPath.Length == 0)
{ {
return "/"; builder.Append("/");
}
else if (!pathData.VirtualPath.StartsWith("/", StringComparison.Ordinal))
{
return "/" + pathData.VirtualPath;
} }
else else
{ {
return pathData.VirtualPath; if (!pathData.VirtualPath.StartsWith("/", StringComparison.Ordinal))
{
builder.Append("/");
}
builder.Append(pathData.VirtualPath);
} }
} }
else else
{ {
if (pathData.VirtualPath.Length == 0) if (pathData.VirtualPath.Length == 0)
{ {
return pathBase; builder.Append(pathBase.Value);
} }
else else
{ {
var builder = new StringBuilder( builder.Append(pathBase.Value);
pathBase.Value,
pathBase.Value.Length + pathData.VirtualPath.Length);
if (pathBase.Value.EndsWith("/", StringComparison.Ordinal)) if (pathBase.Value.EndsWith("/", StringComparison.Ordinal))
{ {
@ -177,10 +162,13 @@ namespace Microsoft.AspNet.Mvc.Routing
} }
builder.Append(pathData.VirtualPath); builder.Append(pathData.VirtualPath);
return builder.ToString();
} }
} }
if (!string.IsNullOrEmpty(fragment))
{
builder.Append("#").Append(fragment);
}
} }
/// <inheritdoc /> /// <inheritdoc />
@ -213,34 +201,47 @@ namespace Microsoft.AspNet.Mvc.Routing
}); });
} }
private string GenerateUrl(string protocol, string host, string path, string fragment) /// <summary>
/// Generates the URL using the specified components.
/// </summary>
/// <param name="protocol">The protocol.</param>
/// <param name="host">The host.</param>
/// <param name="pathData">The <see cref="VirtualPathData"/>.</param>
/// <param name="fragment">The URL fragment.</param>
/// <returns>The generated URL.</returns>
protected virtual string GenerateUrl(string protocol, string host, VirtualPathData pathData, string fragment)
{ {
Debug.Assert(path != null); if (pathData == null)
var url = path;
if (!string.IsNullOrEmpty(fragment))
{ {
url += "#" + fragment; return null;
} }
// VirtualPathData.VirtualPath returns string.Empty instead of null.
Debug.Assert(pathData.VirtualPath != null);
var builder = new StringBuilder();
if (string.IsNullOrEmpty(protocol) && string.IsNullOrEmpty(host)) if (string.IsNullOrEmpty(protocol) && string.IsNullOrEmpty(host))
{ {
// We're returning a partial url (just path + query + fragment), but we still want it AppendPathAndFragment(builder, pathData, fragment);
// to be rooted. // We're returning a partial URL (just path + query + fragment), but we still want it to be rooted.
if (!url.StartsWith("/", StringComparison.Ordinal)) if (builder.Length == 0 || builder[0] != '/')
{ {
url = "/" + url; builder.Insert(0, '/');
} }
return url;
} }
else else
{ {
protocol = string.IsNullOrEmpty(protocol) ? "http" : protocol; protocol = string.IsNullOrEmpty(protocol) ? "http" : protocol;
host = string.IsNullOrEmpty(host) ? HttpContext.Request.Host.Value : host; builder.Append(protocol);
url = protocol + "://" + host + url; builder.Append("://");
return url;
host = string.IsNullOrEmpty(host) ? HttpContext.Request.Host.Value : host;
builder.Append(host);
AppendPathAndFragment(builder, pathData, fragment);
} }
return builder.ToString();
} }
} }
} }

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.Builder; using Microsoft.AspNet.Builder;
@ -892,25 +893,50 @@ namespace Microsoft.AspNet.Mvc.Routing
[Theory] [Theory]
[MemberData(nameof(GeneratePathFromRoute_HandlesLeadingAndTrailingSlashesData))] [MemberData(nameof(GeneratePathFromRoute_HandlesLeadingAndTrailingSlashesData))]
public void GeneratePathFromRoute_HandlesLeadingAndTrailingSlashes( public void AppendPathAndFragment_HandlesLeadingAndTrailingSlashes(
string appBase, string appBase,
string virtualPath, string virtualPath,
string expected) string expected)
{ {
// Arrage // Arrage
var router = new Mock<IRouter>(); var router = Mock.Of<IRouter>();
router.Setup(r => r.GetVirtualPath(It.IsAny<VirtualPathContext>())) var pathData = new VirtualPathData(router, virtualPath)
.Returns(new VirtualPathData(router.Object, virtualPath) {
{ VirtualPath = virtualPath
VirtualPath = virtualPath };
}); var urlHelper = CreateUrlHelper(appBase, router);
var urlHelper = CreateUrlHelper(appBase, router.Object); var builder = new StringBuilder();
// Act // Act
var result = urlHelper.GeneratePathFromRoutePublic("some-name", new RouteValueDictionary()); urlHelper.AppendPathAndFragment(builder, pathData, string.Empty);
// Assert // Assert
Assert.Equal(expected, result); Assert.Equal(expected, builder.ToString());
}
[Theory]
[MemberData(nameof(GeneratePathFromRoute_HandlesLeadingAndTrailingSlashesData))]
public void AppendPathAndFragment_AppendsFragments(
string appBase,
string virtualPath,
string expected)
{
// Arrage
var fragmentValue = "fragment-value";
expected += $"#{fragmentValue}";
var router = Mock.Of<IRouter>();
var pathData = new VirtualPathData(router, virtualPath)
{
VirtualPath = virtualPath
};
var urlHelper = CreateUrlHelper(appBase, router);
var builder = new StringBuilder();
// Act
urlHelper.AppendPathAndFragment(builder, pathData, fragmentValue);
// Assert
Assert.Equal(expected, builder.ToString());
} }
private static HttpContext CreateHttpContext( private static HttpContext CreateHttpContext(
@ -939,13 +965,13 @@ namespace Microsoft.AspNet.Mvc.Routing
return new ActionContext(context, routeData, new ActionDescriptor()); return new ActionContext(context, routeData, new ActionDescriptor());
} }
private static TestableUrlHelper CreateUrlHelper() private static UrlHelper CreateUrlHelper()
{ {
var services = CreateServices(); var services = CreateServices();
var context = CreateHttpContext(services, string.Empty); var context = CreateHttpContext(services, string.Empty);
var actionContext = CreateActionContext(context); var actionContext = CreateActionContext(context);
return new TestableUrlHelper(actionContext); return new UrlHelper(actionContext);
} }
private static UrlHelper CreateUrlHelper(ActionContext context) private static UrlHelper CreateUrlHelper(ActionContext context)
@ -964,7 +990,7 @@ namespace Microsoft.AspNet.Mvc.Routing
return new UrlHelper(actionContext); return new UrlHelper(actionContext);
} }
private static TestableUrlHelper CreateUrlHelper(string host, string protocol, IRouter router) private static UrlHelper CreateUrlHelper(string host, string protocol, IRouter router)
{ {
var services = CreateServices(); var services = CreateServices();
var context = CreateHttpContext(services, string.Empty); var context = CreateHttpContext(services, string.Empty);
@ -973,19 +999,19 @@ namespace Microsoft.AspNet.Mvc.Routing
var actionContext = CreateActionContext(context, router); var actionContext = CreateActionContext(context, router);
return new TestableUrlHelper(actionContext); return new UrlHelper(actionContext);
} }
private static TestableUrlHelper CreateUrlHelper(string appBase, IRouter router) private static UrlHelper CreateUrlHelper(string appBase, IRouter router)
{ {
var services = CreateServices(); var services = CreateServices();
var context = CreateHttpContext(services, appBase); var context = CreateHttpContext(services, appBase);
var actionContext = CreateActionContext(context, router); var actionContext = CreateActionContext(context, router);
return new TestableUrlHelper(actionContext); return new UrlHelper(actionContext);
} }
private static TestableUrlHelper CreateUrlHelperWithRouteCollection( private static UrlHelper CreateUrlHelperWithRouteCollection(
IServiceProvider services, IServiceProvider services,
string appPrefix) string appPrefix)
{ {
@ -1066,16 +1092,5 @@ namespace Microsoft.AspNet.Mvc.Routing
return Task.FromResult(false); return Task.FromResult(false);
} }
} }
private class TestableUrlHelper : UrlHelper
{
public TestableUrlHelper(ActionContext context)
: base(context)
{
}
public string GeneratePathFromRoutePublic(string routeName, RouteValueDictionary values) =>
GeneratePathFromRoute(routeName, values);
}
} }
} }