diff --git a/src/Microsoft.AspNetCore.Routing/Tree/TreeRouteBuilder.cs b/src/Microsoft.AspNetCore.Routing/Tree/TreeRouteBuilder.cs index 7ab234b6cd..978037c26f 100644 --- a/src/Microsoft.AspNetCore.Routing/Tree/TreeRouteBuilder.cs +++ b/src/Microsoft.AspNetCore.Routing/Tree/TreeRouteBuilder.cs @@ -333,7 +333,7 @@ namespace Microsoft.AspNetCore.Routing.Tree continue; } - if (part.IsParameter && (part.IsOptional || part.IsCatchAll)) + if (part.IsParameter && (part.IsOptional || part.IsCatchAll || part.DefaultValue != null)) { current.Matches.Add(new InboundMatch() { Entry = entry, TemplateMatcher = matcher }); } diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Tree/TreeRouterTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Tree/TreeRouterTest.cs index 5cd8562391..6b7dcd4549 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Tree/TreeRouterTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Tree/TreeRouterTest.cs @@ -16,6 +16,7 @@ using Microsoft.Extensions.Options; using Microsoft.Extensions.WebEncoders.Testing; using Moq; using Xunit; +using Xunit.Extensions; namespace Microsoft.AspNetCore.Routing.Tree { @@ -125,6 +126,153 @@ namespace Microsoft.AspNetCore.Routing.Tree Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]); } + public static TheoryData MatchesRoutesWithDefaultsData => + new TheoryData + { + { "/", new object[] { "1", "2", "3", "4" } }, + { "/a", new object[] { "a", "2", "3", "4" } }, + { "/a/b", new object[] { "a", "b", "3", "4" } }, + { "/a/b/c", new object[] { "a", "b", "c", "4" } }, + { "/a/b/c/d", new object[] { "a", "b", "c", "d" } } + }; + + [Theory] + [MemberData(nameof(MatchesRoutesWithDefaultsData))] + public async Task TreeRouter_RouteAsync_MatchesRoutesWithDefaults(string url, object[] routeValues) + { + // Arrange + var routes = new[] { + "{parameter1=1}/{parameter2=2}/{parameter3=3}/{parameter4=4}", + }; + + var expectedRouteGroup = CreateRouteGroup(0, "{parameter1=1}/{parameter2=2}/{parameter3=3}/{parameter4=4}"); + var routeValueKeys = new[] { "parameter1", "parameter2", "parameter3", "parameter4" }; + var expectedRouteValues = new RouteValueDictionary(); + for (int i = 0; i < routeValueKeys.Length; i++) + { + expectedRouteValues.Add(routeValueKeys[i], routeValues[i]); + } + + var builder = CreateBuilder(); + + // We setup the route entries in reverse order of precedence to ensure that when we + // try to route the request, the route with a higher precedence gets tried first. + foreach (var template in routes.Reverse()) + { + MapInboundEntry(builder, template); + } + + var route = builder.Build(); + + var context = CreateRouteContext(url); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]); + foreach (var entry in expectedRouteValues) + { + var data = Assert.Single(context.RouteData.Values, v => v.Key == entry.Key); + Assert.Equal(entry.Value, data.Value); + } + } + + public static TheoryData MatchesConstrainedRoutesWithDefaultsData => + new TheoryData + { + { "/", new object[] { "1", "2", "3", "4" } }, + { "/10", new object[] { "10", "2", "3", "4" } }, + { "/10/11", new object[] { "10", "11", "3", "4" } }, + { "/10/11/12", new object[] { "10", "11", "12", "4" } }, + { "/10/11/12/13", new object[] { "10", "11", "12", "13" } } + }; + + [Theory] + [MemberData(nameof(MatchesConstrainedRoutesWithDefaultsData))] + public async Task TreeRouter_RouteAsync_MatchesConstrainedRoutesWithDefaults(string url, object[] routeValues) + { + // Arrange + var routes = new[] { + "{parameter1:int=1}/{parameter2:int=2}/{parameter3:int=3}/{parameter4:int=4}", + }; + + var expectedRouteGroup = CreateRouteGroup(0, "{parameter1:int=1}/{parameter2:int=2}/{parameter3:int=3}/{parameter4:int=4}"); + var routeValueKeys = new[] { "parameter1", "parameter2", "parameter3", "parameter4" }; + var expectedRouteValues = new RouteValueDictionary(); + for (int i = 0; i < routeValueKeys.Length; i++) + { + expectedRouteValues.Add(routeValueKeys[i], routeValues[i]); + } + + var builder = CreateBuilder(); + + // We setup the route entries in reverse order of precedence to ensure that when we + // try to route the request, the route with a higher precedence gets tried first. + foreach (var template in routes.Reverse()) + { + MapInboundEntry(builder, template); + } + + var route = builder.Build(); + + var context = CreateRouteContext(url); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]); + foreach (var entry in expectedRouteValues) + { + var data = Assert.Single(context.RouteData.Values, v => v.Key == entry.Key); + Assert.Equal(entry.Value, data.Value); + } + } + + [Fact] + public async Task TreeRouter_RouteAsync_MatchesCatchAllRoutesWithDefaults() + { + // Arrange + var routes = new[] { + "{parameter1=1}/{parameter2=2}/{parameter3=3}/{*parameter4=4}", + }; + var url = "/a/b/c"; + var routeValues = new[] { "a", "b", "c", "4" }; + + var expectedRouteGroup = CreateRouteGroup(0, "{parameter1=1}/{parameter2=2}/{parameter3=3}/{*parameter4=4}"); + var routeValueKeys = new[] { "parameter1", "parameter2", "parameter3", "parameter4" }; + var expectedRouteValues = new RouteValueDictionary(); + for (int i = 0; i < routeValueKeys.Length; i++) + { + expectedRouteValues.Add(routeValueKeys[i], routeValues[i]); + } + + var builder = CreateBuilder(); + + // We setup the route entries in reverse order of precedence to ensure that when we + // try to route the request, the route with a higher precedence gets tried first. + foreach (var template in routes.Reverse()) + { + MapInboundEntry(builder, template); + } + + var route = builder.Build(); + + var context = CreateRouteContext(url); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]); + foreach (var entry in expectedRouteValues) + { + var data = Assert.Single(context.RouteData.Values, v => v.Key == entry.Key); + Assert.Equal(entry.Value, data.Value); + } + } + [Fact] public async Task TreeRouter_RouteAsync_DoesNotMatchShorterUrl() {