Issue 140: Support lower case routes
This commit is contained in:
parent
f41b6e4d10
commit
81a17300b2
|
|
@ -9,8 +9,9 @@ using Microsoft.AspNet.Http;
|
||||||
using Microsoft.AspNet.Routing.Logging;
|
using Microsoft.AspNet.Routing.Logging;
|
||||||
using Microsoft.AspNet.Routing.Logging.Internal;
|
using Microsoft.AspNet.Routing.Logging.Internal;
|
||||||
using Microsoft.Framework.DependencyInjection;
|
using Microsoft.Framework.DependencyInjection;
|
||||||
using Microsoft.Framework.OptionsModel;
|
|
||||||
using Microsoft.Framework.Logging;
|
using Microsoft.Framework.Logging;
|
||||||
|
using Microsoft.Framework.OptionsModel;
|
||||||
|
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Routing
|
namespace Microsoft.AspNet.Routing
|
||||||
{
|
{
|
||||||
|
|
@ -154,7 +155,7 @@ namespace Microsoft.AspNet.Routing
|
||||||
if (isValidated || useBestEffort)
|
if (isValidated || useBestEffort)
|
||||||
{
|
{
|
||||||
context.IsBound = isValidated;
|
context.IsBound = isValidated;
|
||||||
return bestPath;
|
return NormalizeVirtualPath(bestPath);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -177,7 +178,7 @@ namespace Microsoft.AspNet.Routing
|
||||||
if (context.IsBound)
|
if (context.IsBound)
|
||||||
{
|
{
|
||||||
// This route has validated route values, short circuit.
|
// This route has validated route values, short circuit.
|
||||||
return path;
|
return NormalizeVirtualPath(path);
|
||||||
}
|
}
|
||||||
else if (bestPath == null)
|
else if (bestPath == null)
|
||||||
{
|
{
|
||||||
|
|
@ -188,7 +189,7 @@ namespace Microsoft.AspNet.Routing
|
||||||
|
|
||||||
if (useBestEffort)
|
if (useBestEffort)
|
||||||
{
|
{
|
||||||
return bestPath;
|
return NormalizeVirtualPath(bestPath);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -197,7 +198,33 @@ namespace Microsoft.AspNet.Routing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void EnsureLogger(HttpContext context)
|
private string NormalizeVirtualPath(String url)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(url)) return url;
|
||||||
|
|
||||||
|
if (_options.LowercaseUrls)
|
||||||
|
{
|
||||||
|
var indexOfSeparator = url.IndexOfAny(new char[] { '?', '#' });
|
||||||
|
|
||||||
|
// No query string, lowercase the url
|
||||||
|
if (indexOfSeparator == -1)
|
||||||
|
{
|
||||||
|
return url.ToLowerInvariant();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var lowercaseUrl = url.Substring(0, indexOfSeparator).ToLowerInvariant();
|
||||||
|
var queryString = url.Substring(indexOfSeparator);
|
||||||
|
|
||||||
|
// queryString will contain the delimiter ? or # as the first character, so it's safe to append.
|
||||||
|
return lowercaseUrl + queryString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureLogger(HttpContext context)
|
||||||
{
|
{
|
||||||
if (_logger == null)
|
if (_logger == null)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,11 @@ namespace Microsoft.AspNet.Routing
|
||||||
{
|
{
|
||||||
public class RouteOptions
|
public class RouteOptions
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether all generated URLs are lower-case.
|
||||||
|
/// </summary>
|
||||||
|
public bool LowercaseUrls { get; set; }
|
||||||
|
|
||||||
private IDictionary<string, Type> _constraintTypeMap = GetDefaultConstraintMap();
|
private IDictionary<string, Type> _constraintTypeMap = GetDefaultConstraintMap();
|
||||||
|
|
||||||
public IDictionary<string, Type> ConstraintMap
|
public IDictionary<string, Type> ConstraintMap
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNet.Http;
|
using Microsoft.AspNet.Http;
|
||||||
using Microsoft.AspNet.Routing.Logging;
|
using Microsoft.AspNet.Routing.Logging;
|
||||||
|
using Microsoft.AspNet.Routing.Template;
|
||||||
using Microsoft.Framework.Logging;
|
using Microsoft.Framework.Logging;
|
||||||
using Microsoft.Framework.OptionsModel;
|
using Microsoft.Framework.OptionsModel;
|
||||||
using Moq;
|
using Moq;
|
||||||
|
|
@ -17,6 +18,61 @@ namespace Microsoft.AspNet.Routing
|
||||||
{
|
{
|
||||||
public class RouteCollectionTest
|
public class RouteCollectionTest
|
||||||
{
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData(@"Home/Index/23", "home/index/23", true)]
|
||||||
|
[InlineData(@"Home/Index/23", "Home/Index/23", false)]
|
||||||
|
[InlineData(@"Home/Index/23?Param1=ABC&Param2=Xyz", "Home/Index/23?Param1=ABC&Param2=Xyz", false)]
|
||||||
|
[InlineData(@"Home/Index/23?Param1=ABC&Param2=Xyz", "home/index/23?Param1=ABC&Param2=Xyz", true)]
|
||||||
|
[InlineData(@"Home/Index/23#Param1=ABC&Param2=Xyz", "Home/Index/23#Param1=ABC&Param2=Xyz", false)]
|
||||||
|
[InlineData(@"Home/Index/23#Param1=ABC&Param2=Xyz", "home/index/23#Param1=ABC&Param2=Xyz", true)]
|
||||||
|
public void GetVirtualPath_CanLowerCaseUrls_BasedOnOptions(
|
||||||
|
string returnUrl,
|
||||||
|
string lowercaseUrl,
|
||||||
|
bool lowercaseUrls)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var target = new Mock<IRouter>(MockBehavior.Strict);
|
||||||
|
target
|
||||||
|
.Setup(e => e.GetVirtualPath(It.IsAny<VirtualPathContext>()))
|
||||||
|
.Returns(returnUrl);
|
||||||
|
|
||||||
|
var routeCollection = new RouteCollection();
|
||||||
|
routeCollection.Add(target.Object);
|
||||||
|
var virtualPathContext = CreateVirtualPathContext(options: GetRouteOptions(lowercaseUrls));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var stringVirtualPath = routeCollection.GetVirtualPath(virtualPathContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(lowercaseUrl, stringVirtualPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(@"\u0130", @"\u0130", true)]
|
||||||
|
[InlineData(@"\u0049", @"\u0049", true)]
|
||||||
|
[InlineData(@"üino", @"üino", true)]
|
||||||
|
public void GetVirtualPath_DoesntLowerCaseUrls_Invariant(
|
||||||
|
string returnUrl,
|
||||||
|
string lowercaseUrl,
|
||||||
|
bool lowercaseUrls)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var target = new Mock<IRouter>(MockBehavior.Strict);
|
||||||
|
target
|
||||||
|
.Setup(e => e.GetVirtualPath(It.IsAny<VirtualPathContext>()))
|
||||||
|
.Returns(returnUrl);
|
||||||
|
|
||||||
|
var routeCollection = new RouteCollection();
|
||||||
|
routeCollection.Add(target.Object);
|
||||||
|
var virtualPathContext = CreateVirtualPathContext(options: GetRouteOptions(lowercaseUrls));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var stringVirtualPath = routeCollection.GetVirtualPath(virtualPathContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(lowercaseUrl, stringVirtualPath);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task RouteAsync_LogsCorrectValuesWhenHandled()
|
public async Task RouteAsync_LogsCorrectValuesWhenHandled()
|
||||||
{
|
{
|
||||||
|
|
@ -124,8 +180,8 @@ namespace Microsoft.AspNet.Routing
|
||||||
public async Task RouteAsync_SecondMatches()
|
public async Task RouteAsync_SecondMatches()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var routes = new RouteCollection();
|
|
||||||
|
|
||||||
|
var routes = new RouteCollection();
|
||||||
var route1 = CreateRoute(accept: false);
|
var route1 = CreateRoute(accept: false);
|
||||||
routes.Add(route1.Object);
|
routes.Add(route1.Object);
|
||||||
|
|
||||||
|
|
@ -151,7 +207,6 @@ namespace Microsoft.AspNet.Routing
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var routes = new RouteCollection();
|
var routes = new RouteCollection();
|
||||||
|
|
||||||
var route1 = CreateRoute(accept: false);
|
var route1 = CreateRoute(accept: false);
|
||||||
routes.Add(route1.Object);
|
routes.Add(route1.Object);
|
||||||
|
|
||||||
|
|
@ -171,18 +226,22 @@ namespace Microsoft.AspNet.Routing
|
||||||
Assert.Empty(context.RouteData.Routers);
|
Assert.Empty(context.RouteData.Routers);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Theory]
|
||||||
public void NamedRouteTests_GetNamedRoute_ReturnsValue()
|
[InlineData(false, "RouteName")]
|
||||||
|
[InlineData(true, "routename")]
|
||||||
|
public void NamedRouteTests_GetNamedRoute_ReturnsValue(bool lowercaseUrls, string expectedUrl)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var routeCollection = GetNestedRouteCollection(new string[] { "Route1", "Route2", "RouteName", "Route3" });
|
var routeCollection = GetNestedRouteCollection(new string[] { "Route1", "Route2", "RouteName", "Route3" });
|
||||||
var virtualPathContext = CreateVirtualPathContext("RouteName");
|
var virtualPathContext = CreateVirtualPathContext(
|
||||||
|
routeName: "RouteName",
|
||||||
|
options: GetRouteOptions(lowercaseUrls));
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var stringVirtualPath = routeCollection.GetVirtualPath(virtualPathContext);
|
var stringVirtualPath = routeCollection.GetVirtualPath(virtualPathContext);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal("RouteName", stringVirtualPath);
|
Assert.Equal(expectedUrl, stringVirtualPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -207,13 +266,13 @@ namespace Microsoft.AspNet.Routing
|
||||||
|
|
||||||
// Add Duplicate route.
|
// Add Duplicate route.
|
||||||
routeCollection.Add(CreateNamedRoute("Route3"));
|
routeCollection.Add(CreateNamedRoute("Route3"));
|
||||||
var virtualPathContext = CreateVirtualPathContext("Route1");
|
var virtualPathContext = CreateVirtualPathContext(routeName: "Route1", options: GetRouteOptions(true));
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var stringVirtualPath = routeCollection.GetVirtualPath(virtualPathContext);
|
var stringVirtualPath = routeCollection.GetVirtualPath(virtualPathContext);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal("Route1", stringVirtualPath);
|
Assert.Equal("route1", stringVirtualPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -225,11 +284,13 @@ namespace Microsoft.AspNet.Routing
|
||||||
|
|
||||||
// Add Duplicate route.
|
// Add Duplicate route.
|
||||||
routeCollection.Add(CreateNamedRoute(ambiguousRoute));
|
routeCollection.Add(CreateNamedRoute(ambiguousRoute));
|
||||||
var virtualPathContext = CreateVirtualPathContext(ambiguousRoute);
|
var virtualPathContext = CreateVirtualPathContext(routeName: ambiguousRoute, options: GetRouteOptions());
|
||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
var ex = Assert.Throws<InvalidOperationException>(() => routeCollection.GetVirtualPath(virtualPathContext));
|
var ex = Assert.Throws<InvalidOperationException>(() => routeCollection.GetVirtualPath(virtualPathContext));
|
||||||
Assert.Equal("The supplied route name 'ambiguousRoute' is ambiguous and matched more than one route.", ex.Message);
|
Assert.Equal(
|
||||||
|
"The supplied route name 'ambiguousRoute' is ambiguous and matched more than one route.",
|
||||||
|
ex.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -526,6 +587,75 @@ namespace Microsoft.AspNet.Routing
|
||||||
route3.Verify(r => r.GetVirtualPath(It.IsAny<VirtualPathContext>()), Times.Once());
|
route3.Verify(r => r.GetVirtualPath(It.IsAny<VirtualPathContext>()), Times.Once());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// "Integration" tests for RouteCollection
|
||||||
|
|
||||||
|
public static IEnumerable<object[]> IntegrationTestData
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
yield return new object[] {
|
||||||
|
"{controller}/{action}",
|
||||||
|
new RouteValueDictionary { { "controller", "Home" }, { "action", "Index" } },
|
||||||
|
"home/index",
|
||||||
|
true };
|
||||||
|
|
||||||
|
yield return new object[] {
|
||||||
|
"{controller}/{action}/",
|
||||||
|
new RouteValueDictionary { { "controller", "Home" }, { "action", "Index" } },
|
||||||
|
"Home/Index",
|
||||||
|
false };
|
||||||
|
|
||||||
|
yield return new object[] {
|
||||||
|
"api/{action}/",
|
||||||
|
new RouteValueDictionary { { "action", "Create" } },
|
||||||
|
"api/create",
|
||||||
|
true };
|
||||||
|
|
||||||
|
yield return new object[] {
|
||||||
|
"api/{action}/{id}",
|
||||||
|
new RouteValueDictionary {
|
||||||
|
{ "action", "Create" },
|
||||||
|
{ "id", "23" },
|
||||||
|
{ "Param1", "Value1" },
|
||||||
|
{ "Param2", "Value2" } },
|
||||||
|
"api/create/23?Param1=Value1&Param2=Value2",
|
||||||
|
true };
|
||||||
|
|
||||||
|
yield return new object[] {
|
||||||
|
"api/{action}/{id}",
|
||||||
|
new RouteValueDictionary {
|
||||||
|
{ "action", "Create" },
|
||||||
|
{ "id", "23" },
|
||||||
|
{ "Param1", "Value1" },
|
||||||
|
{ "Param2", "Value2" } },
|
||||||
|
"api/Create/23?Param1=Value1&Param2=Value2",
|
||||||
|
false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData("IntegrationTestData")]
|
||||||
|
public void GetVirtualPath_Success(
|
||||||
|
string template,
|
||||||
|
RouteValueDictionary values,
|
||||||
|
string expectedUrl,
|
||||||
|
bool lowercaseUrls
|
||||||
|
)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var routeCollection = new RouteCollection();
|
||||||
|
var route = CreateTemplateRoute(template);
|
||||||
|
routeCollection.Add(route);
|
||||||
|
var context = CreateVirtualPathContext(values, options: GetRouteOptions(lowercaseUrls));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var path = routeCollection.GetVirtualPath(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(context.IsBound);
|
||||||
|
Assert.Equal(expectedUrl, path);
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task<TestSink> SetUp(bool enabled, bool handled)
|
private static async Task<TestSink> SetUp(bool enabled, bool handled)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|
@ -615,6 +745,18 @@ namespace Microsoft.AspNet.Routing
|
||||||
return target.Object;
|
return target.Object;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static TemplateRoute CreateTemplateRoute(string template)
|
||||||
|
{
|
||||||
|
var target = new Mock<IRouter>(MockBehavior.Strict);
|
||||||
|
target
|
||||||
|
.Setup(e => e.GetVirtualPath(It.IsAny<VirtualPathContext>()))
|
||||||
|
.Callback<VirtualPathContext>(c => c.IsBound = true)
|
||||||
|
.Returns<VirtualPathContext>(rc => null);
|
||||||
|
|
||||||
|
var resolverMock = new Mock<IInlineConstraintResolver>();
|
||||||
|
return new TemplateRoute(target.Object, template, resolverMock.Object);
|
||||||
|
}
|
||||||
|
|
||||||
private static VirtualPathContext CreateVirtualPathContext(
|
private static VirtualPathContext CreateVirtualPathContext(
|
||||||
string routeName = null,
|
string routeName = null,
|
||||||
ILoggerFactory loggerFactory = null,
|
ILoggerFactory loggerFactory = null,
|
||||||
|
|
@ -646,6 +788,23 @@ namespace Microsoft.AspNet.Routing
|
||||||
return new VirtualPathContext(context.Object, null, null, routeName);
|
return new VirtualPathContext(context.Object, null, null, routeName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static VirtualPathContext CreateVirtualPathContext(
|
||||||
|
RouteValueDictionary values,
|
||||||
|
RouteOptions options = null)
|
||||||
|
{
|
||||||
|
var optionsAccessor = new Mock<IOptions<RouteOptions>>(MockBehavior.Strict);
|
||||||
|
optionsAccessor.SetupGet(o => o.Options).Returns(options);
|
||||||
|
|
||||||
|
var context = new Mock<HttpContext>(MockBehavior.Strict);
|
||||||
|
context.Setup(m => m.RequestServices.GetService(typeof(IOptions<RouteOptions>)))
|
||||||
|
.Returns(optionsAccessor.Object);
|
||||||
|
context.Setup(m => m.RequestServices.GetService(typeof(ILoggerFactory)))
|
||||||
|
.Returns(NullLoggerFactory.Instance);
|
||||||
|
|
||||||
|
|
||||||
|
return new VirtualPathContext(context.Object, null, values);
|
||||||
|
}
|
||||||
|
|
||||||
private static RouteContext CreateRouteContext(
|
private static RouteContext CreateRouteContext(
|
||||||
string requestPath,
|
string requestPath,
|
||||||
ILoggerFactory loggerFactory = null,
|
ILoggerFactory loggerFactory = null,
|
||||||
|
|
@ -697,6 +856,14 @@ namespace Microsoft.AspNet.Routing
|
||||||
|
|
||||||
return target;
|
return target;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static RouteOptions GetRouteOptions(bool lowerCaseUrls = false, bool useBestEffortLinkGeneration = true)
|
||||||
|
{
|
||||||
|
var routeOptions = new RouteOptions();
|
||||||
|
routeOptions.LowercaseUrls = lowerCaseUrls;
|
||||||
|
routeOptions.UseBestEffortLinkGeneration = useBestEffortLinkGeneration;
|
||||||
|
return routeOptions;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue