diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Routing/DispatcherUrlHelper.cs b/src/Microsoft.AspNetCore.Mvc.Core/Routing/DispatcherUrlHelper.cs
new file mode 100644
index 0000000000..bb6e1a1bdc
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Routing/DispatcherUrlHelper.cs
@@ -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
+{
+ ///
+ /// An implementation of that uses to build URLs
+ /// for ASP.NET MVC within an application.
+ ///
+ internal class DispatcherUrlHelper : UrlHelperBase
+ {
+ private readonly ILogger _logger;
+ private readonly ILinkGenerator _linkGenerator;
+
+ ///
+ /// Initializes a new instance of the class using the specified
+ /// .
+ ///
+ /// The for the current request.
+ /// The used to generate the link.
+ /// The .
+ public DispatcherUrlHelper(
+ ActionContext actionContext,
+ ILinkGenerator linkGenerator,
+ ILogger logger)
+ : base(actionContext)
+ {
+ if (linkGenerator == null)
+ {
+ throw new ArgumentNullException(nameof(linkGenerator));
+ }
+
+ if (logger == null)
+ {
+ throw new ArgumentNullException(nameof(logger));
+ }
+
+ _linkGenerator = linkGenerator;
+ _logger = logger;
+ }
+
+ ///
+ 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);
+ }
+
+ ///
+ 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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelper.cs b/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelper.cs
index 31a8b4a9fd..8bab2581d4 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelper.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelper.cs
@@ -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 that contains methods to
/// build URLs for ASP.NET MVC within an application.
///
- 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;
-
///
/// Initializes a new instance of the class using the specified
/// .
///
/// The for the current request.
public UrlHelper(ActionContext actionContext)
+ : base(actionContext)
{
- if (actionContext == null)
- {
- throw new ArgumentNullException(nameof(actionContext));
- }
-
- ActionContext = actionContext;
- _routeValueDictionary = new RouteValueDictionary();
}
- ///
- public ActionContext ActionContext { get; }
-
- ///
- /// Gets the associated with the current request.
- ///
- protected RouteValueDictionary AmbientValues => ActionContext.RouteData.Values;
-
///
/// Gets the associated with the current request.
///
@@ -58,7 +35,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
protected IRouter Router => ActionContext.RouteData.Routers[0];
///
- 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
}
///
- 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;
- }
-
- ///
- public virtual string RouteUrl(UrlRouteContext routeContext)
+ public override string RouteUrl(UrlRouteContext routeContext)
{
if (routeContext == null)
{
@@ -167,7 +95,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
///
///
/// The . The uses these values, in combination with
- /// , to generate the URL.
+ /// , to generate the URL.
///
/// The .
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);
- }
- }
-
- ///
- 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;
- }
-
- ///
- 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, 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;
- 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;
- }
-
///
/// Generates the URL using the specified components.
///
@@ -308,85 +114,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
/// The generated URL.
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);
}
}
}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelperBase.cs b/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelperBase.cs
new file mode 100644
index 0000000000..730323f33b
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelperBase.cs
@@ -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();
+ }
+
+ ///
+ /// Gets the associated with the current request.
+ ///
+ protected RouteValueDictionary AmbientValues { get; }
+
+ ///
+ public ActionContext ActionContext { get; }
+
+ ///
+ 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;
+ }
+
+ ///
+ 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;
+ }
+
+ ///
+ 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()
+ });
+ }
+
+ ///
+ public abstract string Action(UrlActionContext actionContext);
+
+ ///
+ public abstract string RouteUrl(UrlRouteContext routeContext);
+
+ protected RouteValueDictionary GetValuesDictionary(object values)
+ {
+ // Perf: RouteValueDictionary can be cast to IDictionary, 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 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;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelperFactory.cs b/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelperFactory.cs
index f9b8006850..ba0f7e9607 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelperFactory.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelperFactory.cs
@@ -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();
+ if (endpointFeature?.Endpoint != null)
+ {
+ var linkGenerator = httpContext.RequestServices.GetRequiredService();
+ var logger = httpContext.RequestServices.GetRequiredService>();
+ urlHelper = new DispatcherUrlHelper(context, linkGenerator, logger);
+ }
+ else
+ {
+ urlHelper = new UrlHelper(context);
+ }
+
httpContext.Items[typeof(IUrlHelper)] = urlHelper;
return urlHelper;
}
}
-}
+}
\ No newline at end of file
diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/LocalRedirectResultTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/LocalRedirectResultTest.cs
index ebbbac82a7..6e78d6f840 100644
--- a/test/Microsoft.AspNetCore.Mvc.Core.Test/LocalRedirectResultTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/LocalRedirectResultTest.cs
@@ -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;
}
diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/RedirectResultTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/RedirectResultTest.cs
index dc37c52ae7..61d583b998 100644
--- a/test/Microsoft.AspNetCore.Mvc.Core.Test/RedirectResultTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/RedirectResultTest.cs
@@ -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;
}
diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/DispatcherUrlHelperTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/DispatcherUrlHelperTest.cs
new file mode 100644
index 0000000000..6c42555191
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/DispatcherUrlHelperTest.cs
@@ -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(), 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(new EndpointFeature()
+ {
+ Endpoint = new MatcherEndpoint(
+ next => cntxt => Task.CompletedTask,
+ "/",
+ new { },
+ 0,
+ EndpointMetadataCollection.Empty,
+ null,
+ null)
+ });
+
+ var urlHelperFactory = httpContext.RequestServices.GetRequiredService();
+ var urlHelper = urlHelperFactory.GetUrlHelper(actionContext);
+ Assert.IsType(urlHelper);
+ return urlHelper;
+ }
+
+ protected override IServiceProvider CreateServices()
+ {
+ return CreateServices(Enumerable.Empty());
+ }
+
+ 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 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 GetDefaultEndpoints()
+ {
+ var endpoints = new List();
+ 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 endpoints)
+ {
+ if (endpoints == null)
+ {
+ endpoints = Enumerable.Empty();
+ }
+
+ var services = GetCommonServices();
+ services.AddDispatcher();
+ services.TryAddEnumerable(
+ ServiceDescriptor.Singleton(new DefaultEndpointDataSource(endpoints)));
+ services.TryAddSingleton();
+ 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));
+ }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/UrlHelperBaseTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/UrlHelperBaseTest.cs
new file mode 100644
index 0000000000..9bb3f54043
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/UrlHelperBaseTest.cs
@@ -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
+ {
+ { 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()
+ .AddSingleton(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);
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/UrlHelperExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/UrlHelperExtensionsTest.cs
new file mode 100644
index 0000000000..c6e00c231e
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/UrlHelperExtensionsTest.cs
@@ -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()))
+ .Callback((UrlRouteContext context) => actual = context);
+
+ // Act
+ urlHelper.Object.Page("/TestPage");
+
+ // Assert
+ urlHelper.Verify();
+ Assert.NotNull(actual);
+ Assert.Null(actual.RouteName);
+ Assert.Collection(Assert.IsType(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