Integrate Dispatcher's link generator
Related to https://github.com/aspnet/Routing/issues/530
This commit is contained in:
parent
4634a97fae
commit
a0a9c2c585
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue