From 54c14b8782bc2a11528b04eb699aeca3375e2884 Mon Sep 17 00:00:00 2001 From: Kiran Challa Date: Mon, 25 Jun 2018 14:24:34 -0700 Subject: [PATCH] Reacting to Routing repo's EndpointFinder changes --- .../Builder/MvcEndpointInfo.cs | 9 +- .../Internal/MvcEndpointDataSource.cs | 101 ++++- .../Routing/DispatcherUrlHelper.cs | 35 +- .../Routing/UrlHelperFactory.cs | 14 +- .../Internal/MvcEndpointDataSourceTests.cs | 384 +++++++++++++++--- .../Routing/DispatcherUrlHelperTest.cs | 72 ++-- 6 files changed, 511 insertions(+), 104 deletions(-) diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcEndpointInfo.cs b/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcEndpointInfo.cs index f88f6e4ded..bb6a9df95f 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcEndpointInfo.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcEndpointInfo.cs @@ -30,7 +30,8 @@ namespace Microsoft.AspNetCore.Builder ParsedTemplate = TemplateParser.Parse(template); Constraints = GetConstraints(constraintResolver, ParsedTemplate, constraints); - Defaults = GetDefaults(ParsedTemplate, defaults); + Defaults = defaults; + MergedDefaults = GetDefaults(ParsedTemplate, defaults); } catch (Exception exception) { @@ -41,7 +42,13 @@ namespace Microsoft.AspNetCore.Builder public string Name { get; } public string Template { get; } + + // Non-inline defaults public RouteValueDictionary Defaults { get; } + + // Inline and non-inline defaults merged into one + public RouteValueDictionary MergedDefaults { get; } + public IDictionary Constraints { get; } public RouteValueDictionary DataTokens { get; } internal RouteTemplate ParsedTemplate { get; private set; } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcEndpointDataSource.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcEndpointDataSource.cs index 7bce3996a5..dab81b662b 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcEndpointDataSource.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcEndpointDataSource.cs @@ -68,6 +68,14 @@ namespace Microsoft.AspNetCore.Mvc.Internal { if (action.AttributeRouteInfo == null) { + // In traditional conventional routing setup, the routes defined by a user have a static order + // defined by how they are added into the list. We would like to maintain the same order when building + // up the endpoints too. + // + // Start with an order of '1' for conventional routes as attribute routes have a default order of '0'. + // This is for scenarios dealing with migrating existing Routing based code to Dispatcher world. + var conventionalRouteOrder = 0; + // Check each of the conventional templates to see if the action would be reachable // If the action and template are compatible then create an endpoint with the // area/controller/action parameter parts replaced with literals @@ -100,7 +108,13 @@ namespace Microsoft.AspNetCore.Mvc.Internal { var subTemplate = RouteTemplateWriter.ToString(newEndpointTemplate.Segments.Take(i)); - var subEndpoint = CreateEndpoint(action, subTemplate, 0, endpointInfo); + var subEndpoint = CreateEndpoint( + action, + endpointInfo.Name, + subTemplate, + endpointInfo.Defaults, + ++conventionalRouteOrder, + endpointInfo); _endpoints.Add(subEndpoint); } @@ -119,14 +133,26 @@ namespace Microsoft.AspNetCore.Mvc.Internal var newTemplate = RouteTemplateWriter.ToString(newEndpointTemplate.Segments); - var endpoint = CreateEndpoint(action, newTemplate, 0, endpointInfo); + var endpoint = CreateEndpoint( + action, + endpointInfo.Name, + newTemplate, + endpointInfo.Defaults, + ++conventionalRouteOrder, + endpointInfo); _endpoints.Add(endpoint); } } } else { - var endpoint = CreateEndpoint(action, action.AttributeRouteInfo.Template, action.AttributeRouteInfo.Order, action.AttributeRouteInfo); + var endpoint = CreateEndpoint( + action, + action.AttributeRouteInfo.Name, + action.AttributeRouteInfo.Template, + nonInlineDefaults: null, + action.AttributeRouteInfo.Order, + action.AttributeRouteInfo); _endpoints.Add(endpoint); } } @@ -144,7 +170,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal return false; } - private bool UseDefaultValuePlusRemainingSegementsOptional(int segmentIndex, ActionDescriptor action, MvcEndpointInfo endpointInfo, RouteTemplate template) + private bool UseDefaultValuePlusRemainingSegementsOptional( + int segmentIndex, + ActionDescriptor action, + MvcEndpointInfo endpointInfo, + RouteTemplate template) { // Check whether the remaining segments are all optional and one or more of them is // for area/controller/action and has a default value @@ -164,7 +194,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal { if (IsMvcParameter(part.Name)) { - if (endpointInfo.Defaults[part.Name] is string defaultValue + if (endpointInfo.MergedDefaults[part.Name] is string defaultValue && action.RouteValues.TryGetValue(part.Name, out var routeValue) && string.Equals(defaultValue, routeValue, StringComparison.OrdinalIgnoreCase)) { @@ -196,7 +226,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal } else { - if (endpointInfo.Defaults != null && string.Equals(actionValue, endpointInfo.Defaults[routeKey] as string, StringComparison.OrdinalIgnoreCase)) + if (endpointInfo.MergedDefaults != null && string.Equals(actionValue, endpointInfo.MergedDefaults[routeKey] as string, StringComparison.OrdinalIgnoreCase)) { return true; } @@ -235,7 +265,13 @@ namespace Microsoft.AspNetCore.Mvc.Internal } } - private MatcherEndpoint CreateEndpoint(ActionDescriptor action, string template, int order, object source) + private MatcherEndpoint CreateEndpoint( + ActionDescriptor action, + string routeName, + string template, + object nonInlineDefaults, + int order, + object source) { RequestDelegate invokerDelegate = (context) => { @@ -260,10 +296,16 @@ namespace Microsoft.AspNetCore.Mvc.Internal metadata.Add(source); metadata.Add(action); + if (!string.IsNullOrEmpty(routeName)) + { + metadata.Add(new RouteNameMetadata(routeName)); + } + // Add filter descriptors to endpoint metadata if (action.FilterDescriptors != null && action.FilterDescriptors.Count > 0) { - metadata.AddRange(action.FilterDescriptors.OrderBy(f => f, FilterDescriptorOrderComparer.Comparer).Select(f => f.Filter)); + metadata.AddRange(action.FilterDescriptors.OrderBy(f => f, FilterDescriptorOrderComparer.Comparer) + .Select(f => f.Filter)); } if (action.ActionConstraints != null && action.ActionConstraints.Count > 0) @@ -281,14 +323,41 @@ namespace Microsoft.AspNetCore.Mvc.Internal var endpoint = new MatcherEndpoint( next => invokerDelegate, template, - action.RouteValues, + new RouteValueDictionary(nonInlineDefaults), + new RouteValueDictionary(action.RouteValues), order, metadataCollection, - action.DisplayName, - address: null); + action.DisplayName); + + // Use defaults after the endpoint is created as it merges both the inline and + // non-inline defaults into one. + EnsureRequiredValuesInDefaults(endpoint.RequiredValues, endpoint.Defaults); + return endpoint; } + // Ensure required values are a subset of defaults + // Examples: + // + // Template: {controller}/{action}/{category}/{id?} + // Defaults(in-line or non in-line): category=products + // Required values: controller=foo, action=bar + // Final constructed template: foo/bar/{category}/{id?} + // Final defaults: controller=foo, action=bar, category=products + // + // Template: {controller=Home}/{action=Index}/{category=products}/{id?} + // Defaults: controller=Home, action=Index, category=products + // Required values: controller=foo, action=bar + // Final constructed template: foo/bar/{category}/{id?} + // Final defaults: controller=foo, action=bar, category=products + private void EnsureRequiredValuesInDefaults(RouteValueDictionary requiredValues, RouteValueDictionary defaults) + { + foreach (var kvp in requiredValues) + { + defaults[kvp.Key] = kvp.Value; + } + } + private IChangeToken GetCompositeChangeToken() { if (_actionDescriptorChangeProviders.Length == 1) @@ -321,5 +390,15 @@ namespace Microsoft.AspNetCore.Mvc.Internal public override IReadOnlyList Endpoints => _endpoints; public List ConventionalEndpointInfos { get; } + + private class RouteNameMetadata : IRouteNameMetadata + { + public RouteNameMetadata(string routeName) + { + Name = routeName; + } + + public string Name { get; } + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Routing/DispatcherUrlHelper.cs b/src/Microsoft.AspNetCore.Mvc.Core/Routing/DispatcherUrlHelper.cs index bb6e1a1bdc..ea56fb5b6a 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Routing/DispatcherUrlHelper.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Routing/DispatcherUrlHelper.cs @@ -3,6 +3,7 @@ using System; using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.EndpointFinders; using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Mvc.Routing @@ -15,16 +16,21 @@ namespace Microsoft.AspNetCore.Mvc.Routing { private readonly ILogger _logger; private readonly ILinkGenerator _linkGenerator; + private readonly IEndpointFinder _routeValuesBasedEndpointFinder; /// /// Initializes a new instance of the class using the specified /// . /// /// The for the current request. + /// + /// The which finds endpoints by required route values. + /// /// The used to generate the link. /// The . public DispatcherUrlHelper( ActionContext actionContext, + IEndpointFinder routeValuesBasedEndpointFinder, ILinkGenerator linkGenerator, ILogger logger) : base(actionContext) @@ -40,6 +46,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing } _linkGenerator = linkGenerator; + _routeValuesBasedEndpointFinder = routeValuesBasedEndpointFinder; _logger = logger; } @@ -79,12 +86,17 @@ namespace Microsoft.AspNetCore.Mvc.Routing valuesDictionary["controller"] = urlActionContext.Controller; } - var successfullyGeneratedLink = _linkGenerator.TryGetLink( - new LinkGeneratorContext() + var endpoints = _routeValuesBasedEndpointFinder.FindEndpoints( + new RouteValuesBasedEndpointFinderContext() { - SuppliedValues = valuesDictionary, + ExplicitValues = valuesDictionary, AmbientValues = AmbientValues - }, + }); + + var successfullyGeneratedLink = _linkGenerator.TryGetLink( + endpoints, + valuesDictionary, + AmbientValues, out var link); if (!successfullyGeneratedLink) @@ -107,13 +119,18 @@ namespace Microsoft.AspNetCore.Mvc.Routing var valuesDictionary = routeContext.Values as RouteValueDictionary ?? GetValuesDictionary(routeContext.Values); - var successfullyGeneratedLink = _linkGenerator.TryGetLink( - new LinkGeneratorContext() + var endpoints = _routeValuesBasedEndpointFinder.FindEndpoints( + new RouteValuesBasedEndpointFinderContext() { - Address = new Address(routeContext.RouteName), - SuppliedValues = valuesDictionary, + RouteName = routeContext.RouteName, + ExplicitValues = valuesDictionary, AmbientValues = AmbientValues - }, + }); + + var successfullyGeneratedLink = _linkGenerator.TryGetLink( + endpoints, + valuesDictionary, + AmbientValues, out var link); if (!successfullyGeneratedLink) diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelperFactory.cs b/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelperFactory.cs index ba0f7e9607..d8c257ffdc 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelperFactory.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelperFactory.cs @@ -5,6 +5,7 @@ using System; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Core; using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.EndpointFinders; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -49,9 +50,16 @@ namespace Microsoft.AspNetCore.Mvc.Routing var endpointFeature = httpContext.Features.Get(); if (endpointFeature?.Endpoint != null) { - var linkGenerator = httpContext.RequestServices.GetRequiredService(); - var logger = httpContext.RequestServices.GetRequiredService>(); - urlHelper = new DispatcherUrlHelper(context, linkGenerator, logger); + var services = httpContext.RequestServices; + var linkGenerator = services.GetRequiredService(); + var routeValuesBasedEndpointFinder = services.GetRequiredService>(); + var logger = services.GetRequiredService>(); + + urlHelper = new DispatcherUrlHelper( + context, + routeValuesBasedEndpointFinder, + linkGenerator, + logger); } else { diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/MvcEndpointDataSourceTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/MvcEndpointDataSourceTests.cs index f35cc05871..f3b6d778df 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/MvcEndpointDataSourceTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/MvcEndpointDataSourceTests.cs @@ -11,7 +11,6 @@ using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Matchers; @@ -20,7 +19,7 @@ using Microsoft.Extensions.Primitives; using Moq; using Xunit; -namespace Microsoft.AspNetCore.Mvc.Core.Test.Internal +namespace Microsoft.AspNetCore.Mvc.Internal { public class MvcEndpointDataSourceTests { @@ -29,7 +28,7 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test.Internal { // Arrange var routeValue = "Value"; - var routeValues = new Dictionary + var requiredValues = new Dictionary { ["Name"] = routeValue }; @@ -43,7 +42,7 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test.Internal { new ActionDescriptor { - RouteValues = routeValues, + RouteValues = requiredValues, DisplayName = displayName, AttributeRouteInfo = new AttributeRouteInfo { @@ -66,7 +65,7 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test.Internal var endpoint = Assert.Single(dataSource.Endpoints); var matcherEndpoint = Assert.IsType(endpoint); - var endpointValue = matcherEndpoint.Values["Name"]; + var endpointValue = matcherEndpoint.RequiredValues["Name"]; Assert.Equal(routeValue, endpointValue); Assert.Equal(displayName, matcherEndpoint.DisplayName); @@ -187,13 +186,9 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test.Internal public void InitializeEndpoints_SingleAction(string endpointInfoRoute, string[] finalEndpointTemplates) { // Arrange - var mockDescriptorProvider = new Mock(); - mockDescriptorProvider.Setup(m => m.ActionDescriptors).Returns(new ActionDescriptorCollection(new List - { - CreateActionDescriptor("TestController", "TestAction") - }, 0)); - - var dataSource = CreateMvcEndpointDataSource(mockDescriptorProvider.Object); + var actionDescriptorCollection = GetActionDescriptorCollection( + new { controller = "TestController", action = "TestAction" }); + var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, endpointInfoRoute)); // Act @@ -218,13 +213,9 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test.Internal public void InitializeEndpoints_AreaSingleAction(string endpointInfoRoute, string[] finalEndpointTemplates) { // Arrange - var mockDescriptorProvider = new Mock(); - mockDescriptorProvider.Setup(m => m.ActionDescriptors).Returns(new ActionDescriptorCollection(new List - { - CreateActionDescriptor("TestController", "TestAction", "TestArea") - }, 0)); - - var dataSource = CreateMvcEndpointDataSource(mockDescriptorProvider.Object); + var actionDescriptorCollection = GetActionDescriptorCollection( + new { controller = "TestController", action = "TestAction", area = "TestArea" }); + var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, endpointInfoRoute)); // Act @@ -243,13 +234,9 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test.Internal public void InitializeEndpoints_SingleAction_WithActionDefault() { // Arrange - var mockDescriptorProvider = new Mock(); - mockDescriptorProvider.Setup(m => m.ActionDescriptors).Returns(new ActionDescriptorCollection(new List - { - CreateActionDescriptor("TestController", "TestAction") - }, 0)); - - var dataSource = CreateMvcEndpointDataSource(mockDescriptorProvider.Object); + var actionDescriptorCollection = GetActionDescriptorCollection( + new { controller = "TestController", action = "TestAction" }); + var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo( string.Empty, "{controller}/{action}", @@ -268,15 +255,11 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test.Internal public void InitializeEndpoints_MultipleActions_WithActionConstraint() { // Arrange - var mockDescriptorProvider = new Mock(); - mockDescriptorProvider.Setup(m => m.ActionDescriptors).Returns(new ActionDescriptorCollection(new List - { - CreateActionDescriptor("TestController", "TestAction"), - CreateActionDescriptor("TestController", "TestAction1"), - CreateActionDescriptor("TestController", "TestAction2") - }, 0)); - - var dataSource = CreateMvcEndpointDataSource(mockDescriptorProvider.Object); + var actionDescriptorCollection = GetActionDescriptorCollection( + new { controller = "TestController", action = "TestAction" }, + new { controller = "TestController", action = "TestAction1" }, + new { controller = "TestController", action = "TestAction2" }); + var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo( string.Empty, "{controller}/{action}", @@ -297,16 +280,12 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test.Internal public void InitializeEndpoints_MultipleActions(string endpointInfoRoute, string[] finalEndpointTemplates) { // Arrange - var mockDescriptorProvider = new Mock(); - mockDescriptorProvider.Setup(m => m.ActionDescriptors).Returns(new ActionDescriptorCollection(new List - { - CreateActionDescriptor("TestController1", "TestAction1"), - CreateActionDescriptor("TestController1", "TestAction2"), - CreateActionDescriptor("TestController1", "TestAction3"), - CreateActionDescriptor("TestController2", "TestAction1") - }, 0)); - - var dataSource = CreateMvcEndpointDataSource(mockDescriptorProvider.Object); + var actionDescriptorCollection = GetActionDescriptorCollection( + new { controller = "TestController1", action = "TestAction1" }, + new { controller = "TestController1", action = "TestAction2" }, + new { controller = "TestController1", action = "TestAction3" }, + new { controller = "TestController2", action = "TestAction1" }); + var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo( string.Empty, endpointInfoRoute)); @@ -322,6 +301,276 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test.Internal Assert.Collection(dataSource.Endpoints, inspectors); } + [Fact] + public void ConventionalRoute_WithNoRouteName_DoesNotAddRouteNameMetadata() + { + // Arrange + var actionDescriptorCollection = GetActionDescriptorCollection( + new { controller = "Home", action = "Index" }); + var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); + dataSource.ConventionalEndpointInfos.Add( + CreateEndpointInfo(string.Empty, "named/{controller}/{action}/{id?}")); + + // Act + dataSource.InitializeEndpoints(); + + // Assert + var endpoint = Assert.Single(dataSource.Endpoints); + var matcherEndpoint = Assert.IsType(endpoint); + var routeNameMetadata = matcherEndpoint.Metadata.GetMetadata(); + Assert.Null(routeNameMetadata); + } + + [Fact] + public void CanCreateMultipleEndpoints_WithSameRouteName() + { + // Arrange + var actionDescriptorCollection = GetActionDescriptorCollection( + new { controller = "Home", action = "Index" }, + new { controller = "Products", action = "Details" }); + var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); + dataSource.ConventionalEndpointInfos.Add( + CreateEndpointInfo("namedRoute", "named/{controller}/{action}/{id?}")); + + // Act + dataSource.InitializeEndpoints(); + + // Assert + Assert.Collection( + dataSource.Endpoints, + (ep) => + { + var matcherEndpoint = Assert.IsType(ep); + var routeNameMetadata = matcherEndpoint.Metadata.GetMetadata(); + Assert.NotNull(routeNameMetadata); + Assert.Equal("namedRoute", routeNameMetadata.Name); + Assert.Equal("named/Home/Index/{id?}", matcherEndpoint.Template); + }, + (ep) => + { + var matcherEndpoint = Assert.IsType(ep); + var routeNameMetadata = matcherEndpoint.Metadata.GetMetadata(); + Assert.NotNull(routeNameMetadata); + Assert.Equal("namedRoute", routeNameMetadata.Name); + Assert.Equal("named/Products/Details/{id?}", matcherEndpoint.Template); + }); + } + + [Fact] + public void InitializeEndpoints_ConventionalRoutes_StaticallyDefinedOrder_IsMaintained() + { + // Arrange + var actionDescriptorCollection = GetActionDescriptorCollection( + new { controller = "Home", action = "Index" }, + new { controller = "Products", action = "Details" }); + var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); + dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo( + name: string.Empty, + template: "{controller}/{action}/{id?}")); + dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo( + name: "namedRoute", + "named/{controller}/{action}/{id?}")); + + // Act + dataSource.InitializeEndpoints(); + + // Assert + Assert.Collection( + dataSource.Endpoints, + (ep) => + { + var matcherEndpoint = Assert.IsType(ep); + Assert.Equal("Home/Index/{id?}", matcherEndpoint.Template); + Assert.Equal(1, matcherEndpoint.Order); + }, + (ep) => + { + var matcherEndpoint = Assert.IsType(ep); + Assert.Equal("named/Home/Index/{id?}", matcherEndpoint.Template); + Assert.Equal(2, matcherEndpoint.Order); + }, + (ep) => + { + var matcherEndpoint = Assert.IsType(ep); + Assert.Equal("Products/Details/{id?}", matcherEndpoint.Template); + Assert.Equal(1, matcherEndpoint.Order); + }, + (ep) => + { + var matcherEndpoint = Assert.IsType(ep); + Assert.Equal("named/Products/Details/{id?}", matcherEndpoint.Template); + Assert.Equal(2, matcherEndpoint.Order); + }); + } + + [Fact] + public void RequiredValue_WithNoCorresponding_TemplateParameter_DoesNotProduceEndpoint() + { + // Arrange + var requiredValues = new RouteValueDictionary(new { area = "admin", controller = "home", action = "index" }); + var actionDescriptorCollection = GetActionDescriptorCollection(requiredValues); + var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); + dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, "{controller}/{action}")); + + // Act + dataSource.InitializeEndpoints(); + + // Assert + Assert.Empty(dataSource.Endpoints); + } + + // Since area, controller, action and page are special, check to see if the followin test succeeds for a + // custom required value too. + [Fact(Skip = "Needs review")] + public void NonReservedRequiredValue_WithNoCorresponding_TemplateParameter_DoesNotProduceEndpoint() + { + // Arrange + var requiredValues = new RouteValueDictionary(new { controller = "home", action = "index", foo = "bar" }); + var actionDescriptorCollection = GetActionDescriptorCollection(requiredValues); + var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); + dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, "{controller}/{action}")); + + // Act + dataSource.InitializeEndpoints(); + + // Assert + Assert.Empty(dataSource.Endpoints); + } + + [Fact] + public void TemplateParameter_WithNoDefaultOrRequiredValue_DoesNotProduceEndpoint() + { + // Arrange + var requiredValues = new RouteValueDictionary(new { controller = "home", action = "index" }); + var actionDescriptorCollection = GetActionDescriptorCollection(requiredValues); + var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); + dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, "{area}/{controller}/{action}")); + + // Act + dataSource.InitializeEndpoints(); + + // Assert + Assert.Empty(dataSource.Endpoints); + } + + [Fact] + public void TemplateParameter_WithDefaultValue_AndNullRequiredValue_DoesNotProduceEndpoint() + { + // Arrange + var requiredValues = new RouteValueDictionary(new { area = (string)null, controller = "home", action = "index" }); + var actionDescriptorCollection = GetActionDescriptorCollection(requiredValues); + var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); + dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, "{area=admin}/{controller}/{action}")); + + // Act + dataSource.InitializeEndpoints(); + + // Assert + Assert.Empty(dataSource.Endpoints); + } + + [Fact] + public void TemplateParameter_WithNullRequiredValue_DoesNotProduceEndpoint() + { + // Arrange + var requiredValues = new RouteValueDictionary(new { area = (string)null, controller = "home", action = "index" }); + var actionDescriptorCollection = GetActionDescriptorCollection(requiredValues); + var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); + dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, "{area}/{controller}/{action}")); + + // Act + dataSource.InitializeEndpoints(); + + // Assert + Assert.Empty(dataSource.Endpoints); + } + + [Fact] + public void NoDefaultValues_RequiredValues_UsedToCreateDefaultValues() + { + // Arrange + var expectedDefaults = new RouteValueDictionary(new { controller = "Foo", action = "Bar" }); + var actionDescriptorCollection = GetActionDescriptorCollection(requiredValues: expectedDefaults); + var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); + dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, "{controller}/{action}")); + + // Act + dataSource.InitializeEndpoints(); + + // Assert + var endpoint = Assert.Single(dataSource.Endpoints); + var matcherEndpoint = Assert.IsType(endpoint); + Assert.Equal("Foo/Bar", matcherEndpoint.Template); + AssertIsSubset(expectedDefaults, matcherEndpoint.Defaults); + } + + [Fact] + public void RequiredValues_NotPresent_InDefaultValues_IsAddedToDefaultValues() + { + // Arrange + var requiredValues = new RouteValueDictionary( + new { controller = "Foo", action = "Bar", subarea = "test" }); + var expectedDefaults = requiredValues; + var actionDescriptorCollection = GetActionDescriptorCollection(requiredValues: requiredValues); + var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); + dataSource.ConventionalEndpointInfos.Add( + CreateEndpointInfo(string.Empty, "{controller=Home}/{action=Index}")); + + // Act + dataSource.InitializeEndpoints(); + + // Assert + var endpoint = Assert.Single(dataSource.Endpoints); + var matcherEndpoint = Assert.IsType(endpoint); + Assert.Equal("Foo/Bar", matcherEndpoint.Template); + AssertIsSubset(expectedDefaults, matcherEndpoint.Defaults); + } + + [Fact] + public void RequiredValues_IsSubsetOf_DefaultValues() + { + // Arrange + var requiredValues = new RouteValueDictionary( + new { controller = "Foo", action = "Bar", subarea = "test" }); + var expectedDefaults = new RouteValueDictionary( + new { controller = "Foo", action = "Bar", subarea = "test", subscription = "general" }); + var actionDescriptorCollection = GetActionDescriptorCollection(requiredValues: requiredValues); + var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); + dataSource.ConventionalEndpointInfos.Add( + CreateEndpointInfo(string.Empty, "{controller=Home}/{action=Index}/{subscription=general}")); + + // Act + dataSource.InitializeEndpoints(); + + // Assert + var endpoint = Assert.Single(dataSource.Endpoints); + var matcherEndpoint = Assert.IsType(endpoint); + Assert.Equal("Foo/Bar/{subscription=general}", matcherEndpoint.Template); + AssertIsSubset(expectedDefaults, matcherEndpoint.Defaults); + } + + [Fact] + public void RequiredValues_HavingNull_AndNotPresentInDefaultValues_IsAddedToDefaultValues() + { + // Arrange + var requiredValues = new RouteValueDictionary( + new { area = (string)null, controller = "Foo", action = "Bar", page = (string)null }); + var expectedDefaults = requiredValues; + var actionDescriptorCollection = GetActionDescriptorCollection(requiredValues: requiredValues); + var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); + dataSource.ConventionalEndpointInfos.Add( + CreateEndpointInfo(string.Empty, "{controller=Home}/{action=Index}")); + + // Act + dataSource.InitializeEndpoints(); + + // Assert + var endpoint = Assert.Single(dataSource.Endpoints); + var matcherEndpoint = Assert.IsType(endpoint); + Assert.Equal("Foo/Bar", matcherEndpoint.Template); + AssertIsSubset(expectedDefaults, matcherEndpoint.Defaults); + } + private MvcEndpointDataSource CreateMvcEndpointDataSource( IActionDescriptorCollectionProvider actionDescriptorCollectionProvider = null, MvcEndpointInvokerFactory mvcEndpointInvokerFactory = null, @@ -362,18 +611,45 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test.Internal return new MvcEndpointInfo(name, template, defaults, constraints, dataTokens, constraintResolver); } + private IActionDescriptorCollectionProvider GetActionDescriptorCollection(params object[] requiredValues) + { + var actionDescriptors = new List(); + foreach (var requiredValue in requiredValues) + { + actionDescriptors.Add(CreateActionDescriptor(requiredValue)); + } + + var actionDescriptorCollectionProvider = new Mock(); + actionDescriptorCollectionProvider + .Setup(m => m.ActionDescriptors) + .Returns(new ActionDescriptorCollection(actionDescriptors, version: 0)); + return actionDescriptorCollectionProvider.Object; + } + private ActionDescriptor CreateActionDescriptor(string controller, string action, string area = null) { - return new ActionDescriptor + return CreateActionDescriptor(new { controller = controller, action = action, area = area }); + } + + private ActionDescriptor CreateActionDescriptor(object requiredValues) + { + var actionDescriptor = new ActionDescriptor(); + var routeValues = new RouteValueDictionary(requiredValues); + foreach (var kvp in routeValues) { - RouteValues = - { - ["controller"] = controller, - ["action"] = action, - ["area"] = area - }, - DisplayName = string.Empty, - }; + actionDescriptor.RouteValues[kvp.Key] = kvp.Value?.ToString(); + } + return actionDescriptor; + } + + private void AssertIsSubset(RouteValueDictionary subset, RouteValueDictionary fullSet) + { + foreach (var subsetPair in subset) + { + var isPresent = fullSet.TryGetValue(subsetPair.Key, out var fullSetPairValue); + Assert.True(isPresent); + Assert.Equal(subsetPair.Value, fullSetPairValue); + } } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/DispatcherUrlHelperTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/DispatcherUrlHelperTest.cs index 6c42555191..5d52f760a7 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/DispatcherUrlHelperTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/DispatcherUrlHelperTest.cs @@ -36,12 +36,11 @@ namespace Microsoft.AspNetCore.Mvc.Routing endpoints.Add(new MatcherEndpoint( next => httpContext => Task.CompletedTask, template, - null, + new RouteValueDictionary(), + new RouteValueDictionary(), 0, EndpointMetadataCollection.Empty, - null, - new Address(routeName) - )); + null)); return CreateUrlHelper(endpoints, appRoot, host, protocol); } @@ -53,10 +52,10 @@ namespace Microsoft.AspNetCore.Mvc.Routing Endpoint = new MatcherEndpoint( next => cntxt => Task.CompletedTask, "/", - new { }, + new RouteValueDictionary(), + new RouteValueDictionary(), 0, EndpointMetadataCollection.Empty, - null, null) }); @@ -79,7 +78,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing string template, object defaults) { - var endpoint = GetEndpoint(routeName, template, defaults); + var endpoint = GetEndpoint(routeName, template, new RouteValueDictionary(defaults)); var services = CreateServices(new[] { endpoint }); var httpContext = CreateHttpContext(services, appRoot: "", host: null, protocol: null); var actionContext = CreateActionContext(httpContext); @@ -101,25 +100,36 @@ namespace Microsoft.AspNetCore.Mvc.Routing private List GetDefaultEndpoints() { var endpoints = new List(); - endpoints.Add(new MatcherEndpoint( - next => (httpContext) => Task.CompletedTask, - "{controller}/{action}/{id}", - new { id = "defaultid" }, - 0, - EndpointMetadataCollection.Empty, - "RouteWithNoName", - address: null)); - endpoints.Add(new MatcherEndpoint( - next => (httpContext) => Task.CompletedTask, - "named/{controller}/{action}/{id}", - new { id = "defaultid" }, - 0, - EndpointMetadataCollection.Empty, - "RouteWithNoName", - new Address("namedroute"))); + endpoints.Add(CreateEndpoint(null, "home/newaction/{id?}", new { id = "defaultid", controller = "home", action = "newaction" }, 1)); + endpoints.Add(CreateEndpoint(null, "home/contact/{id?}", new { id = "defaultid", controller = "home", action = "contact" }, 2)); + endpoints.Add(CreateEndpoint(null, "home2/newaction/{id?}", new { id = "defaultid", controller = "home2", action = "newaction" }, 3)); + endpoints.Add(CreateEndpoint(null, "home2/contact/{id?}", new { id = "defaultid", controller = "home2", action = "contact" }, 4)); + endpoints.Add(CreateEndpoint(null, "home3/contact/{id?}", new { id = "defaultid", controller = "home3", action = "contact" }, 5)); + endpoints.Add(CreateEndpoint("namedroute", "named/home/newaction/{id?}", new { id = "defaultid", controller = "home", action = "newaction" }, 6)); + endpoints.Add(CreateEndpoint("namedroute", "named/home2/newaction/{id?}", new { id = "defaultid", controller = "home2", action = "newaction" }, 7)); + endpoints.Add(CreateEndpoint("namedroute", "named/home/contact/{id?}", new { id = "defaultid", controller = "home", action = "contact" }, 8)); + endpoints.Add(CreateEndpoint("MyRouteName", "any/url", new { }, 9)); return endpoints; } + private MatcherEndpoint CreateEndpoint(string routeName, string template, object defaults, int order) + { + var metadata = EndpointMetadataCollection.Empty; + if (!string.IsNullOrEmpty(routeName)) + { + metadata = new EndpointMetadataCollection(new[] { new RouteNameMetadata(routeName) }); + } + + return new MatcherEndpoint( + next => (httpContext) => Task.CompletedTask, + template, + new RouteValueDictionary(defaults), + new RouteValueDictionary(), + order, + metadata, + "DisplayName"); + } + private IServiceProvider CreateServices(IEnumerable endpoints) { if (endpoints == null) @@ -135,16 +145,26 @@ namespace Microsoft.AspNetCore.Mvc.Routing return services.BuildServiceProvider(); } - private MatcherEndpoint GetEndpoint(string name, string template, object defaults) + private MatcherEndpoint GetEndpoint(string name, string template, RouteValueDictionary defaults) { return new MatcherEndpoint( next => c => Task.CompletedTask, template, defaults, + new RouteValueDictionary(), 0, EndpointMetadataCollection.Empty, - null, - new Address(name)); + null); + } + + private class RouteNameMetadata : IRouteNameMetadata + { + public RouteNameMetadata(string routeName) + { + Name = routeName; + } + + public string Name { get; } } } }