[Fixes #188] Added support for AppendTrailingSlash in RouteOptions

This commit is contained in:
Ajay Bhargav Baaskaran 2015-06-17 16:47:43 -07:00
parent 7172608722
commit b135a9d53b
3 changed files with 53 additions and 26 deletions

View File

@ -203,24 +203,31 @@ namespace Microsoft.AspNet.Routing
{ {
var url = path.Value; var url = path.Value;
if (!string.IsNullOrEmpty(url) && _options.LowercaseUrls) if (!string.IsNullOrEmpty(url) && (_options.LowercaseUrls || _options.AppendTrailingSlash))
{ {
var indexOfSeparator = url.IndexOfAny(new char[] { '?', '#' }); var indexOfSeparator = url.IndexOfAny(new char[] { '?', '#' });
var urlWithoutQueryString = url;
var queryString = string.Empty;
// No query string, lowercase the url if (indexOfSeparator != -1)
if (indexOfSeparator == -1)
{ {
url = url.ToLowerInvariant(); urlWithoutQueryString = url.Substring(0, indexOfSeparator);
queryString = url.Substring(indexOfSeparator);
} }
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. if (_options.LowercaseUrls)
url = lowercaseUrl + queryString; {
urlWithoutQueryString = urlWithoutQueryString.ToLowerInvariant();
} }
if (_options.AppendTrailingSlash && !urlWithoutQueryString.EndsWith("/"))
{
urlWithoutQueryString += "/";
}
// queryString will contain the delimiter ? or # as the first character, so it's safe to append.
url = urlWithoutQueryString + queryString;
return new PathString(url); return new PathString(url);
} }

View File

@ -15,6 +15,11 @@ namespace Microsoft.AspNet.Routing
/// </summary> /// </summary>
public bool LowercaseUrls { get; set; } public bool LowercaseUrls { get; set; }
/// <summary>
/// Gets or sets a value indicating whether a trailing slash should be appended to the generated URLs.
/// </summary>
public bool AppendTrailingSlash { get; set; }
private IDictionary<string, Type> _constraintTypeMap = GetDefaultConstraintMap(); private IDictionary<string, Type> _constraintTypeMap = GetDefaultConstraintMap();
public IDictionary<string, Type> ConstraintMap public IDictionary<string, Type> ConstraintMap

View File

@ -20,16 +20,22 @@ namespace Microsoft.AspNet.Routing
public class RouteCollectionTest public class RouteCollectionTest
{ {
[Theory] [Theory]
[InlineData(@"Home/Index/23", "/home/index/23", true)] [InlineData(@"Home/Index/23", "/home/index/23", true, false)]
[InlineData(@"Home/Index/23", "/Home/Index/23", false)] [InlineData(@"Home/Index/23", "/Home/Index/23", false, false)]
[InlineData(@"Home/Index/23?Param1=ABC&Param2=Xyz", "/Home/Index/23?Param1=ABC&Param2=Xyz", false)] [InlineData(@"Home/Index/23", "/home/index/23/", true, true)]
[InlineData(@"Home/Index/23?Param1=ABC&Param2=Xyz", "/home/index/23?Param1=ABC&Param2=Xyz", true)] [InlineData(@"Home/Index/23", "/Home/Index/23/", false, 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", false, true)]
[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, false)]
public void GetVirtualPath_CanLowerCaseUrls_BasedOnOptions( [InlineData(@"Home/Index/23?Param1=ABC&Param2=Xyz", "/home/index/23/?Param1=ABC&Param2=Xyz", true, true)]
[InlineData(@"Home/Index/23#Param1=ABC&Param2=Xyz", "/Home/Index/23/#Param1=ABC&Param2=Xyz", false, true)]
[InlineData(@"Home/Index/23#Param1=ABC&Param2=Xyz", "/home/index/23#Param1=ABC&Param2=Xyz", true, false)]
[InlineData(@"Home/Index/23/?Param1=ABC&Param2=Xyz", "/home/index/23/?Param1=ABC&Param2=Xyz", true, true)]
[InlineData(@"Home/Index/23/#Param1=ABC&Param2=Xyz", "/home/index/23/#Param1=ABC&Param2=Xyz", true, false)]
public void GetVirtualPath_CanLowerCaseUrls_And_AppendTrailingSlash_BasedOnOptions(
string returnUrl, string returnUrl,
string lowercaseUrl, string expectedUrl,
bool lowercaseUrls) bool lowercaseUrls,
bool appendTrailingSlash)
{ {
// Arrange // Arrange
var target = new Mock<IRouter>(MockBehavior.Strict); var target = new Mock<IRouter>(MockBehavior.Strict);
@ -39,17 +45,21 @@ namespace Microsoft.AspNet.Routing
var routeCollection = new RouteCollection(); var routeCollection = new RouteCollection();
routeCollection.Add(target.Object); routeCollection.Add(target.Object);
var virtualPathContext = CreateVirtualPathContext(options: GetRouteOptions(lowercaseUrls)); var virtualPathContext = CreateVirtualPathContext(
options: GetRouteOptions(
lowerCaseUrls: lowercaseUrls,
useBestEffortLinkGeneration: true,
appendTrailingSlash: appendTrailingSlash));
// Act // Act
var pathData = routeCollection.GetVirtualPath(virtualPathContext); var pathData = routeCollection.GetVirtualPath(virtualPathContext);
// Assert // Assert
Assert.Equal(new PathString(lowercaseUrl), pathData.VirtualPath); Assert.Equal(new PathString(expectedUrl), pathData.VirtualPath);
Assert.Same(target.Object, pathData.Router); Assert.Same(target.Object, pathData.Router);
Assert.Empty(pathData.DataTokens); Assert.Empty(pathData.DataTokens);
} }
[Theory] [Theory]
[InlineData(@"\u0130", @"/\u0130", true)] [InlineData(@"\u0130", @"/\u0130", true)]
[InlineData(@"\u0049", @"/\u0049", true)] [InlineData(@"\u0049", @"/\u0049", true)]
@ -114,7 +124,7 @@ namespace Microsoft.AspNet.Routing
Assert.Equal(dataToken.Value, pathData.DataTokens[dataToken.Key]); Assert.Equal(dataToken.Value, pathData.DataTokens[dataToken.Key]);
} }
} }
[Fact] [Fact]
public async Task RouteAsync_FirstMatches() public async Task RouteAsync_FirstMatches()
{ {
@ -225,7 +235,7 @@ namespace Microsoft.AspNet.Routing
// Assert // Assert
Assert.Null(stringVirtualPath); Assert.Null(stringVirtualPath);
} }
[Fact] [Fact]
public void NamedRouteTests_GetNamedRoute_AmbiguousRoutesInCollection_DoesNotThrowForUnambiguousRoute() public void NamedRouteTests_GetNamedRoute_AmbiguousRoutesInCollection_DoesNotThrowForUnambiguousRoute()
{ {
@ -245,7 +255,7 @@ namespace Microsoft.AspNet.Routing
Assert.Equal("Route1", namedRouter.Name); Assert.Equal("Route1", namedRouter.Name);
Assert.Empty(pathData.DataTokens); Assert.Empty(pathData.DataTokens);
} }
[Fact] [Fact]
public void NamedRouteTests_GetNamedRoute_AmbiguousRoutesInCollection_ThrowsForAmbiguousRoute() public void NamedRouteTests_GetNamedRoute_AmbiguousRoutesInCollection_ThrowsForAmbiguousRoute()
{ {
@ -880,11 +890,16 @@ namespace Microsoft.AspNet.Routing
return target; return target;
} }
private static RouteOptions GetRouteOptions(bool lowerCaseUrls = false, bool useBestEffortLinkGeneration = true) private static RouteOptions GetRouteOptions(
bool lowerCaseUrls = false,
bool useBestEffortLinkGeneration = true,
bool appendTrailingSlash = false)
{ {
var routeOptions = new RouteOptions(); var routeOptions = new RouteOptions();
routeOptions.LowercaseUrls = lowerCaseUrls; routeOptions.LowercaseUrls = lowerCaseUrls;
routeOptions.UseBestEffortLinkGeneration = useBestEffortLinkGeneration; routeOptions.UseBestEffortLinkGeneration = useBestEffortLinkGeneration;
routeOptions.AppendTrailingSlash = appendTrailingSlash;
return routeOptions; return routeOptions;
} }
} }