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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Text;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
|
||||||
|
|
@ -14,38 +11,18 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
/// An implementation of <see cref="IUrlHelper"/> that contains methods to
|
/// An implementation of <see cref="IUrlHelper"/> that contains methods to
|
||||||
/// build URLs for ASP.NET MVC within an application.
|
/// build URLs for ASP.NET MVC within an application.
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="UrlHelper"/> class using the specified
|
/// Initializes a new instance of the <see cref="UrlHelper"/> class using the specified
|
||||||
/// <paramref name="actionContext"/>.
|
/// <paramref name="actionContext"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="actionContext">The <see cref="Mvc.ActionContext"/> for the current request.</param>
|
/// <param name="actionContext">The <see cref="Mvc.ActionContext"/> for the current request.</param>
|
||||||
public UrlHelper(ActionContext actionContext)
|
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>
|
/// <summary>
|
||||||
/// Gets the <see cref="Http.HttpContext"/> associated with the current request.
|
/// Gets the <see cref="Http.HttpContext"/> associated with the current request.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -58,7 +35,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
protected IRouter Router => ActionContext.RouteData.Routers[0];
|
protected IRouter Router => ActionContext.RouteData.Routers[0];
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual string Action(UrlActionContext actionContext)
|
public override string Action(UrlActionContext actionContext)
|
||||||
{
|
{
|
||||||
if (actionContext == null)
|
if (actionContext == null)
|
||||||
{
|
{
|
||||||
|
|
@ -69,9 +46,8 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
|
|
||||||
if (actionContext.Action == null)
|
if (actionContext.Action == null)
|
||||||
{
|
{
|
||||||
object action;
|
|
||||||
if (!valuesDictionary.ContainsKey("action") &&
|
if (!valuesDictionary.ContainsKey("action") &&
|
||||||
AmbientValues.TryGetValue("action", out action))
|
AmbientValues.TryGetValue("action", out var action))
|
||||||
{
|
{
|
||||||
valuesDictionary["action"] = action;
|
valuesDictionary["action"] = action;
|
||||||
}
|
}
|
||||||
|
|
@ -83,9 +59,8 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
|
|
||||||
if (actionContext.Controller == null)
|
if (actionContext.Controller == null)
|
||||||
{
|
{
|
||||||
object controller;
|
|
||||||
if (!valuesDictionary.ContainsKey("controller") &&
|
if (!valuesDictionary.ContainsKey("controller") &&
|
||||||
AmbientValues.TryGetValue("controller", out controller))
|
AmbientValues.TryGetValue("controller", out var controller))
|
||||||
{
|
{
|
||||||
valuesDictionary["controller"] = controller;
|
valuesDictionary["controller"] = controller;
|
||||||
}
|
}
|
||||||
|
|
@ -100,54 +75,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual bool IsLocalUrl(string url)
|
public override string RouteUrl(UrlRouteContext routeContext)
|
||||||
{
|
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
if (routeContext == null)
|
if (routeContext == null)
|
||||||
{
|
{
|
||||||
|
|
@ -167,7 +95,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
/// </param>
|
/// </param>
|
||||||
/// <param name="values">
|
/// <param name="values">
|
||||||
/// The <see cref="RouteValueDictionary"/>. The <see cref="Router"/> uses these values, in combination with
|
/// 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>
|
/// </param>
|
||||||
/// <returns>The <see cref="VirtualPathData"/>.</returns>
|
/// <returns>The <see cref="VirtualPathData"/>.</returns>
|
||||||
protected virtual VirtualPathData GetVirtualPathData(string routeName, RouteValueDictionary values)
|
protected virtual VirtualPathData GetVirtualPathData(string routeName, RouteValueDictionary values)
|
||||||
|
|
@ -176,128 +104,6 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
return Router.GetVirtualPath(context);
|
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>
|
/// <summary>
|
||||||
/// Generates the URL using the specified components.
|
/// Generates the URL using the specified components.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -308,85 +114,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
/// <returns>The generated URL.</returns>
|
/// <returns>The generated URL.</returns>
|
||||||
protected virtual string GenerateUrl(string protocol, string host, VirtualPathData pathData, string fragment)
|
protected virtual string GenerateUrl(string protocol, string host, VirtualPathData pathData, string fragment)
|
||||||
{
|
{
|
||||||
if (pathData == null)
|
return GenerateUrl(protocol, host, pathData?.VirtualPath, fragment);
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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 System;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc.Core;
|
using Microsoft.AspNetCore.Mvc.Core;
|
||||||
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.Routing
|
namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
{
|
{
|
||||||
|
|
@ -37,16 +40,27 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perf: Create only one UrlHelper per context
|
// Perf: Create only one UrlHelper per context
|
||||||
object value;
|
if (httpContext.Items.TryGetValue(typeof(IUrlHelper), out var value) && value is IUrlHelper)
|
||||||
if (httpContext.Items.TryGetValue(typeof(IUrlHelper), out value) && value is IUrlHelper)
|
|
||||||
{
|
{
|
||||||
return (IUrlHelper)value;
|
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;
|
httpContext.Items[typeof(IUrlHelper)] = urlHelper;
|
||||||
|
|
||||||
return urlHelper;
|
return urlHelper;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
using Microsoft.AspNetCore.Http.Internal;
|
using Microsoft.AspNetCore.Http.Internal;
|
||||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||||
|
|
@ -168,13 +169,15 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
var serviceProvider = GetServiceProvider();
|
var serviceProvider = GetServiceProvider();
|
||||||
|
|
||||||
httpContext.Setup(o => o.Response)
|
httpContext.Setup(o => o.Response)
|
||||||
.Returns(response);
|
.Returns(response);
|
||||||
httpContext.SetupGet(o => o.RequestServices)
|
httpContext.SetupGet(o => o.RequestServices)
|
||||||
.Returns(serviceProvider);
|
.Returns(serviceProvider);
|
||||||
httpContext.SetupGet(o => o.Items)
|
httpContext.SetupGet(o => o.Items)
|
||||||
.Returns(new ItemsDictionary());
|
.Returns(new ItemsDictionary());
|
||||||
httpContext.Setup(o => o.Request.PathBase)
|
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;
|
return httpContext.Object;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
using Microsoft.AspNetCore.Http.Internal;
|
using Microsoft.AspNetCore.Http.Internal;
|
||||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||||
using Microsoft.AspNetCore.Mvc.Internal;
|
|
||||||
using Microsoft.AspNetCore.Mvc.Routing;
|
using Microsoft.AspNetCore.Mvc.Routing;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
@ -146,13 +146,15 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
var serviceProvider = GetServiceProvider();
|
var serviceProvider = GetServiceProvider();
|
||||||
|
|
||||||
httpContext.Setup(o => o.Response)
|
httpContext.Setup(o => o.Response)
|
||||||
.Returns(response);
|
.Returns(response);
|
||||||
httpContext.SetupGet(o => o.RequestServices)
|
httpContext.SetupGet(o => o.RequestServices)
|
||||||
.Returns(serviceProvider);
|
.Returns(serviceProvider);
|
||||||
httpContext.SetupGet(o => o.Items)
|
httpContext.SetupGet(o => o.Items)
|
||||||
.Returns(new ItemsDictionary());
|
.Returns(new ItemsDictionary());
|
||||||
httpContext.Setup(o => o.Request.PathBase)
|
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;
|
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