diff --git a/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternBuilder.cs b/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternBuilder.cs index 1875ce4667..b6d8fb4842 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternBuilder.cs +++ b/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternBuilder.cs @@ -58,8 +58,7 @@ namespace Microsoft.AspNetCore.Dispatcher.Patterns var segment = PathSegments[i]; for (var j = 0; j < segment.Parts.Count; j++) { - var parameter = segment.Parts[j] as RoutePatternParameter; - if (parameter != null) + if (segment.Parts[j] is RoutePatternParameter parameter) { parameters.Add(parameter); } diff --git a/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternParser.cs b/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternParser.cs index 3a83b7d8e7..4e6e792a10 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternParser.cs +++ b/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternParser.cs @@ -33,13 +33,9 @@ namespace Microsoft.AspNetCore.Dispatcher.Patterns throw new ArgumentNullException(nameof(pattern)); } - if (IsInvalidPattern(pattern)) - { - throw new RoutePatternException(pattern, Resources.TemplateRoute_InvalidRouteTemplate); - } + var trimmedPattern = TrimPrefix(pattern); - var context = new TemplateParserContext(pattern); - var builder = RoutePatternBuilder.Create(pattern); + var context = new TemplateParserContext(trimmedPattern); var segments = new List(); while (context.MoveNext()) @@ -69,6 +65,7 @@ namespace Microsoft.AspNetCore.Dispatcher.Patterns if (IsAllValid(context, segments)) { + var builder = RoutePatternBuilder.Create(pattern); for (var i = 0; i < segments.Count; i++) { builder.PathSegments.Add(segments[i]); @@ -257,7 +254,7 @@ namespace Microsoft.AspNetCore.Dispatcher.Patterns private static bool ParseLiteral(TemplateParserContext context, List parts) { context.Mark(); - + while (true) { if (context.Current == Separator) @@ -469,10 +466,21 @@ namespace Microsoft.AspNetCore.Dispatcher.Patterns return true; } - private static bool IsInvalidPattern(string routeTemplate) + private static string TrimPrefix(string routePattern) { - return routeTemplate.StartsWith("~", StringComparison.Ordinal) || - routeTemplate.StartsWith("/", StringComparison.Ordinal); + if (routePattern.StartsWith("~/", StringComparison.Ordinal)) + { + return routePattern.Substring(2); + } + else if (routePattern.StartsWith("/", StringComparison.Ordinal)) + { + return routePattern.Substring(1); + } + else if (routePattern.StartsWith("~", StringComparison.Ordinal)) + { + throw new RoutePatternException(routePattern, Resources.TemplateRoute_InvalidRouteTemplate); + } + return routePattern; } [DebuggerDisplay("{DebuggerToString()}")] @@ -555,8 +563,8 @@ namespace Microsoft.AspNetCore.Dispatcher.Patterns } else if (_mark.HasValue) { - return _template.Substring(0, _mark.Value) + - "|" + + return _template.Substring(0, _mark.Value) + + "|" + _template.Substring(_mark.Value, _index - _mark.Value) + "|" + _template.Substring(_index); diff --git a/src/Microsoft.AspNetCore.Dispatcher/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Dispatcher/Properties/Resources.Designer.cs index ff22f3d26c..235ce5baf6 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Dispatcher/Properties/Resources.Designer.cs @@ -221,7 +221,7 @@ namespace Microsoft.AspNetCore.Dispatcher => string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_InvalidParameterName"), p0); /// - /// The route template cannot start with a '/' or '~' character. + /// The route template cannot start with a '~' character. /// internal static string TemplateRoute_InvalidRouteTemplate { @@ -229,7 +229,7 @@ namespace Microsoft.AspNetCore.Dispatcher } /// - /// The route template cannot start with a '/' or '~' character. + /// The route template cannot start with a '~' character. /// internal static string FormatTemplateRoute_InvalidRouteTemplate() => GetString("TemplateRoute_InvalidRouteTemplate"); diff --git a/src/Microsoft.AspNetCore.Dispatcher/Resources.resx b/src/Microsoft.AspNetCore.Dispatcher/Resources.resx index cb7814278c..eec436af5f 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/Resources.resx +++ b/src/Microsoft.AspNetCore.Dispatcher/Resources.resx @@ -164,7 +164,7 @@ The route parameter name '{0}' is invalid. Route parameter names must be non-empty and cannot contain these characters: '{{', '}}', '/'. The '?' character marks a parameter as optional, and can occur only at the end of the parameter. The '*' character marks a parameter as catch-all, and can occur only at the start of the parameter. - The route template cannot start with a '/' or '~' character. + The route template cannot start with a '~' character unless followed by a '/'. There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character. diff --git a/src/Microsoft.AspNetCore.Dispatcher/RoutePatternBinder.cs b/src/Microsoft.AspNetCore.Dispatcher/RoutePatternBinder.cs index b0e272d1f3..be6f51f86a 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/RoutePatternBinder.cs +++ b/src/Microsoft.AspNetCore.Dispatcher/RoutePatternBinder.cs @@ -74,8 +74,7 @@ namespace Microsoft.AspNetCore.Dispatcher // If it's a parameter subsegment, examine the current value to see if it matches the new value var parameterName = parameter.Name; - object newParameterValue; - var hasNewParameterValue = values.TryGetValue(parameterName, out newParameterValue); + var hasNewParameterValue = values.TryGetValue(parameterName, out var newParameterValue); object currentParameterValue = null; var hasCurrentParameterValue = ambientValues != null && @@ -179,8 +178,7 @@ namespace Microsoft.AspNetCore.Dispatcher continue; } - object value; - if (values.TryGetValue(filter.Key, out value)) + if (values.TryGetValue(filter.Key, out var value)) { if (!RoutePartsEqual(value, filter.Value)) { diff --git a/src/Microsoft.AspNetCore.Routing/Template/TemplateParser.cs b/src/Microsoft.AspNetCore.Routing/Template/TemplateParser.cs index 6a8d69d019..d5c2da0120 100644 --- a/src/Microsoft.AspNetCore.Routing/Template/TemplateParser.cs +++ b/src/Microsoft.AspNetCore.Routing/Template/TemplateParser.cs @@ -17,7 +17,7 @@ namespace Microsoft.AspNetCore.Routing.Template try { - var inner = Microsoft.AspNetCore.Dispatcher.Patterns.RoutePattern.Parse(routeTemplate); + var inner = RoutePattern.Parse(routeTemplate); return new RouteTemplate(inner); } catch (ArgumentException ex) when (ex.InnerException is RoutePatternException) diff --git a/test/Microsoft.AspNetCore.Dispatcher.Test/Patterns/RoutePatternParserTest.cs b/test/Microsoft.AspNetCore.Dispatcher.Test/Patterns/RoutePatternParserTest.cs index febd928adb..42e5118a83 100644 --- a/test/Microsoft.AspNetCore.Dispatcher.Test/Patterns/RoutePatternParserTest.cs +++ b/test/Microsoft.AspNetCore.Dispatcher.Test/Patterns/RoutePatternParserTest.cs @@ -622,12 +622,16 @@ namespace Microsoft.AspNetCore.Dispatcher.Patterns "a literal string."); } - [Fact] - public void InvalidTemplate_CannotStartWithSlash() + [Theory] + [InlineData("/foo")] + [InlineData("~/foo")] + public void ValidTemplate_CanStartWithSlashOrTildeSlash(string routePattern) { - ExceptionAssert.Throws( - () => RoutePatternParser.Parse("/foo"), - "The route template cannot start with a '/' or '~' character."); + // Arrange & Act + var pattern = RoutePatternParser.Parse(routePattern); + + // Assert + Assert.Equal(routePattern, pattern.RawText); } [Fact] @@ -635,7 +639,7 @@ namespace Microsoft.AspNetCore.Dispatcher.Patterns { ExceptionAssert.Throws( () => RoutePatternParser.Parse("~foo"), - "The route template cannot start with a '/' or '~' character."); + "The route template cannot start with a '~' character unless followed by a '/'."); } [Fact] diff --git a/test/Microsoft.AspNetCore.Dispatcher.Test/RoutePatternBinderTest.cs b/test/Microsoft.AspNetCore.Dispatcher.Test/RoutePatternBinderTest.cs index ef49cccd2e..5716f03c32 100644 --- a/test/Microsoft.AspNetCore.Dispatcher.Test/RoutePatternBinderTest.cs +++ b/test/Microsoft.AspNetCore.Dispatcher.Test/RoutePatternBinderTest.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text.Encodings.Web; -using Microsoft.AspNetCore.Dispatcher.Patterns; using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.WebEncoders.Testing; using Xunit; @@ -651,6 +650,28 @@ namespace Microsoft.AspNetCore.Dispatcher UrlEncoder.Default); } + [Fact] + public void GetUrlWithLeadingTildeSlash() + { + RunTest( + "~/foo", + null, + null, + new DispatcherValueCollection(new { }), + "/UrlEncode[[foo]]"); + } + + [Fact] + public void GetUrlWithLeadingSlash() + { + RunTest( + "/foo", + null, + null, + new DispatcherValueCollection(new { }), + "/UrlEncode[[foo]]"); + } + [Fact] public void GetUrlWithCatchAllWithValue() { @@ -1192,8 +1213,7 @@ namespace Microsoft.AspNetCore.Dispatcher foreach (var kvp in expectedParts.Parameters) { - string value; - Assert.True(actualParts.Parameters.TryGetValue(kvp.Key, out value)); + Assert.True(actualParts.Parameters.TryGetValue(kvp.Key, out var value)); Assert.Equal(kvp.Value, value); } } diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateParserTests.cs b/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateParserTests.cs index ce54e9485e..eabc3ec25e 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateParserTests.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateParserTests.cs @@ -765,13 +765,16 @@ namespace Microsoft.AspNetCore.Routing.Template.Tests "Parameter name: routeTemplate"); } - [Fact] - public void InvalidTemplate_CannotStartWithSlash() + [Theory] + [InlineData("/foo")] + [InlineData("~/foo")] + public void ValidTemplate_CanStartWithSlashOrTildeSlash(string routeTemplate) { - ExceptionAssert.Throws( - () => TemplateParser.Parse("/foo"), - "The route template cannot start with a '/' or '~' character." + Environment.NewLine + - "Parameter name: routeTemplate"); + // Arrange & Act + var pattern = TemplateParser.Parse(routeTemplate); + + // Assert + Assert.Equal(routeTemplate, pattern.TemplateText); } [Fact] @@ -779,7 +782,7 @@ namespace Microsoft.AspNetCore.Routing.Template.Tests { ExceptionAssert.Throws( () => TemplateParser.Parse("~foo"), - "The route template cannot start with a '/' or '~' character." + Environment.NewLine + + "The route template cannot start with a '~' character unless followed by a '/'." + Environment.NewLine + "Parameter name: routeTemplate"); }