diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/Abstractions/ActionDescriptor.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/Abstractions/ActionDescriptor.cs index 80bd3f55c5..383ebf6154 100644 --- a/src/Microsoft.AspNetCore.Mvc.Abstractions/Abstractions/ActionDescriptor.cs +++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/Abstractions/ActionDescriptor.cs @@ -29,7 +29,7 @@ namespace Microsoft.AspNetCore.Mvc.Abstractions public AttributeRouteInfo AttributeRouteInfo { get; set; } - public IDictionary RouteValueDefaults { get; } + public IDictionary RouteValueDefaults { get; set; } /// /// The set of constraints for this action. Must all be satisfied for the action to be selected. @@ -53,6 +53,6 @@ namespace Microsoft.AspNetCore.Mvc.Abstractions /// /// Stores arbitrary metadata properties associated with the . /// - public IDictionary Properties { get; } + public IDictionary Properties { get; set; } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/AttributeRoute.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/AttributeRoute.cs index 5b94ffb36d..8ddd32aefe 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/AttributeRoute.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/AttributeRoute.cs @@ -98,23 +98,28 @@ namespace Microsoft.AspNetCore.Mvc.Internal // it on startup. if (_router == null || _router.Version != actions.Version) { - _router = BuildRoute(actions); + var entries = GetEntries(actions); + _router = BuildRoute(entries, actions.Version); } return _router; } - private TreeRouter BuildRoute(ActionDescriptorCollection actions) + // internal for testing + internal AttributeRouteEntries GetEntries(ActionDescriptorCollection actions) { - var routeBuilder = new TreeRouteBuilder(_target, _loggerFactory); + var entries = new AttributeRouteEntries(); + var routeInfos = GetRouteInfos(_constraintResolver, actions.Items); - // We're creating one AttributeRouteGenerationEntry per action. This allows us to match the intended + // We're creating one TreeRouteLinkGenerationEntry per action. This allows us to match the intended // action by expected route values, and then use the TemplateBinder to generate the link. foreach (var routeInfo in routeInfos) { - routeBuilder.Add(new TreeRouteLinkGenerationEntry() + entries.LinkGenerationEntries.Add(new TreeRouteLinkGenerationEntry() { + // Using routeInfo.Defaults here WITHOUT adding the RouteGroupKey. We don't want to impact the + // behavior of link generation. Binder = new TemplateBinder(_urlEncoder, _contextPool, routeInfo.ParsedTemplate, routeInfo.Defaults), Defaults = routeInfo.Defaults, Constraints = routeInfo.Constraints, @@ -133,24 +138,44 @@ namespace Microsoft.AspNetCore.Mvc.Internal var distinctRouteInfosByGroup = GroupRouteInfosByGroupId(routeInfos); foreach (var routeInfo in distinctRouteInfosByGroup) { - routeBuilder.Add(new TreeRouteMatchingEntry() + // Note that because we only support 'inline' defaults, each routeInfo group also has the same + // set of defaults. + // + // We then inject the route group as a default for the matcher so it gets passed back to MVC + // for use in action selection. + var defaults = new RouteValueDictionary(routeInfo.Defaults); + defaults[TreeRouter.RouteGroupKey] = routeInfo.RouteGroup; + + entries.MatchingEntries.Add(new TreeRouteMatchingEntry() { Order = routeInfo.Order, Precedence = routeInfo.MatchPrecedence, Target = _target, RouteName = routeInfo.Name, - RouteTemplate = TemplateParser.Parse(routeInfo.RouteTemplate), - TemplateMatcher = new TemplateMatcher( - routeInfo.ParsedTemplate, - new RouteValueDictionary(StringComparer.OrdinalIgnoreCase) - { - { TreeRouter.RouteGroupKey, routeInfo.RouteGroup } - }), - Constraints = routeInfo.Constraints + RouteTemplate = routeInfo.ParsedTemplate, + TemplateMatcher = new TemplateMatcher(routeInfo.ParsedTemplate, defaults), + Constraints = routeInfo.Constraints, }); } - return routeBuilder.Build(actions.Version); + return entries; + } + + private TreeRouter BuildRoute(AttributeRouteEntries entries, int version) + { + var routeBuilder = new TreeRouteBuilder(_target, _loggerFactory); + + foreach (var entry in entries.LinkGenerationEntries) + { + routeBuilder.Add(entry); + } + + foreach (var entry in entries.MatchingEntries) + { + routeBuilder.Add(entry); + } + + return routeBuilder.Build(version); } private static IEnumerable GroupRouteInfosByGroupId(List routeInfos) @@ -182,8 +207,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal // of memory, so sharing is worthwhile. var templateCache = new Dictionary(StringComparer.OrdinalIgnoreCase); - var attributeRoutedActions = actions.Where(a => a.AttributeRouteInfo != null && - a.AttributeRouteInfo.Template != null); + var attributeRoutedActions = actions.Where(a => a.AttributeRouteInfo?.Template != null); foreach (var action in attributeRoutedActions) { var routeInfo = GetRouteInfo(constraintResolver, templateCache, action); diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/AttributeRouteEntries.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/AttributeRouteEntries.cs new file mode 100644 index 0000000000..d1acc42b72 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/AttributeRouteEntries.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.AspNetCore.Routing.Tree; + +namespace Microsoft.AspNetCore.Mvc.Internal +{ + public class AttributeRouteEntries + { + public List LinkGenerationEntries { get; } = new List(); + + public List MatchingEntries { get; } = new List(); + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/AttributeRouteTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/AttributeRouteTest.cs index 19ebf862a6..d05ac8019f 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/AttributeRouteTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/AttributeRouteTest.cs @@ -3,13 +3,16 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Constraints; using Microsoft.AspNetCore.Routing.Internal; +using Microsoft.AspNetCore.Routing.Template; using Microsoft.AspNetCore.Routing.Tree; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; @@ -31,14 +34,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal public async Task AttributeRoute_UsesUpdatedActionDescriptors() { // Arrange - var handler = new Mock(MockBehavior.Strict); - handler - .Setup(h => h.RouteAsync(It.IsAny())) - .Callback(c => c.Handler = NullHandler) - .Returns(Task.FromResult(true)) - .Verifiable(); + var handler = CreateHandler(); - var actionDescriptors = new List() + var actions = new List() { new ActionDescriptor() { @@ -64,21 +62,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal }, }; - var actionDescriptorProvider = new Mock(MockBehavior.Strict); - actionDescriptorProvider - .SetupGet(ad => ad.ActionDescriptors) - .Returns(new ActionDescriptorCollection(actionDescriptors, version: 1)); - - var policy = new UriBuilderContextPooledObjectPolicy(new UrlTestEncoder()); - var pool = new DefaultObjectPool(policy); - - var route = new AttributeRoute( - handler.Object, - actionDescriptorProvider.Object, - Mock.Of(), - pool, - new UrlTestEncoder(), - NullLoggerFactory.Instance); + var actionDescriptorProvider = CreateActionDescriptorProvider(actions); + var route = CreateRoute(handler.Object, actionDescriptorProvider.Object); var requestServices = new Mock(MockBehavior.Strict); requestServices @@ -102,10 +87,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal handler.Verify(h => h.RouteAsync(It.IsAny()), Times.Once()); // Arrange 2 - remove the action and update the collection - actionDescriptors.RemoveAt(1); + actions.RemoveAt(1); actionDescriptorProvider .SetupGet(ad => ad.ActionDescriptors) - .Returns(new ActionDescriptorCollection(actionDescriptors, version: 2)); + .Returns(new ActionDescriptorCollection(actions, version: 2)); context = new RouteContext(httpContext); @@ -118,5 +103,513 @@ namespace Microsoft.AspNetCore.Mvc.Internal handler.Verify(h => h.RouteAsync(It.IsAny()), Times.Once()); } + + [Fact] + public void AttributeRoute_GetEntries_CreatesLinkGenerationEntry() + { + // Arrange + var actions = new List() + { + new ActionDescriptor() + { + AttributeRouteInfo = new AttributeRouteInfo() + { + Template = "api/Blog/{id}", + Name = "BLOG_INDEX", + Order = 17, + }, + RouteConstraints = new List() + { + new RouteDataActionConstraint(TreeRouter.RouteGroupKey, "1"), + }, + RouteValueDefaults = new Dictionary() + { + { "controller", "Blog" }, + { "action", "Index" }, + }, + }, + }; + + var actionDescriptorProvider = CreateActionDescriptorProvider(actions); + + var route = CreateRoute(CreateHandler().Object, actionDescriptorProvider.Object); + + // Act + var entries = route.GetEntries(actionDescriptorProvider.Object.ActionDescriptors); + + // Assert + Assert.Collection( + entries.LinkGenerationEntries, + e => + { + Assert.NotNull(e.Binder); + Assert.Empty(e.Constraints); + Assert.Empty(e.Defaults); + Assert.Equal(RoutePrecedence.ComputeGenerated(e.Template), e.GenerationPrecedence); + Assert.Equal("BLOG_INDEX", e.Name); + Assert.Equal(17, e.Order); + Assert.Equal(actions[0].RouteValueDefaults, e.RequiredLinkValues); + Assert.Equal("1", e.RouteGroup); + Assert.Equal("api/Blog/{id}", e.Template.TemplateText); + }); + } + + [Fact] + public void AttributeRoute_GetEntries_CreatesLinkGenerationEntry_WithConstraint() + { + // Arrange + var actions = new List() + { + new ActionDescriptor() + { + AttributeRouteInfo = new AttributeRouteInfo() + { + Template = "api/Blog/{id:int}", + Name = "BLOG_INDEX", + Order = 17, + }, + RouteConstraints = new List() + { + new RouteDataActionConstraint(TreeRouter.RouteGroupKey, "1"), + }, + RouteValueDefaults = new Dictionary() + { + { "controller", "Blog" }, + { "action", "Index" }, + }, + }, + }; + + var actionDescriptorProvider = CreateActionDescriptorProvider(actions); + + var route = CreateRoute(CreateHandler().Object, actionDescriptorProvider.Object); + + // Act + var entries = route.GetEntries(actionDescriptorProvider.Object.ActionDescriptors); + + // Assert + Assert.Collection( + entries.LinkGenerationEntries, + e => + { + Assert.NotNull(e.Binder); + Assert.Single(e.Constraints, kvp => kvp.Key == "id"); + Assert.Empty(e.Defaults); + Assert.Equal(RoutePrecedence.ComputeGenerated(e.Template), e.GenerationPrecedence); + Assert.Equal("BLOG_INDEX", e.Name); + Assert.Equal(17, e.Order); + Assert.Equal(actions[0].RouteValueDefaults, e.RequiredLinkValues); + Assert.Equal("1", e.RouteGroup); + Assert.Equal("api/Blog/{id:int}", e.Template.TemplateText); + }); + } + + [Fact] + public void AttributeRoute_GetEntries_CreatesLinkGenerationEntry_WithDefault() + { + // Arrange + var actions = new List() + { + new ActionDescriptor() + { + AttributeRouteInfo = new AttributeRouteInfo() + { + Template = "api/Blog/{*slug=hello}", + Name = "BLOG_INDEX", + Order = 17, + }, + RouteConstraints = new List() + { + new RouteDataActionConstraint(TreeRouter.RouteGroupKey, "1"), + }, + RouteValueDefaults = new Dictionary() + { + { "controller", "Blog" }, + { "action", "Index" }, + }, + }, + }; + + var actionDescriptorProvider = CreateActionDescriptorProvider(actions); + + var route = CreateRoute(CreateHandler().Object, actionDescriptorProvider.Object); + + // Act + var entries = route.GetEntries(actionDescriptorProvider.Object.ActionDescriptors); + + // Assert + Assert.Collection( + entries.LinkGenerationEntries, + e => + { + Assert.NotNull(e.Binder); + Assert.Empty(e.Constraints); + Assert.Equal(new RouteValueDictionary(new { slug = "hello" }), e.Defaults); + Assert.Equal(RoutePrecedence.ComputeGenerated(e.Template), e.GenerationPrecedence); + Assert.Equal("BLOG_INDEX", e.Name); + Assert.Equal(17, e.Order); + Assert.Equal(actions[0].RouteValueDefaults, e.RequiredLinkValues); + Assert.Equal("1", e.RouteGroup); + Assert.Equal("api/Blog/{*slug=hello}", e.Template.TemplateText); + }); + } + + // These actions seem like duplicates, but this is a real case that can happen where two different + // actions define the same route info. Link generation happens based on the action name + controller + // name. + [Fact] + public void AttributeRoute_GetEntries_CreatesLinkGenerationEntry_ForEachAction() + { + // Arrange + var actions = new List() + { + new ActionDescriptor() + { + AttributeRouteInfo = new AttributeRouteInfo() + { + Template = "api/Blog/{id}", + Name = "BLOG_INDEX", + Order = 17, + }, + RouteConstraints = new List() + { + new RouteDataActionConstraint(TreeRouter.RouteGroupKey, "1"), + }, + RouteValueDefaults = new Dictionary() + { + { "controller", "Blog" }, + { "action", "Index" }, + }, + }, + new ActionDescriptor() + { + AttributeRouteInfo = new AttributeRouteInfo() + { + Template = "api/Blog/{id}", + Name = "BLOG_INDEX", + Order = 17, + }, + RouteConstraints = new List() + { + new RouteDataActionConstraint(TreeRouter.RouteGroupKey, "1"), + }, + RouteValueDefaults = new Dictionary() + { + { "controller", "Blog" }, + { "action", "Index2" }, + }, + }, + }; + + var actionDescriptorProvider = CreateActionDescriptorProvider(actions); + + var route = CreateRoute(CreateHandler().Object, actionDescriptorProvider.Object); + + // Act + var entries = route.GetEntries(actionDescriptorProvider.Object.ActionDescriptors); + + // Assert + Assert.Collection( + entries.LinkGenerationEntries, + e => + { + Assert.NotNull(e.Binder); + Assert.Empty(e.Constraints); + Assert.Empty(e.Defaults); + Assert.Equal(RoutePrecedence.ComputeGenerated(e.Template), e.GenerationPrecedence); + Assert.Equal("BLOG_INDEX", e.Name); + Assert.Equal(17, e.Order); + Assert.Equal(actions[0].RouteValueDefaults, e.RequiredLinkValues); + Assert.Equal("1", e.RouteGroup); + Assert.Equal("api/Blog/{id}", e.Template.TemplateText); + }, + e => + { + Assert.NotNull(e.Binder); + Assert.Empty(e.Constraints); + Assert.Empty(e.Defaults); + Assert.Equal(RoutePrecedence.ComputeGenerated(e.Template), e.GenerationPrecedence); + Assert.Equal("BLOG_INDEX", e.Name); + Assert.Equal(17, e.Order); + Assert.Equal(actions[1].RouteValueDefaults, e.RequiredLinkValues); + Assert.Equal("1", e.RouteGroup); + Assert.Equal("api/Blog/{id}", e.Template.TemplateText); + }); + } + + [Fact] + public void AttributeRoute_GetEntries_CreatesMatchingEntry() + { + // Arrange + var actions = new List() + { + new ActionDescriptor() + { + AttributeRouteInfo = new AttributeRouteInfo() + { + Template = "api/Blog/{id}", + Name = "BLOG_INDEX", + Order = 17, + }, + RouteConstraints = new List() + { + new RouteDataActionConstraint(TreeRouter.RouteGroupKey, "1"), + }, + RouteValueDefaults = new Dictionary() + { + { "controller", "Blog" }, + { "action", "Index" }, + }, + }, + }; + + var actionDescriptorProvider = CreateActionDescriptorProvider(actions); + + var handler = CreateHandler().Object; + var route = CreateRoute(handler, actionDescriptorProvider.Object); + + // Act + var entries = route.GetEntries(actionDescriptorProvider.Object.ActionDescriptors); + + // Assert + Assert.Collection( + entries.MatchingEntries, + e => + { + Assert.Empty(e.Constraints); + Assert.Equal(17, e.Order); + Assert.Equal(RoutePrecedence.ComputeMatched(e.RouteTemplate), e.Precedence); + Assert.Equal("BLOG_INDEX", e.RouteName); + Assert.Equal("api/Blog/{id}", e.RouteTemplate.TemplateText); + Assert.Same(handler, e.Target); + Assert.Collection( + e.TemplateMatcher.Defaults.OrderBy(kvp => kvp.Key), + kvp => Assert.Equal(new KeyValuePair(TreeRouter.RouteGroupKey, "1"), kvp)); + Assert.Same(e.RouteTemplate, e.TemplateMatcher.Template); + }); + } + + [Fact] + public void AttributeRoute_GetEntries_CreatesMatchingEntry_WithConstraint() + { + // Arrange + var actions = new List() + { + new ActionDescriptor() + { + AttributeRouteInfo = new AttributeRouteInfo() + { + Template = "api/Blog/{id:int}", + Name = "BLOG_INDEX", + Order = 17, + }, + RouteConstraints = new List() + { + new RouteDataActionConstraint(TreeRouter.RouteGroupKey, "1"), + }, + RouteValueDefaults = new Dictionary() + { + { "controller", "Blog" }, + { "action", "Index" }, + }, + }, + }; + + var actionDescriptorProvider = CreateActionDescriptorProvider(actions); + + var handler = CreateHandler().Object; + var route = CreateRoute(handler, actionDescriptorProvider.Object); + + // Act + var entries = route.GetEntries(actionDescriptorProvider.Object.ActionDescriptors); + + // Assert + Assert.Collection( + entries.MatchingEntries, + e => + { + Assert.Single(e.Constraints, kvp => kvp.Key == "id"); + Assert.Equal(17, e.Order); + Assert.Equal(RoutePrecedence.ComputeMatched(e.RouteTemplate), e.Precedence); + Assert.Equal("BLOG_INDEX", e.RouteName); + Assert.Equal("api/Blog/{id:int}", e.RouteTemplate.TemplateText); + Assert.Same(handler, e.Target); + Assert.Collection( + e.TemplateMatcher.Defaults.OrderBy(kvp => kvp.Key), + kvp => Assert.Equal(new KeyValuePair(TreeRouter.RouteGroupKey, "1"), kvp)); + Assert.Same(e.RouteTemplate, e.TemplateMatcher.Template); + }); + } + + [Fact] + public void AttributeRoute_GetEntries_CreatesMatchingEntry_WithDefault() + { + // Arrange + var actions = new List() + { + new ActionDescriptor() + { + AttributeRouteInfo = new AttributeRouteInfo() + { + Template = "api/Blog/{*slug=hello}", + Name = "BLOG_INDEX", + Order = 17, + }, + RouteConstraints = new List() + { + new RouteDataActionConstraint(TreeRouter.RouteGroupKey, "1"), + }, + RouteValueDefaults = new Dictionary() + { + { "controller", "Blog" }, + { "action", "Index" }, + }, + }, + }; + + var actionDescriptorProvider = CreateActionDescriptorProvider(actions); + + var handler = CreateHandler().Object; + var route = CreateRoute(handler, actionDescriptorProvider.Object); + + // Act + var entries = route.GetEntries(actionDescriptorProvider.Object.ActionDescriptors); + + // Assert + Assert.Collection( + entries.MatchingEntries, + e => + { + Assert.Empty(e.Constraints); + Assert.Equal(17, e.Order); + Assert.Equal(RoutePrecedence.ComputeMatched(e.RouteTemplate), e.Precedence); + Assert.Equal("BLOG_INDEX", e.RouteName); + Assert.Equal("api/Blog/{*slug=hello}", e.RouteTemplate.TemplateText); + Assert.Same(handler, e.Target); + Assert.Collection( + e.TemplateMatcher.Defaults.OrderBy(kvp => kvp.Key), + kvp => Assert.Equal(new KeyValuePair(TreeRouter.RouteGroupKey, "1"), kvp), + kvp => Assert.Equal(new KeyValuePair("slug", "hello"), kvp)); + Assert.Same(e.RouteTemplate, e.TemplateMatcher.Template); + }); + } + + // These actions seem like duplicates, but this is a real case that can happen where two different + // actions define the same route info. Link generation happens based on the action name + controller + // name. + [Fact] + public void AttributeRoute_GetEntries_CreatesMatchingEntry_CombinesLikeActions() + { + // Arrange + var actions = new List() + { + new ActionDescriptor() + { + AttributeRouteInfo = new AttributeRouteInfo() + { + Template = "api/Blog/{id}", + Name = "BLOG_INDEX", + Order = 17, + }, + RouteConstraints = new List() + { + new RouteDataActionConstraint(TreeRouter.RouteGroupKey, "1"), + }, + RouteValueDefaults = new Dictionary() + { + { "controller", "Blog" }, + { "action", "Index" }, + }, + }, + new ActionDescriptor() + { + AttributeRouteInfo = new AttributeRouteInfo() + { + Template = "api/Blog/{id}", + Name = "BLOG_INDEX", + Order = 17, + }, + RouteConstraints = new List() + { + new RouteDataActionConstraint(TreeRouter.RouteGroupKey, "1"), + }, + RouteValueDefaults = new Dictionary() + { + { "controller", "Blog" }, + { "action", "Index2" }, + }, + }, + }; + + var actionDescriptorProvider = CreateActionDescriptorProvider(actions); + + var handler = CreateHandler().Object; + var route = CreateRoute(handler, actionDescriptorProvider.Object); + + // Act + var entries = route.GetEntries(actionDescriptorProvider.Object.ActionDescriptors); + + // Assert + Assert.Collection( + entries.MatchingEntries, + e => + { + Assert.Empty(e.Constraints); + Assert.Equal(17, e.Order); + Assert.Equal(RoutePrecedence.ComputeMatched(e.RouteTemplate), e.Precedence); + Assert.Equal("BLOG_INDEX", e.RouteName); + Assert.Equal("api/Blog/{id}", e.RouteTemplate.TemplateText); + Assert.Same(handler, e.Target); + Assert.Collection( + e.TemplateMatcher.Defaults.OrderBy(kvp => kvp.Key), + kvp => Assert.Equal(new KeyValuePair(TreeRouter.RouteGroupKey, "1"), kvp)); + Assert.Same(e.RouteTemplate, e.TemplateMatcher.Template); + }); + } + + private static Mock CreateHandler() + { + var handler = new Mock(MockBehavior.Strict); + handler + .Setup(h => h.RouteAsync(It.IsAny())) + .Callback(c => c.Handler = NullHandler) + .Returns(TaskCache.CompletedTask) + .Verifiable(); + return handler; + } + + private static Mock CreateActionDescriptorProvider( + IReadOnlyList actions) + { + var actionDescriptorProvider = new Mock(MockBehavior.Strict); + actionDescriptorProvider + .SetupGet(ad => ad.ActionDescriptors) + .Returns(new ActionDescriptorCollection(actions, version: 1)); + + return actionDescriptorProvider; + } + + private static AttributeRoute CreateRoute( + IRouter handler, + IActionDescriptorCollectionProvider actionDescriptorProvider) + { + var constraintResolver = new Mock(); + constraintResolver + .Setup(c => c.ResolveConstraint("int")) + .Returns(new IntRouteConstraint()); + + var policy = new UriBuilderContextPooledObjectPolicy(new UrlTestEncoder()); + var pool = new DefaultObjectPool(policy); + + var route = new AttributeRoute( + handler, + actionDescriptorProvider, + constraintResolver.Object, + pool, + new UrlTestEncoder(), + NullLoggerFactory.Instance); + + return route; + } } } diff --git a/test/WebSites/RoutingWebSite/Controllers/TeamController.cs b/test/WebSites/RoutingWebSite/Controllers/TeamController.cs index 85f1c0fc5c..71ab959a19 100644 --- a/test/WebSites/RoutingWebSite/Controllers/TeamController.cs +++ b/test/WebSites/RoutingWebSite/Controllers/TeamController.cs @@ -63,8 +63,8 @@ namespace RoutingWebSite return Content(Url.Action(), "text/plain"); } - [HttpGet("/TeamName/{*Name}/")] - public ActionResult GetTeam(string name = "DefaultName") + [HttpGet("/TeamName/{*Name=DefaultName}/")] + public ActionResult GetTeam(string name) { return _generator.Generate("/TeamName/" + name); }