diff --git a/src/Microsoft.AspNet.Routing/RouteCollection.cs b/src/Microsoft.AspNet.Routing/RouteCollection.cs
index c6f3ab1dbb..2a85f426de 100644
--- a/src/Microsoft.AspNet.Routing/RouteCollection.cs
+++ b/src/Microsoft.AspNet.Routing/RouteCollection.cs
@@ -9,8 +9,9 @@ using Microsoft.AspNet.Http;
using Microsoft.AspNet.Routing.Logging;
using Microsoft.AspNet.Routing.Logging.Internal;
using Microsoft.Framework.DependencyInjection;
-using Microsoft.Framework.OptionsModel;
using Microsoft.Framework.Logging;
+using Microsoft.Framework.OptionsModel;
+
namespace Microsoft.AspNet.Routing
{
@@ -154,7 +155,7 @@ namespace Microsoft.AspNet.Routing
if (isValidated || useBestEffort)
{
context.IsBound = isValidated;
- return bestPath;
+ return NormalizeVirtualPath(bestPath);
}
else
{
@@ -177,7 +178,7 @@ namespace Microsoft.AspNet.Routing
if (context.IsBound)
{
// This route has validated route values, short circuit.
- return path;
+ return NormalizeVirtualPath(path);
}
else if (bestPath == null)
{
@@ -188,7 +189,7 @@ namespace Microsoft.AspNet.Routing
if (useBestEffort)
{
- return bestPath;
+ return NormalizeVirtualPath(bestPath);
}
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)
{
diff --git a/src/Microsoft.AspNet.Routing/RouteOptions.cs b/src/Microsoft.AspNet.Routing/RouteOptions.cs
index a17f762a93..b8785a4cf1 100644
--- a/src/Microsoft.AspNet.Routing/RouteOptions.cs
+++ b/src/Microsoft.AspNet.Routing/RouteOptions.cs
@@ -9,6 +9,11 @@ namespace Microsoft.AspNet.Routing
{
public class RouteOptions
{
+ ///
+ /// Gets or sets a value indicating whether all generated URLs are lower-case.
+ ///
+ public bool LowercaseUrls { get; set; }
+
private IDictionary _constraintTypeMap = GetDefaultConstraintMap();
public IDictionary ConstraintMap
diff --git a/test/Microsoft.AspNet.Routing.Tests/RouteCollectionTest.cs b/test/Microsoft.AspNet.Routing.Tests/RouteCollectionTest.cs
index 081885a8f5..fc64702a99 100644
--- a/test/Microsoft.AspNet.Routing.Tests/RouteCollectionTest.cs
+++ b/test/Microsoft.AspNet.Routing.Tests/RouteCollectionTest.cs
@@ -8,6 +8,7 @@ using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Routing.Logging;
+using Microsoft.AspNet.Routing.Template;
using Microsoft.Framework.Logging;
using Microsoft.Framework.OptionsModel;
using Moq;
@@ -17,6 +18,61 @@ namespace Microsoft.AspNet.Routing
{
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(MockBehavior.Strict);
+ target
+ .Setup(e => e.GetVirtualPath(It.IsAny()))
+ .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(MockBehavior.Strict);
+ target
+ .Setup(e => e.GetVirtualPath(It.IsAny()))
+ .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]
public async Task RouteAsync_LogsCorrectValuesWhenHandled()
{
@@ -124,8 +180,8 @@ namespace Microsoft.AspNet.Routing
public async Task RouteAsync_SecondMatches()
{
// Arrange
- var routes = new RouteCollection();
+ var routes = new RouteCollection();
var route1 = CreateRoute(accept: false);
routes.Add(route1.Object);
@@ -151,7 +207,6 @@ namespace Microsoft.AspNet.Routing
{
// Arrange
var routes = new RouteCollection();
-
var route1 = CreateRoute(accept: false);
routes.Add(route1.Object);
@@ -171,18 +226,22 @@ namespace Microsoft.AspNet.Routing
Assert.Empty(context.RouteData.Routers);
}
- [Fact]
- public void NamedRouteTests_GetNamedRoute_ReturnsValue()
+ [Theory]
+ [InlineData(false, "RouteName")]
+ [InlineData(true, "routename")]
+ public void NamedRouteTests_GetNamedRoute_ReturnsValue(bool lowercaseUrls, string expectedUrl)
{
// Arrange
var routeCollection = GetNestedRouteCollection(new string[] { "Route1", "Route2", "RouteName", "Route3" });
- var virtualPathContext = CreateVirtualPathContext("RouteName");
+ var virtualPathContext = CreateVirtualPathContext(
+ routeName: "RouteName",
+ options: GetRouteOptions(lowercaseUrls));
// Act
var stringVirtualPath = routeCollection.GetVirtualPath(virtualPathContext);
// Assert
- Assert.Equal("RouteName", stringVirtualPath);
+ Assert.Equal(expectedUrl, stringVirtualPath);
}
[Fact]
@@ -207,13 +266,13 @@ namespace Microsoft.AspNet.Routing
// Add Duplicate route.
routeCollection.Add(CreateNamedRoute("Route3"));
- var virtualPathContext = CreateVirtualPathContext("Route1");
+ var virtualPathContext = CreateVirtualPathContext(routeName: "Route1", options: GetRouteOptions(true));
// Act
var stringVirtualPath = routeCollection.GetVirtualPath(virtualPathContext);
// Assert
- Assert.Equal("Route1", stringVirtualPath);
+ Assert.Equal("route1", stringVirtualPath);
}
[Fact]
@@ -225,11 +284,13 @@ namespace Microsoft.AspNet.Routing
// Add Duplicate route.
routeCollection.Add(CreateNamedRoute(ambiguousRoute));
- var virtualPathContext = CreateVirtualPathContext(ambiguousRoute);
+ var virtualPathContext = CreateVirtualPathContext(routeName: ambiguousRoute, options: GetRouteOptions());
// Act & Assert
var ex = Assert.Throws(() => 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]
@@ -526,6 +587,75 @@ namespace Microsoft.AspNet.Routing
route3.Verify(r => r.GetVirtualPath(It.IsAny()), Times.Once());
}
+ // "Integration" tests for RouteCollection
+
+ public static IEnumerable