Integrate Dispatcher's link generator

Related to https://github.com/aspnet/Routing/issues/530
This commit is contained in:
Kiran Challa 2018-06-14 17:46:28 -07:00 committed by Kiran Challa
parent 4634a97fae
commit a0a9c2c585
11 changed files with 2450 additions and 2031 deletions

View File

@ -0,0 +1,127 @@
// 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.Routing;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Mvc.Routing
{
/// <summary>
/// An implementation of <see cref="IUrlHelper"/> that uses <see cref="ILinkGenerator"/> to build URLs
/// for ASP.NET MVC within an application.
/// </summary>
internal class DispatcherUrlHelper : UrlHelperBase
{
private readonly ILogger<DispatcherUrlHelper> _logger;
private readonly ILinkGenerator _linkGenerator;
/// <summary>
/// Initializes a new instance of the <see cref="DispatcherUrlHelper"/> class using the specified
/// <paramref name="actionContext"/>.
/// </summary>
/// <param name="actionContext">The <see cref="Mvc.ActionContext"/> for the current request.</param>
/// <param name="linkGenerator">The <see cref="ILinkGenerator"/> used to generate the link.</param>
/// <param name="logger">The <see cref="ILogger"/>.</param>
public DispatcherUrlHelper(
ActionContext actionContext,
ILinkGenerator linkGenerator,
ILogger<DispatcherUrlHelper> logger)
: base(actionContext)
{
if (linkGenerator == null)
{
throw new ArgumentNullException(nameof(linkGenerator));
}
if (logger == null)
{
throw new ArgumentNullException(nameof(logger));
}
_linkGenerator = linkGenerator;
_logger = logger;
}
/// <inheritdoc />
public override string Action(UrlActionContext urlActionContext)
{
if (urlActionContext == null)
{
throw new ArgumentNullException(nameof(urlActionContext));
}
var valuesDictionary = GetValuesDictionary(urlActionContext.Values);
if (urlActionContext.Action == null)
{
if (!valuesDictionary.ContainsKey("action") &&
AmbientValues.TryGetValue("action", out var action))
{
valuesDictionary["action"] = action;
}
}
else
{
valuesDictionary["action"] = urlActionContext.Action;
}
if (urlActionContext.Controller == null)
{
if (!valuesDictionary.ContainsKey("controller") &&
AmbientValues.TryGetValue("controller", out var controller))
{
valuesDictionary["controller"] = controller;
}
}
else
{
valuesDictionary["controller"] = urlActionContext.Controller;
}
var successfullyGeneratedLink = _linkGenerator.TryGetLink(
new LinkGeneratorContext()
{
SuppliedValues = valuesDictionary,
AmbientValues = AmbientValues
},
out var link);
if (!successfullyGeneratedLink)
{
//TODO: log here
return null;
}
return GenerateUrl(urlActionContext.Protocol, urlActionContext.Host, link, urlActionContext.Fragment);
}
/// <inheritdoc />
public override string RouteUrl(UrlRouteContext routeContext)
{
if (routeContext == null)
{
throw new ArgumentNullException(nameof(routeContext));
}
var valuesDictionary = routeContext.Values as RouteValueDictionary ?? GetValuesDictionary(routeContext.Values);
var successfullyGeneratedLink = _linkGenerator.TryGetLink(
new LinkGeneratorContext()
{
Address = new Address(routeContext.RouteName),
SuppliedValues = valuesDictionary,
AmbientValues = AmbientValues
},
out var link);
if (!successfullyGeneratedLink)
{
return null;
}
return GenerateUrl(routeContext.Protocol, routeContext.Host, link, routeContext.Fragment);
}
}
}

View File

@ -2,9 +2,6 @@
// 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.Diagnostics;
using System.Text;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
@ -14,38 +11,18 @@ namespace Microsoft.AspNetCore.Mvc.Routing
/// An implementation of <see cref="IUrlHelper"/> that contains methods to
/// build URLs for ASP.NET MVC within an application.
/// </summary>
public class UrlHelper : IUrlHelper
public class UrlHelper : UrlHelperBase
{
// Perf: Share the StringBuilder object across multiple calls of GenerateURL for this UrlHelper
private StringBuilder _stringBuilder;
// Perf: Reuse the RouteValueDictionary across multiple calls of Action for this UrlHelper
private readonly RouteValueDictionary _routeValueDictionary;
/// <summary>
/// Initializes a new instance of the <see cref="UrlHelper"/> class using the specified
/// <paramref name="actionContext"/>.
/// </summary>
/// <param name="actionContext">The <see cref="Mvc.ActionContext"/> for the current request.</param>
public UrlHelper(ActionContext actionContext)
: base(actionContext)
{
if (actionContext == null)
{
throw new ArgumentNullException(nameof(actionContext));
}
ActionContext = actionContext;
_routeValueDictionary = new RouteValueDictionary();
}
/// <inheritdoc />
public ActionContext ActionContext { get; }
/// <summary>
/// Gets the <see cref="RouteValueDictionary"/> associated with the current request.
/// </summary>
protected RouteValueDictionary AmbientValues => ActionContext.RouteData.Values;
/// <summary>
/// Gets the <see cref="Http.HttpContext"/> associated with the current request.
/// </summary>
@ -58,7 +35,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
protected IRouter Router => ActionContext.RouteData.Routers[0];
/// <inheritdoc />
public virtual string Action(UrlActionContext actionContext)
public override string Action(UrlActionContext actionContext)
{
if (actionContext == null)
{
@ -69,9 +46,8 @@ namespace Microsoft.AspNetCore.Mvc.Routing
if (actionContext.Action == null)
{
object action;
if (!valuesDictionary.ContainsKey("action") &&
AmbientValues.TryGetValue("action", out action))
AmbientValues.TryGetValue("action", out var action))
{
valuesDictionary["action"] = action;
}
@ -83,9 +59,8 @@ namespace Microsoft.AspNetCore.Mvc.Routing
if (actionContext.Controller == null)
{
object controller;
if (!valuesDictionary.ContainsKey("controller") &&
AmbientValues.TryGetValue("controller", out controller))
AmbientValues.TryGetValue("controller", out var controller))
{
valuesDictionary["controller"] = controller;
}
@ -100,54 +75,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
}
/// <inheritdoc />
public virtual bool IsLocalUrl(string url)
{
if (string.IsNullOrEmpty(url))
{
return false;
}
// Allows "/" or "/foo" but not "//" or "/\".
if (url[0] == '/')
{
// url is exactly "/"
if (url.Length == 1)
{
return true;
}
// url doesn't start with "//" or "/\"
if (url[1] != '/' && url[1] != '\\')
{
return true;
}
return false;
}
// Allows "~/" or "~/foo" but not "~//" or "~/\".
if (url[0] == '~' && url.Length > 1 && url[1] == '/')
{
// url is exactly "~/"
if (url.Length == 2)
{
return true;
}
// url doesn't start with "~//" or "~/\"
if (url[2] != '/' && url[2] != '\\')
{
return true;
}
return false;
}
return false;
}
/// <inheritdoc />
public virtual string RouteUrl(UrlRouteContext routeContext)
public override string RouteUrl(UrlRouteContext routeContext)
{
if (routeContext == null)
{
@ -167,7 +95,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
/// </param>
/// <param name="values">
/// The <see cref="RouteValueDictionary"/>. The <see cref="Router"/> uses these values, in combination with
/// <see cref="AmbientValues"/>, to generate the URL.
/// <see cref="UrlHelperBase.AmbientValues"/>, to generate the URL.
/// </param>
/// <returns>The <see cref="VirtualPathData"/>.</returns>
protected virtual VirtualPathData GetVirtualPathData(string routeName, RouteValueDictionary values)
@ -176,128 +104,6 @@ namespace Microsoft.AspNetCore.Mvc.Routing
return Router.GetVirtualPath(context);
}
// Internal for unit testing.
internal void AppendPathAndFragment(StringBuilder builder, VirtualPathData pathData, string fragment)
{
var pathBase = HttpContext.Request.PathBase;
if (!pathBase.HasValue)
{
if (pathData.VirtualPath.Length == 0)
{
builder.Append("/");
}
else
{
if (!pathData.VirtualPath.StartsWith("/", StringComparison.Ordinal))
{
builder.Append("/");
}
builder.Append(pathData.VirtualPath);
}
}
else
{
if (pathData.VirtualPath.Length == 0)
{
builder.Append(pathBase.Value);
}
else
{
builder.Append(pathBase.Value);
if (pathBase.Value.EndsWith("/", StringComparison.Ordinal))
{
builder.Length--;
}
if (!pathData.VirtualPath.StartsWith("/", StringComparison.Ordinal))
{
builder.Append("/");
}
builder.Append(pathData.VirtualPath);
}
}
if (!string.IsNullOrEmpty(fragment))
{
builder.Append("#").Append(fragment);
}
}
/// <inheritdoc />
public virtual string Content(string contentPath)
{
if (string.IsNullOrEmpty(contentPath))
{
return null;
}
else if (contentPath[0] == '~')
{
var segment = new PathString(contentPath.Substring(1));
var applicationPath = HttpContext.Request.PathBase;
return applicationPath.Add(segment).Value;
}
return contentPath;
}
/// <inheritdoc />
public virtual string Link(string routeName, object values)
{
return RouteUrl(new UrlRouteContext()
{
RouteName = routeName,
Values = values,
Protocol = HttpContext.Request.Scheme,
Host = HttpContext.Request.Host.ToUriComponent()
});
}
private RouteValueDictionary GetValuesDictionary(object values)
{
// Perf: RouteValueDictionary can be cast to IDictionary<string, object>, but it is
// special cased to avoid allocating boxed Enumerator.
var routeValuesDictionary = values as RouteValueDictionary;
if (routeValuesDictionary != null)
{
_routeValueDictionary.Clear();
foreach (var kvp in routeValuesDictionary)
{
_routeValueDictionary.Add(kvp.Key, kvp.Value);
}
return _routeValueDictionary;
}
var dictionaryValues = values as IDictionary<string, object>;
if (dictionaryValues != null)
{
_routeValueDictionary.Clear();
foreach (var kvp in dictionaryValues)
{
_routeValueDictionary.Add(kvp.Key, kvp.Value);
}
return _routeValueDictionary;
}
return new RouteValueDictionary(values);
}
private StringBuilder GetStringBuilder()
{
if(_stringBuilder == null)
{
_stringBuilder = new StringBuilder();
}
return _stringBuilder;
}
/// <summary>
/// Generates the URL using the specified components.
/// </summary>
@ -308,85 +114,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
/// <returns>The generated URL.</returns>
protected virtual string GenerateUrl(string protocol, string host, VirtualPathData pathData, string fragment)
{
if (pathData == null)
{
return null;
}
// VirtualPathData.VirtualPath returns string.Empty instead of null.
Debug.Assert(pathData.VirtualPath != 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, pathData, fragment, out url))
{
return url;
}
var builder = GetStringBuilder();
try
{
if (string.IsNullOrEmpty(protocol) && string.IsNullOrEmpty(host))
{
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;
}
finally
{
// Clear the StringBuilder so that it can reused for the next call.
builder.Clear();
}
}
private bool TryFastGenerateUrl(
string protocol,
string host,
VirtualPathData pathData,
string fragment,
out string url)
{
var pathBase = HttpContext.Request.PathBase;
url = null;
if (string.IsNullOrEmpty(protocol)
&& string.IsNullOrEmpty(host)
&& string.IsNullOrEmpty(fragment)
&& !pathBase.HasValue)
{
if (pathData.VirtualPath.Length == 0)
{
url = "/";
return true;
}
else if (pathData.VirtualPath.StartsWith("/", StringComparison.Ordinal))
{
url = pathData.VirtualPath;
return true;
}
}
return false;
return GenerateUrl(protocol, host, pathData?.VirtualPath, fragment);
}
}
}

View File

@ -0,0 +1,293 @@
// 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.Text;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
namespace Microsoft.AspNetCore.Mvc.Routing
{
public abstract class UrlHelperBase : IUrlHelper
{
// Perf: Share the StringBuilder object across multiple calls of GenerateURL for this UrlHelper
private StringBuilder _stringBuilder;
// Perf: Reuse the RouteValueDictionary across multiple calls of Action for this UrlHelper
private readonly RouteValueDictionary _routeValueDictionary;
protected UrlHelperBase(ActionContext actionContext)
{
if (actionContext == null)
{
throw new ArgumentNullException(nameof(actionContext));
}
ActionContext = actionContext;
AmbientValues = actionContext.RouteData.Values;
_routeValueDictionary = new RouteValueDictionary();
}
/// <summary>
/// Gets the <see cref="RouteValueDictionary"/> associated with the current request.
/// </summary>
protected RouteValueDictionary AmbientValues { get; }
/// <inheritdoc />
public ActionContext ActionContext { get; }
/// <inheritdoc />
public virtual bool IsLocalUrl(string url)
{
if (string.IsNullOrEmpty(url))
{
return false;
}
// Allows "/" or "/foo" but not "//" or "/\".
if (url[0] == '/')
{
// url is exactly "/"
if (url.Length == 1)
{
return true;
}
// url doesn't start with "//" or "/\"
if (url[1] != '/' && url[1] != '\\')
{
return true;
}
return false;
}
// Allows "~/" or "~/foo" but not "~//" or "~/\".
if (url[0] == '~' && url.Length > 1 && url[1] == '/')
{
// url is exactly "~/"
if (url.Length == 2)
{
return true;
}
// url doesn't start with "~//" or "~/\"
if (url[2] != '/' && url[2] != '\\')
{
return true;
}
return false;
}
return false;
}
/// <inheritdoc />
public virtual string Content(string contentPath)
{
if (string.IsNullOrEmpty(contentPath))
{
return null;
}
else if (contentPath[0] == '~')
{
var segment = new PathString(contentPath.Substring(1));
var applicationPath = ActionContext.HttpContext.Request.PathBase;
return applicationPath.Add(segment).Value;
}
return contentPath;
}
/// <inheritdoc />
public virtual string Link(string routeName, object values)
{
return RouteUrl(new UrlRouteContext()
{
RouteName = routeName,
Values = values,
Protocol = ActionContext.HttpContext.Request.Scheme,
Host = ActionContext.HttpContext.Request.Host.ToUriComponent()
});
}
/// <inheritdoc />
public abstract string Action(UrlActionContext actionContext);
/// <inheritdoc />
public abstract string RouteUrl(UrlRouteContext routeContext);
protected RouteValueDictionary GetValuesDictionary(object values)
{
// Perf: RouteValueDictionary can be cast to IDictionary<string, object>, but it is
// special cased to avoid allocating boxed Enumerator.
if (values is RouteValueDictionary routeValuesDictionary)
{
_routeValueDictionary.Clear();
foreach (var kvp in routeValuesDictionary)
{
_routeValueDictionary.Add(kvp.Key, kvp.Value);
}
return _routeValueDictionary;
}
if (values is IDictionary<string, object> dictionaryValues)
{
_routeValueDictionary.Clear();
foreach (var kvp in dictionaryValues)
{
_routeValueDictionary.Add(kvp.Key, kvp.Value);
}
return _routeValueDictionary;
}
return new RouteValueDictionary(values);
}
protected string GenerateUrl(string protocol, string host, string virtualPath, string fragment)
{
if (virtualPath == 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, virtualPath, fragment, out url))
{
return url;
}
var builder = GetStringBuilder();
try
{
var pathBase = ActionContext.HttpContext.Request.PathBase;
if (string.IsNullOrEmpty(protocol) && string.IsNullOrEmpty(host))
{
AppendPathAndFragment(builder, pathBase, virtualPath, 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) ? ActionContext.HttpContext.Request.Host.Value : host;
builder.Append(host);
AppendPathAndFragment(builder, pathBase, virtualPath, fragment);
}
var path = builder.ToString();
return path;
}
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)
{
if (!pathBase.HasValue)
{
if (virtualPath.Length == 0)
{
builder.Append("/");
}
else
{
if (!virtualPath.StartsWith("/", StringComparison.Ordinal))
{
builder.Append("/");
}
builder.Append(virtualPath);
}
}
else
{
if (virtualPath.Length == 0)
{
builder.Append(pathBase.Value);
}
else
{
builder.Append(pathBase.Value);
if (pathBase.Value.EndsWith("/", StringComparison.Ordinal))
{
builder.Length--;
}
if (!virtualPath.StartsWith("/", StringComparison.Ordinal))
{
builder.Append("/");
}
builder.Append(virtualPath);
}
}
if (!string.IsNullOrEmpty(fragment))
{
builder.Append("#").Append(fragment);
}
}
private bool TryFastGenerateUrl(
string protocol,
string host,
string virtualPath,
string fragment,
out string url)
{
var pathBase = ActionContext.HttpContext.Request.PathBase;
url = null;
if (string.IsNullOrEmpty(protocol)
&& string.IsNullOrEmpty(host)
&& string.IsNullOrEmpty(fragment)
&& !pathBase.HasValue)
{
if (virtualPath.Length == 0)
{
url = "/";
return true;
}
else if (virtualPath.StartsWith("/", StringComparison.Ordinal))
{
url = virtualPath;
return true;
}
}
return false;
}
private StringBuilder GetStringBuilder()
{
if (_stringBuilder == null)
{
_stringBuilder = new StringBuilder();
}
return _stringBuilder;
}
}
}

View File

@ -4,6 +4,9 @@
using System;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Mvc.Routing
{
@ -37,16 +40,27 @@ namespace Microsoft.AspNetCore.Mvc.Routing
}
// Perf: Create only one UrlHelper per context
object value;
if (httpContext.Items.TryGetValue(typeof(IUrlHelper), out value) && value is IUrlHelper)
if (httpContext.Items.TryGetValue(typeof(IUrlHelper), out var value) && value is IUrlHelper)
{
return (IUrlHelper)value;
}
var urlHelper = new UrlHelper(context);
IUrlHelper urlHelper;
var endpointFeature = httpContext.Features.Get<IEndpointFeature>();
if (endpointFeature?.Endpoint != null)
{
var linkGenerator = httpContext.RequestServices.GetRequiredService<ILinkGenerator>();
var logger = httpContext.RequestServices.GetRequiredService<ILogger<DispatcherUrlHelper>>();
urlHelper = new DispatcherUrlHelper(context, linkGenerator, logger);
}
else
{
urlHelper = new UrlHelper(context);
}
httpContext.Items[typeof(IUrlHelper)] = urlHelper;
return urlHelper;
}
}
}
}

View File

@ -4,6 +4,7 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Infrastructure;
@ -168,13 +169,15 @@ namespace Microsoft.AspNetCore.Mvc
var serviceProvider = GetServiceProvider();
httpContext.Setup(o => o.Response)
.Returns(response);
.Returns(response);
httpContext.SetupGet(o => o.RequestServices)
.Returns(serviceProvider);
.Returns(serviceProvider);
httpContext.SetupGet(o => o.Items)
.Returns(new ItemsDictionary());
.Returns(new ItemsDictionary());
httpContext.Setup(o => o.Request.PathBase)
.Returns(new PathString(appRoot));
.Returns(new PathString(appRoot));
httpContext.SetupGet(h => h.Features)
.Returns(new FeatureCollection());
return httpContext.Object;
}

View File

@ -4,10 +4,10 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
@ -146,13 +146,15 @@ namespace Microsoft.AspNetCore.Mvc
var serviceProvider = GetServiceProvider();
httpContext.Setup(o => o.Response)
.Returns(response);
.Returns(response);
httpContext.SetupGet(o => o.RequestServices)
.Returns(serviceProvider);
.Returns(serviceProvider);
httpContext.SetupGet(o => o.Items)
.Returns(new ItemsDictionary());
.Returns(new ItemsDictionary());
httpContext.Setup(o => o.Request.PathBase)
.Returns(new PathString(appRoot));
.Returns(new PathString(appRoot));
httpContext.SetupGet(h => h.Features)
.Returns(new FeatureCollection());
return httpContext.Object;
}

View File

@ -0,0 +1,150 @@
// 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.Threading.Tasks;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Matchers;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Routing
{
public class DispatcherUrlHelperTest : UrlHelperTestBase
{
protected override IUrlHelper CreateUrlHelper(string appRoot, string host, string protocol)
{
return CreateUrlHelper(Enumerable.Empty<MatcherEndpoint>(), appRoot, host, protocol);
}
protected override IUrlHelper CreateUrlHelperWithDefaultRoutes(string appRoot, string host, string protocol)
{
return CreateUrlHelper(GetDefaultEndpoints(), appRoot, host, protocol);
}
protected override IUrlHelper CreateUrlHelperWithDefaultRoutes(
string appRoot,
string host,
string protocol,
string routeName,
string template)
{
var endpoints = GetDefaultEndpoints();
endpoints.Add(new MatcherEndpoint(
next => httpContext => Task.CompletedTask,
template,
null,
0,
EndpointMetadataCollection.Empty,
null,
new Address(routeName)
));
return CreateUrlHelper(endpoints, appRoot, host, protocol);
}
protected override IUrlHelper CreateUrlHelper(ActionContext actionContext)
{
var httpContext = actionContext.HttpContext;
httpContext.Features.Set<IEndpointFeature>(new EndpointFeature()
{
Endpoint = new MatcherEndpoint(
next => cntxt => Task.CompletedTask,
"/",
new { },
0,
EndpointMetadataCollection.Empty,
null,
null)
});
var urlHelperFactory = httpContext.RequestServices.GetRequiredService<IUrlHelperFactory>();
var urlHelper = urlHelperFactory.GetUrlHelper(actionContext);
Assert.IsType<DispatcherUrlHelper>(urlHelper);
return urlHelper;
}
protected override IServiceProvider CreateServices()
{
return CreateServices(Enumerable.Empty<Endpoint>());
}
protected override IUrlHelper CreateUrlHelper(
string appRoot,
string host,
string protocol,
string routeName,
string template,
object defaults)
{
var endpoint = GetEndpoint(routeName, template, defaults);
var services = CreateServices(new[] { endpoint });
var httpContext = CreateHttpContext(services, appRoot: "", host: null, protocol: null);
var actionContext = CreateActionContext(httpContext);
return CreateUrlHelper(actionContext);
}
private IUrlHelper CreateUrlHelper(
IEnumerable<MatcherEndpoint> endpoints,
string appRoot,
string host,
string protocol)
{
var serviceProvider = CreateServices(endpoints);
var httpContext = CreateHttpContext(serviceProvider, appRoot, host, protocol);
var actionContext = CreateActionContext(httpContext);
return CreateUrlHelper(actionContext);
}
private List<MatcherEndpoint> GetDefaultEndpoints()
{
var endpoints = new List<MatcherEndpoint>();
endpoints.Add(new MatcherEndpoint(
next => (httpContext) => Task.CompletedTask,
"{controller}/{action}/{id}",
new { id = "defaultid" },
0,
EndpointMetadataCollection.Empty,
"RouteWithNoName",
address: null));
endpoints.Add(new MatcherEndpoint(
next => (httpContext) => Task.CompletedTask,
"named/{controller}/{action}/{id}",
new { id = "defaultid" },
0,
EndpointMetadataCollection.Empty,
"RouteWithNoName",
new Address("namedroute")));
return endpoints;
}
private IServiceProvider CreateServices(IEnumerable<Endpoint> endpoints)
{
if (endpoints == null)
{
endpoints = Enumerable.Empty<Endpoint>();
}
var services = GetCommonServices();
services.AddDispatcher();
services.TryAddEnumerable(
ServiceDescriptor.Singleton<EndpointDataSource>(new DefaultEndpointDataSource(endpoints)));
services.TryAddSingleton<IUrlHelperFactory, UrlHelperFactory>();
return services.BuildServiceProvider();
}
private MatcherEndpoint GetEndpoint(string name, string template, object defaults)
{
return new MatcherEndpoint(
next => c => Task.CompletedTask,
template,
defaults,
0,
EndpointMetadataCollection.Empty,
null,
new Address(name));
}
}
}

View File

@ -0,0 +1,171 @@
// 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.Text;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.ObjectPool;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Routing
{
public class UrlHelperBaseTest
{
public static TheoryData GeneratePathFromRoute_HandlesLeadingAndTrailingSlashesData =>
new TheoryData<string, string, string>
{
{ null, "", "/" },
{ null, "/", "/" },
{ null, "Hello", "/Hello" },
{ null, "/Hello", "/Hello" },
{ "/", "", "/" },
{ "/", "hello", "/hello" },
{ "/", "/hello", "/hello" },
{ "/hello", "", "/hello" },
{ "/hello/", "", "/hello/" },
{ "/hello", "/", "/hello/" },
{ "/hello/", "world", "/hello/world" },
{ "/hello/", "/world", "/hello/world" },
{ "/hello/", "/world 123", "/hello/world 123" },
{ "/hello/", "/world%20123", "/hello/world%20123" },
};
[Theory]
[MemberData(nameof(GeneratePathFromRoute_HandlesLeadingAndTrailingSlashesData))]
public void AppendPathAndFragment_HandlesLeadingAndTrailingSlashes(
string appBase,
string virtualPath,
string expected)
{
// Arrange
var services = CreateServices();
var httpContext = CreateHttpContext(services, appBase, host: null, protocol: null);
var builder = new StringBuilder();
// Act
UrlHelperBase.AppendPathAndFragment(builder, httpContext.Request.PathBase, virtualPath, string.Empty);
// Assert
Assert.Equal(expected, builder.ToString());
}
[Theory]
[MemberData(nameof(GeneratePathFromRoute_HandlesLeadingAndTrailingSlashesData))]
public void AppendPathAndFragment_AppendsFragments(
string appBase,
string virtualPath,
string expected)
{
// Arrange
var fragmentValue = "fragment-value";
expected += $"#{fragmentValue}";
var services = CreateServices();
var httpContext = CreateHttpContext(services, appBase, host: null, protocol: null);
var builder = new StringBuilder();
// Act
UrlHelperBase.AppendPathAndFragment(builder, httpContext.Request.PathBase, virtualPath, fragmentValue);
// Assert
Assert.Equal(expected, builder.ToString());
}
[Theory]
[InlineData(null, null, null, "/", null, "/")]
[InlineData(null, null, null, "/Hello", null, "/Hello")]
[InlineData(null, null, null, "Hello", null, "/Hello")]
[InlineData("/", null, null, "", null, "/")]
[InlineData("/hello/", null, null, "/world", null, "/hello/world")]
[InlineData("/hello/", "https", "myhost", "/world", "fragment-value", "https://myhost/hello/world#fragment-value")]
public void GenerateUrl_FastAndSlowPathsReturnsExpected(
string appBase,
string protocol,
string host,
string virtualPath,
string fragment,
string expected)
{
// Arrange
var services = CreateServices();
var httpContext = CreateHttpContext(services, appBase, host, protocol);
var actionContext = CreateActionContext(httpContext);
var urlHelper = new TestUrlHelper(actionContext);
// Act
var url = urlHelper.GenerateUrl(protocol, host, virtualPath, fragment);
// Assert
Assert.Equal(expected, url);
}
private static IServiceProvider CreateServices()
{
var services = new ServiceCollection();
services.AddOptions();
services.AddLogging();
services.AddRouting();
services
.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>()
.AddSingleton<UrlEncoder>(UrlEncoder.Default);
return services.BuildServiceProvider();
}
private static HttpContext CreateHttpContext(
IServiceProvider services,
string appRoot,
string host,
string protocol)
{
appRoot = string.IsNullOrEmpty(appRoot) ? string.Empty : appRoot;
host = string.IsNullOrEmpty(host) ? "localhost" : host;
var context = new DefaultHttpContext();
context.RequestServices = services;
context.Request.PathBase = new PathString(appRoot);
context.Request.Host = new HostString(host);
context.Request.Scheme = protocol;
return context;
}
private static ActionContext CreateActionContext(HttpContext context)
{
return new ActionContext(context, new RouteData(), new ActionDescriptor());
}
private class TestUrlHelper : UrlHelperBase
{
public TestUrlHelper(ActionContext actionContext) :
base(actionContext)
{
}
public override string Action(UrlActionContext actionContext)
{
throw new NotImplementedException();
}
public override string RouteUrl(UrlRouteContext routeContext)
{
throw new NotImplementedException();
}
public new string GenerateUrl(
string protocol,
string host,
string virtualPath,
string fragment)
{
return base.GenerateUrl(
protocol,
host,
virtualPath,
fragment);
}
}
}
}

View File

@ -0,0 +1,585 @@
// 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 Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Routing;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Core.Test.Routing
{
public class UrlHelperExtensionsTest
{
[Fact]
public void Page_WithName_Works()
{
// Arrange
UrlRouteContext actual = null;
var routeData = new RouteData
{
Values =
{
{ "page", "ambient-page" },
}
};
var actionContext = new ActionContext
{
RouteData = routeData,
};
var urlHelper = CreateMockUrlHelper(actionContext);
urlHelper.Setup(h => h.RouteUrl(It.IsAny<UrlRouteContext>()))
.Callback((UrlRouteContext context) => actual = context);
// Act
urlHelper.Object.Page("/TestPage");
// Assert
urlHelper.Verify();
Assert.NotNull(actual);
Assert.Null(actual.RouteName);
Assert.Collection(Assert.IsType<RouteValueDictionary>(actual.Values),
value =>
{
Assert.Equal("page", value.Key);
Assert.Equal("/TestPage", value.Value);
});
Assert.Null(actual.Host);
Assert.Null(actual.Protocol);
Assert.Null(actual.Fragment);
}
public static TheoryData Page_WithNameAndRouteValues_WorksData
{
get => new TheoryData<object>
{
{ new { id = 10 } },
{
new Dictionary<string, object>
{
["id"] = 10,
}
},
{
new RouteValueDictionary
{
["id"] = 10,
}
},
};
}
[Theory]
[MemberData(nameof(Page_WithNameAndRouteValues_WorksData))]
public void Page_WithNameAndRouteValues_Works(object values)
{
// Arrange
UrlRouteContext actual = null;
var urlHelper = CreateMockUrlHelper();
urlHelper.Setup(h => h.RouteUrl(It.IsAny<UrlRouteContext>()))
.Callback((UrlRouteContext context) => actual = context);
// Act
urlHelper.Object.Page("/TestPage", values);
// Assert
urlHelper.Verify();
Assert.NotNull(actual);
Assert.Null(actual.RouteName);
Assert.Collection(Assert.IsType<RouteValueDictionary>(actual.Values),
value =>
{
Assert.Equal("id", value.Key);
Assert.Equal(10, value.Value);
},
value =>
{
Assert.Equal("page", value.Key);
Assert.Equal("/TestPage", value.Value);
});
Assert.Null(actual.Host);
Assert.Null(actual.Protocol);
Assert.Null(actual.Fragment);
}
[Fact]
public void Page_WithNameRouteValuesAndProtocol_Works()
{
// Arrange
UrlRouteContext actual = null;
var urlHelper = CreateMockUrlHelper();
urlHelper.Setup(h => h.RouteUrl(It.IsAny<UrlRouteContext>()))
.Callback((UrlRouteContext context) => actual = context);
// Act
urlHelper.Object.Page("/TestPage", pageHandler: null, values: new { id = 13 }, protocol: "https");
// Assert
urlHelper.Verify();
Assert.NotNull(actual);
Assert.Null(actual.RouteName);
Assert.Collection(Assert.IsType<RouteValueDictionary>(actual.Values),
value =>
{
Assert.Equal("id", value.Key);
Assert.Equal(13, value.Value);
},
value =>
{
Assert.Equal("page", value.Key);
Assert.Equal("/TestPage", value.Value);
});
Assert.Equal("https", actual.Protocol);
Assert.Null(actual.Host);
Assert.Null(actual.Fragment);
}
[Fact]
public void Page_WithNameRouteValuesProtocolAndHost_Works()
{
// Arrange
UrlRouteContext actual = null;
var urlHelper = CreateMockUrlHelper();
urlHelper.Setup(h => h.RouteUrl(It.IsAny<UrlRouteContext>()))
.Callback((UrlRouteContext context) => actual = context);
// Act
urlHelper.Object.Page("/TestPage", pageHandler: null, values: new { id = 13 }, protocol: "https", host: "mytesthost");
// Assert
urlHelper.Verify();
Assert.NotNull(actual);
Assert.Null(actual.RouteName);
Assert.Collection(Assert.IsType<RouteValueDictionary>(actual.Values),
value =>
{
Assert.Equal("id", value.Key);
Assert.Equal(13, value.Value);
},
value =>
{
Assert.Equal("page", value.Key);
Assert.Equal("/TestPage", value.Value);
});
Assert.Equal("https", actual.Protocol);
Assert.Equal("mytesthost", actual.Host);
Assert.Null(actual.Fragment);
}
[Fact]
public void Page_WithNameRouteValuesProtocolHostAndFragment_Works()
{
// Arrange
UrlRouteContext actual = null;
var urlHelper = CreateMockUrlHelper();
urlHelper.Setup(h => h.RouteUrl(It.IsAny<UrlRouteContext>()))
.Callback((UrlRouteContext context) => actual = context);
// Act
urlHelper.Object.Page("/TestPage", "test-handler", new { id = 13 }, "https", "mytesthost", "#toc");
// Assert
urlHelper.Verify();
Assert.NotNull(actual);
Assert.Null(actual.RouteName);
Assert.Collection(Assert.IsType<RouteValueDictionary>(actual.Values),
value =>
{
Assert.Equal("id", value.Key);
Assert.Equal(13, value.Value);
},
value =>
{
Assert.Equal("page", value.Key);
Assert.Equal("/TestPage", value.Value);
},
value =>
{
Assert.Equal("handler", value.Key);
Assert.Equal("test-handler", value.Value);
});
Assert.Equal("https", actual.Protocol);
Assert.Equal("mytesthost", actual.Host);
Assert.Equal("#toc", actual.Fragment);
}
[Fact]
public void Page_UsesAmbientRouteValue_WhenPageIsNull()
{
// Arrange
UrlRouteContext actual = null;
var routeData = new RouteData
{
Values =
{
{ "page", "ambient-page" },
}
};
var actionContext = new ActionContext
{
RouteData = routeData,
};
var urlHelper = CreateMockUrlHelper(actionContext);
urlHelper.Setup(h => h.RouteUrl(It.IsAny<UrlRouteContext>()))
.Callback((UrlRouteContext context) => actual = context);
// Act
string page = null;
urlHelper.Object.Page(page, new { id = 13 });
// Assert
urlHelper.Verify();
Assert.NotNull(actual);
Assert.Null(actual.RouteName);
Assert.Collection(Assert.IsType<RouteValueDictionary>(actual.Values),
value =>
{
Assert.Equal("id", value.Key);
Assert.Equal(13, value.Value);
},
value =>
{
Assert.Equal("page", value.Key);
Assert.Equal("ambient-page", value.Value);
});
}
[Fact]
public void Page_SetsHandlerToNull_IfValueIsNotSpecifiedInRouteValues()
{
// Arrange
UrlRouteContext actual = null;
var routeData = new RouteData
{
Values =
{
{ "page", "ambient-page" },
{ "handler", "ambient-handler" },
}
};
var actionContext = new ActionContext
{
RouteData = routeData,
};
var urlHelper = CreateMockUrlHelper(actionContext);
urlHelper.Setup(h => h.RouteUrl(It.IsAny<UrlRouteContext>()))
.Callback((UrlRouteContext context) => actual = context);
// Act
string page = null;
urlHelper.Object.Page(page, new { id = 13 });
// Assert
urlHelper.Verify();
Assert.NotNull(actual);
Assert.Null(actual.RouteName);
Assert.Collection(Assert.IsType<RouteValueDictionary>(actual.Values),
value =>
{
Assert.Equal("id", value.Key);
Assert.Equal(13, value.Value);
},
value =>
{
Assert.Equal("page", value.Key);
Assert.Equal("ambient-page", value.Value);
},
value =>
{
Assert.Equal("handler", value.Key);
Assert.Null(value.Value);
});
}
[Fact]
public void Page_UsesExplicitlySpecifiedHandlerValue()
{
// Arrange
UrlRouteContext actual = null;
var routeData = new RouteData
{
Values =
{
{ "page", "ambient-page" },
{ "handler", "ambient-handler" },
}
};
var actionContext = new ActionContext
{
RouteData = routeData,
};
var urlHelper = CreateMockUrlHelper(actionContext);
urlHelper.Setup(h => h.RouteUrl(It.IsAny<UrlRouteContext>()))
.Callback((UrlRouteContext context) => actual = context);
// Act
string page = null;
urlHelper.Object.Page(page, "exact-handler", new { handler = "route-value-handler" });
// Assert
urlHelper.Verify();
Assert.NotNull(actual);
Assert.Null(actual.RouteName);
Assert.Collection(Assert.IsType<RouteValueDictionary>(actual.Values),
value =>
{
Assert.Equal("handler", value.Key);
Assert.Equal("exact-handler", value.Value);
},
value =>
{
Assert.Equal("page", value.Key);
Assert.Equal("ambient-page", value.Value);
});
}
[Fact]
public void Page_UsesValueFromRouteValueIfPageHandlerIsNotExplicitySpecified()
{
// Arrange
UrlRouteContext actual = null;
var routeData = new RouteData
{
Values =
{
{ "page", "ambient-page" },
{ "handler", "ambient-handler" },
}
};
var actionContext = new ActionContext
{
RouteData = routeData,
};
var urlHelper = CreateMockUrlHelper(actionContext);
urlHelper.Setup(h => h.RouteUrl(It.IsAny<UrlRouteContext>()))
.Callback((UrlRouteContext context) => actual = context);
// Act
string page = null;
urlHelper.Object.Page(page, pageHandler: null, values: new { handler = "route-value-handler" });
// Assert
urlHelper.Verify();
Assert.NotNull(actual);
Assert.Null(actual.RouteName);
Assert.Collection(Assert.IsType<RouteValueDictionary>(actual.Values),
value =>
{
Assert.Equal("handler", value.Key);
Assert.Equal("route-value-handler", value.Value);
},
value =>
{
Assert.Equal("page", value.Key);
Assert.Equal("ambient-page", value.Value);
});
}
[Theory]
[InlineData("Sibling", "/Dir1/Dir2/Sibling")]
[InlineData("Dir3/Sibling", "/Dir1/Dir2/Dir3/Sibling")]
[InlineData("Dir4/Dir5/Index", "/Dir1/Dir2/Dir4/Dir5/Index")]
public void Page_CalculatesPathRelativeToViewEnginePath_WhenNotRooted(string pageName, string expected)
{
// Arrange
UrlRouteContext actual = null;
var routeData = new RouteData();
var actionContext = GetActionContextForPage("/Dir1/Dir2/About");
var urlHelper = CreateMockUrlHelper(actionContext);
urlHelper.Setup(h => h.RouteUrl(It.IsAny<UrlRouteContext>()))
.Callback((UrlRouteContext context) => actual = context);
// Act
urlHelper.Object.Page(pageName);
// Assert
urlHelper.Verify();
Assert.NotNull(actual);
Assert.Null(actual.RouteName);
Assert.Collection(Assert.IsType<RouteValueDictionary>(actual.Values),
value =>
{
Assert.Equal("page", value.Key);
Assert.Equal(expected, value.Value);
});
}
[Fact]
public void Page_CalculatesPathRelativeToViewEnginePath_ForIndexPagePaths()
{
// Arrange
var expected = "/Dir1/Dir2/Sibling";
UrlRouteContext actual = null;
var actionContext = GetActionContextForPage("/Dir1/Dir2/");
var urlHelper = CreateMockUrlHelper(actionContext);
urlHelper.Setup(h => h.RouteUrl(It.IsAny<UrlRouteContext>()))
.Callback((UrlRouteContext context) => actual = context);
// Act
urlHelper.Object.Page("Sibling");
// Assert
urlHelper.Verify();
Assert.NotNull(actual);
Assert.Null(actual.RouteName);
Assert.Collection(Assert.IsType<RouteValueDictionary>(actual.Values),
value =>
{
Assert.Equal("page", value.Key);
Assert.Equal(expected, value.Value);
});
}
[Fact]
public void Page_CalculatesPathRelativeToViewEnginePath_WhenNotRooted_ForPageAtRoot()
{
// Arrange
var expected = "/SiblingName";
UrlRouteContext actual = null;
var routeData = new RouteData();
var actionContext = new ActionContext
{
ActionDescriptor = new ActionDescriptor
{
RouteValues = new Dictionary<string, string>
{
{ "page", "/Home" },
},
},
RouteData = new RouteData
{
Values =
{
[ "page" ] = "/Home"
},
},
};
var urlHelper = CreateMockUrlHelper(actionContext);
urlHelper.Setup(h => h.RouteUrl(It.IsAny<UrlRouteContext>()))
.Callback((UrlRouteContext context) => actual = context);
// Act
urlHelper.Object.Page("SiblingName");
// Assert
urlHelper.Verify();
Assert.NotNull(actual);
Assert.Null(actual.RouteName);
Assert.Collection(Assert.IsType<RouteValueDictionary>(actual.Values),
value =>
{
Assert.Equal("page", value.Key);
Assert.Equal(expected, value.Value);
});
}
[Fact]
public void Page_Throws_IfRouteValueDoesNotIncludePageKey()
{
// Arrange
var expected = "SiblingName";
UrlRouteContext actual = null;
var routeData = new RouteData();
var actionContext = new ActionContext
{
RouteData = new RouteData(),
};
var urlHelper = CreateMockUrlHelper(actionContext);
urlHelper.Setup(h => h.RouteUrl(It.IsAny<UrlRouteContext>()))
.Callback((UrlRouteContext context) => actual = context);
// Act & Assert
var ex = Assert.Throws<InvalidOperationException>(() => urlHelper.Object.Page(expected));
Assert.Equal($"The relative page path '{expected}' can only be used while executing a Razor Page. " +
"Specify a root relative path with a leading '/' to generate a URL outside of a Razor Page.", ex.Message);
}
[Fact]
public void Page_UsesAreaValueFromRouteValueIfSpecified()
{
// Arrange
UrlRouteContext actual = null;
var routeData = new RouteData
{
Values =
{
{ "page", "ambient-page" },
{ "area", "ambient-area" },
}
};
var actionContext = new ActionContext
{
RouteData = routeData,
};
var urlHelper = CreateMockUrlHelper(actionContext);
urlHelper.Setup(h => h.RouteUrl(It.IsAny<UrlRouteContext>()))
.Callback((UrlRouteContext context) => actual = context);
// Act
string page = null;
urlHelper.Object.Page(page, values: new { area = "specified-area" });
// Assert
urlHelper.Verify();
Assert.NotNull(actual);
Assert.Null(actual.RouteName);
Assert.Collection(Assert.IsType<RouteValueDictionary>(actual.Values).OrderBy(v => v.Key),
value =>
{
Assert.Equal("area", value.Key);
Assert.Equal("specified-area", value.Value);
},
value =>
{
Assert.Equal("page", value.Key);
Assert.Equal("ambient-page", value.Value);
});
}
private static Mock<IUrlHelper> CreateMockUrlHelper(ActionContext context = null)
{
if (context == null)
{
context = GetActionContextForPage("/Page");
}
var urlHelper = new Mock<IUrlHelper>();
urlHelper.SetupGet(h => h.ActionContext)
.Returns(context);
return urlHelper;
}
private static ActionContext GetActionContextForPage(string page)
{
return new ActionContext
{
ActionDescriptor = new ActionDescriptor
{
RouteValues = new Dictionary<string, string>
{
{ "page", page },
},
},
RouteData = new RouteData
{
Values =
{
[ "page" ] = page
},
},
};
}
}
}

File diff suppressed because it is too large Load Diff