From 3eff7684a36fd2855f10748918196cc5bb12f029 Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Thu, 20 Aug 2020 08:32:34 -0700 Subject: [PATCH 1/2] [Mvc] Add support for order in dynamic route value transformers --- ...ontrollerEndpointRouteBuilderExtensions.cs | 18 +-- .../ControllerActionEndpointDataSource.cs | 20 ++- .../RoutingDynamicOrderTest.cs | 121 ++++++++++++++++++ .../Controllers/DynamicOrderController.cs | 30 +++++ .../RoutingWebSite/StartupForDynamicOrder.cs | 112 ++++++++++++++++ 5 files changed, 287 insertions(+), 14 deletions(-) create mode 100644 src/Mvc/test/Mvc.FunctionalTests/RoutingDynamicOrderTest.cs create mode 100644 src/Mvc/test/WebSites/RoutingWebSite/Controllers/DynamicOrderController.cs create mode 100644 src/Mvc/test/WebSites/RoutingWebSite/StartupForDynamicOrder.cs diff --git a/src/Mvc/Mvc.Core/src/Builder/ControllerEndpointRouteBuilderExtensions.cs b/src/Mvc/Mvc.Core/src/Builder/ControllerEndpointRouteBuilderExtensions.cs index 4defbc5d2ada..6c14cca67fc0 100644 --- a/src/Mvc/Mvc.Core/src/Builder/ControllerEndpointRouteBuilderExtensions.cs +++ b/src/Mvc/Mvc.Core/src/Builder/ControllerEndpointRouteBuilderExtensions.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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; @@ -506,18 +506,10 @@ public static void MapDynamicControllerRoute(this IEndpointRouteBu EnsureControllerServices(endpoints); // Called for side-effect to make sure that the data source is registered. - GetOrCreateDataSource(endpoints).CreateInertEndpoints = true; - - endpoints.Map( - pattern, - context => - { - throw new InvalidOperationException("This endpoint is not expected to be executed directly."); - }) - .Add(b => - { - b.Metadata.Add(new DynamicControllerRouteValueTransformerMetadata(typeof(TTransformer), state)); - }); + var controllerDataSource = GetOrCreateDataSource(endpoints); + + // The data source is just used to share the common order with conventionally routed actions. + controllerDataSource.AddDynamicControllerEndpoint(endpoints, pattern, typeof(TTransformer), state); } private static DynamicControllerMetadata CreateDynamicControllerMetadata(string action, string controller, string area) diff --git a/src/Mvc/Mvc.Core/src/Routing/ControllerActionEndpointDataSource.cs b/src/Mvc/Mvc.Core/src/Routing/ControllerActionEndpointDataSource.cs index 8a27c982dc98..f935dbd7d473 100644 --- a/src/Mvc/Mvc.Core/src/Routing/ControllerActionEndpointDataSource.cs +++ b/src/Mvc/Mvc.Core/src/Routing/ControllerActionEndpointDataSource.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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; @@ -108,6 +108,24 @@ protected override List CreateEndpoints(IReadOnlyList + { + throw new InvalidOperationException("This endpoint is not expected to be executed directly."); + }) + .Add(b => + { + ((RouteEndpointBuilder)b).Order = order; + b.Metadata.Add(new DynamicControllerRouteValueTransformerMetadata(transformerType, state)); + }); + } } } diff --git a/src/Mvc/test/Mvc.FunctionalTests/RoutingDynamicOrderTest.cs b/src/Mvc/test/Mvc.FunctionalTests/RoutingDynamicOrderTest.cs new file mode 100644 index 000000000000..d564e89309ee --- /dev/null +++ b/src/Mvc/test/Mvc.FunctionalTests/RoutingDynamicOrderTest.cs @@ -0,0 +1,121 @@ +// 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; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using RoutingWebSite; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.FunctionalTests +{ + public class RoutingDynamicOrderTest : IClassFixture> + { + public RoutingDynamicOrderTest(MvcTestFixture fixture) + { + Factory = fixture.Factories.FirstOrDefault() ?? fixture.WithWebHostBuilder(ConfigureWebHostBuilder); + } + + private static void ConfigureWebHostBuilder(IWebHostBuilder builder) => builder.UseStartup(); + + public WebApplicationFactory Factory { get; } + + [Fact] + public async Task PrefersAttributeRoutesOverDynamicRoutes() + { + var factory = Factory + .WithWebHostBuilder(b => b.UseSetting("Scenario", RoutingWebSite.StartupForDynamicOrder.DynamicOrderScenarios.AttributeRouteDynamicRoute)); + + var client = factory.CreateClient(); + + // Arrange + var url = "http://localhost/attribute-dynamic-order/Controller=Home,Action=Index"; + var request = new HttpRequestMessage(HttpMethod.Get, url); + + // Act + var response = await client.SendAsync(request); + var content = await response.Content.ReadFromJsonAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("AttributeRouteSlug", content.RouteName); + } + + [Fact] + public async Task DynamicRoutesAreMatchedInDefinitionOrderOverPrecedence() + { + AppContext.SetSwitch("Microsoft.AspNetCore.Routing.UseCorrectCatchAllBehavior", isEnabled: true); + var factory = Factory + .WithWebHostBuilder(b => b.UseSetting("Scenario", RoutingWebSite.StartupForDynamicOrder.DynamicOrderScenarios.MultipleDynamicRoute)); + + var client = factory.CreateClient(); + + // Arrange + var url = "http://localhost/dynamic-order/specific/Controller=Home,Action=Index"; + var request = new HttpRequestMessage(HttpMethod.Get, url); + + // Act + var response = await client.SendAsync(request); + var content = await response.Content.ReadFromJsonAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.True(content.RouteValues.TryGetValue("version", out var version)); + Assert.Equal("slug", version); + } + + [Fact] + public async Task ConventionalRoutesDefinedEarlierWinOverDynamicControllerRoutes() + { + AppContext.SetSwitch("Microsoft.AspNetCore.Routing.UseCorrectCatchAllBehavior", isEnabled: true); + var factory = Factory + .WithWebHostBuilder(b => b.UseSetting("Scenario", RoutingWebSite.StartupForDynamicOrder.DynamicOrderScenarios.ConventionalRouteDynamicRoute)); + + var client = factory.CreateClient(); + + // Arrange + var url = "http://localhost/conventional-dynamic-order-before"; + var request = new HttpRequestMessage(HttpMethod.Get, url); + + // Act + var response = await client.SendAsync(request); + var content = await response.Content.ReadFromJsonAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.False(content.RouteValues.TryGetValue("version", out var version)); + } + + [Fact] + public async Task ConventionalRoutesDefinedLaterLooseToDynamicControllerRoutes() + { + AppContext.SetSwitch("Microsoft.AspNetCore.Routing.UseCorrectCatchAllBehavior", isEnabled: true); + var factory = Factory + .WithWebHostBuilder(b => b.UseSetting("Scenario", RoutingWebSite.StartupForDynamicOrder.DynamicOrderScenarios.ConventionalRouteDynamicRoute)); + + var client = factory.CreateClient(); + + // Arrange + var url = "http://localhost/conventional-dynamic-order-after"; + var request = new HttpRequestMessage(HttpMethod.Get, url); + + // Act + var response = await client.SendAsync(request); + var content = await response.Content.ReadFromJsonAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.True(content.RouteValues.TryGetValue("version", out var version)); + Assert.Equal("slug", version); + } + + private record RouteInfo(string RouteName, IDictionary RouteValues); + } +} diff --git a/src/Mvc/test/WebSites/RoutingWebSite/Controllers/DynamicOrderController.cs b/src/Mvc/test/WebSites/RoutingWebSite/Controllers/DynamicOrderController.cs new file mode 100644 index 000000000000..d58b9fc24c31 --- /dev/null +++ b/src/Mvc/test/WebSites/RoutingWebSite/Controllers/DynamicOrderController.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; + +namespace Mvc.RoutingWebSite.Controllers +{ + public class DynamicOrderController : Controller + { + private readonly TestResponseGenerator _generator; + + public DynamicOrderController(TestResponseGenerator generator) + { + _generator = generator; + } + + [HttpGet("attribute-dynamic-order/{**slug}", Name = "AttributeRouteSlug")] + public IActionResult Get(string slug) + { + return _generator.Generate(Url.RouteUrl("AttributeRouteSlug", new { slug })); + } + + [HttpGet] + public IActionResult Index() + { + return _generator.Generate(Url.RouteUrl(null, new { controller = "DynamicOrder", action = "Index" })); + } + } +} diff --git a/src/Mvc/test/WebSites/RoutingWebSite/StartupForDynamicOrder.cs b/src/Mvc/test/WebSites/RoutingWebSite/StartupForDynamicOrder.cs new file mode 100644 index 000000000000..37b14a5cf563 --- /dev/null +++ b/src/Mvc/test/WebSites/RoutingWebSite/StartupForDynamicOrder.cs @@ -0,0 +1,112 @@ +// 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; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace RoutingWebSite +{ + // For by tests for dynamic routing to pages/controllers + public class StartupForDynamicOrder + { + public static class DynamicOrderScenarios + { + public const string AttributeRouteDynamicRoute = nameof(AttributeRouteDynamicRoute); + public const string MultipleDynamicRoute = nameof(MultipleDynamicRoute); + public const string ConventionalRouteDynamicRoute = nameof(ConventionalRouteDynamicRoute); + } + + public IConfiguration Configuration { get; } + + public StartupForDynamicOrder(IConfiguration configuration) + { + Configuration = configuration; + } + + public void ConfigureServices(IServiceCollection services) + { + services + .AddMvc() + .AddNewtonsoftJson() + .SetCompatibilityVersion(CompatibilityVersion.Latest); + + services.AddTransient(); + services.AddScoped(); + services.AddSingleton(); + + // Used by some controllers defined in this project. + services.Configure(options => options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer)); + } + + public void Configure(IApplicationBuilder app) + { + var scenario = Configuration.GetValue("Scenario"); + app.UseRouting(); + app.UseEndpoints(endpoints => + { + // Route order definition is important for all these routes: + switch (scenario) + { + case DynamicOrderScenarios.AttributeRouteDynamicRoute: + endpoints.MapDynamicControllerRoute("attribute-dynamic-order/{**slug}", new DynamicVersion() { Version = "slug" }); + endpoints.MapControllers(); + break; + case DynamicOrderScenarios.ConventionalRouteDynamicRoute: + endpoints.MapControllerRoute(null, "conventional-dynamic-order-before", new { controller = "DynamicOrder", action = "Index" }); + endpoints.MapDynamicControllerRoute("{conventional-dynamic-order}", new DynamicVersion() { Version = "slug" }); + endpoints.MapControllerRoute(null, "conventional-dynamic-order-after", new { controller = "DynamicOrder", action = "Index" }); + break; + case DynamicOrderScenarios.MultipleDynamicRoute: + endpoints.MapDynamicControllerRoute("dynamic-order/{**slug}", new DynamicVersion() { Version = "slug" }); + endpoints.MapDynamicControllerRoute("dynamic-order/specific/{**slug}", new DynamicVersion() { Version = "specific" }); + break; + default: + throw new InvalidOperationException("Invalid scenario configuration."); + } + }); + + app.Map("/afterrouting", b => b.Run(c => + { + return c.Response.WriteAsync("Hello from middleware after routing"); + })); + } + + private class Transformer : DynamicRouteValueTransformer + { + // Turns a format like `controller=Home,action=Index` into an RVD + public override ValueTask TransformAsync(HttpContext httpContext, RouteValueDictionary values) + { + var kvps = ((string)values?["slug"])?.Split("/")?.LastOrDefault()?.Split(",") ?? Array.Empty(); + + // Go to index by default if the route doesn't follow the slug pattern, we want to make sure always match to + // test the order is applied + var results = new RouteValueDictionary(); + results["controller"] = "Home"; + results["action"] = "Index"; + + foreach (var kvp in kvps) + { + var split = kvp.Split("="); + if (split.Length == 2) + { + results[split[0]] = split[1]; + } + } + + results["version"] = ((DynamicVersion)State).Version; + + return new ValueTask(results); + } + } + } +} From ea2f8adbb5855c765a3a31021bb848fe33c07b01 Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Thu, 20 Aug 2020 11:54:30 -0700 Subject: [PATCH 2/2] Implement order for dynamic pages too --- .../MvcCoreServiceCollectionExtensions.cs | 1 + .../OrderedEndpointsSequenceProvider.cs | 26 +++++++++ .../ControllerActionEndpointDataSource.cs | 45 +++++++-------- .../ControllerActionEndpointDataSourceTest.cs | 4 +- ...azorPagesEndpointRouteBuilderExtensions.cs | 13 +---- .../PageActionEndpointDataSource.cs | 32 ++++++++++- .../PageActionEndpointDataSourceTest.cs | 4 +- ...rollerActionEndpointDatasourceBenchmark.cs | 5 +- .../RoutingDynamicOrderTest.cs | 56 +++++++++++++++++-- .../RoutingWebSite/Pages/DynamicPage.cshtml | 2 +- .../RoutingWebSite/StartupForDynamicOrder.cs | 36 +++++++++--- 11 files changed, 164 insertions(+), 60 deletions(-) create mode 100644 src/Mvc/Mvc.Core/src/DependencyInjection/OrderedEndpointsSequenceProvider.cs diff --git a/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs b/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs index 82181cd034d1..4f5593e86acd 100644 --- a/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs +++ b/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs @@ -269,6 +269,7 @@ internal static void AddMvcCoreServices(IServiceCollection services) // // Endpoint Routing / Endpoints // + services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); diff --git a/src/Mvc/Mvc.Core/src/DependencyInjection/OrderedEndpointsSequenceProvider.cs b/src/Mvc/Mvc.Core/src/DependencyInjection/OrderedEndpointsSequenceProvider.cs new file mode 100644 index 000000000000..132f7dfb6d80 --- /dev/null +++ b/src/Mvc/Mvc.Core/src/DependencyInjection/OrderedEndpointsSequenceProvider.cs @@ -0,0 +1,26 @@ +// 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. + +namespace Microsoft.AspNetCore.Mvc.Infrastructure +{ + internal class OrderedEndpointsSequenceProvider + { + private object Lock = new object(); + + // In traditional conventional routing setup, the routes defined by a user have a 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 Router based code to Endpoint Routing world. + private int _current = 1; + + public int GetNext() + { + lock (Lock) + { + return _current++; + } + } + } +} diff --git a/src/Mvc/Mvc.Core/src/Routing/ControllerActionEndpointDataSource.cs b/src/Mvc/Mvc.Core/src/Routing/ControllerActionEndpointDataSource.cs index f935dbd7d473..fb9288b9cf84 100644 --- a/src/Mvc/Mvc.Core/src/Routing/ControllerActionEndpointDataSource.cs +++ b/src/Mvc/Mvc.Core/src/Routing/ControllerActionEndpointDataSource.cs @@ -15,27 +15,19 @@ namespace Microsoft.AspNetCore.Mvc.Routing internal class ControllerActionEndpointDataSource : ActionEndpointDataSourceBase { private readonly ActionEndpointFactory _endpointFactory; + private readonly OrderedEndpointsSequenceProvider _orderSequence; private readonly List _routes; - private int _order; - public ControllerActionEndpointDataSource( IActionDescriptorCollectionProvider actions, - ActionEndpointFactory endpointFactory) + ActionEndpointFactory endpointFactory, + OrderedEndpointsSequenceProvider orderSequence) : base(actions) { _endpointFactory = endpointFactory; - + _orderSequence = orderSequence; _routes = new List(); - // In traditional conventional routing setup, the routes defined by a user have a 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 Router based code to Endpoint Routing world. - _order = 1; - DefaultBuilder = new ControllerActionEndpointConventionBuilder(Lock, Conventions); // IMPORTANT: this needs to be the last thing we do in the constructor. @@ -59,7 +51,7 @@ public ControllerActionEndpointConventionBuilder AddRoute( lock (Lock) { var conventions = new List>(); - _routes.Add(new ConventionalRouteEntry(routeName, pattern, defaults, constraints, dataTokens, _order++, conventions)); + _routes.Add(new ConventionalRouteEntry(routeName, pattern, defaults, constraints, dataTokens, _orderSequence.GetNext(), conventions)); return new ControllerActionEndpointConventionBuilder(Lock, conventions); } } @@ -112,19 +104,22 @@ protected override List CreateEndpoints(IReadOnlyList - { - throw new InvalidOperationException("This endpoint is not expected to be executed directly."); - }) - .Add(b => - { - ((RouteEndpointBuilder)b).Order = order; - b.Metadata.Add(new DynamicControllerRouteValueTransformerMetadata(transformerType, state)); - }); + endpoints.Map( + pattern, + context => + { + throw new InvalidOperationException("This endpoint is not expected to be executed directly."); + }) + .Add(b => + { + ((RouteEndpointBuilder)b).Order = order; + b.Metadata.Add(new DynamicControllerRouteValueTransformerMetadata(transformerType, state)); + }); + } } } } diff --git a/src/Mvc/Mvc.Core/test/Routing/ControllerActionEndpointDataSourceTest.cs b/src/Mvc/Mvc.Core/test/Routing/ControllerActionEndpointDataSourceTest.cs index deb97532ce6b..6001dab41545 100644 --- a/src/Mvc/Mvc.Core/test/Routing/ControllerActionEndpointDataSourceTest.cs +++ b/src/Mvc/Mvc.Core/test/Routing/ControllerActionEndpointDataSourceTest.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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; @@ -385,7 +385,7 @@ private static bool SupportsLinkGeneration(RouteEndpoint endpoint) private protected override ActionEndpointDataSourceBase CreateDataSource(IActionDescriptorCollectionProvider actions, ActionEndpointFactory endpointFactory) { - return new ControllerActionEndpointDataSource(actions, endpointFactory); + return new ControllerActionEndpointDataSource(actions, endpointFactory, new OrderedEndpointsSequenceProvider()); } protected override ActionDescriptor CreateActionDescriptor( diff --git a/src/Mvc/Mvc.RazorPages/src/Builder/RazorPagesEndpointRouteBuilderExtensions.cs b/src/Mvc/Mvc.RazorPages/src/Builder/RazorPagesEndpointRouteBuilderExtensions.cs index a1f48fc00ee2..bd0c27186b46 100644 --- a/src/Mvc/Mvc.RazorPages/src/Builder/RazorPagesEndpointRouteBuilderExtensions.cs +++ b/src/Mvc/Mvc.RazorPages/src/Builder/RazorPagesEndpointRouteBuilderExtensions.cs @@ -337,18 +337,9 @@ public static void MapDynamicPageRoute(this IEndpointRouteBuilder EnsureRazorPagesServices(endpoints); // Called for side-effect to make sure that the data source is registered. - GetOrCreateDataSource(endpoints).CreateInertEndpoints = true; + var dataSource = GetOrCreateDataSource(endpoints); - endpoints.Map( - pattern, - context => - { - throw new InvalidOperationException("This endpoint is not expected to be executed directly."); - }) - .Add(b => - { - b.Metadata.Add(new DynamicPageRouteValueTransformerMetadata(typeof(TTransformer), state)); - }); + dataSource.AddDynamicPageEndpoint(endpoints, pattern, typeof(TTransformer), state); } private static DynamicPageMetadata CreateDynamicPageMetadata(string page, string area) diff --git a/src/Mvc/Mvc.RazorPages/src/Infrastructure/PageActionEndpointDataSource.cs b/src/Mvc/Mvc.RazorPages/src/Infrastructure/PageActionEndpointDataSource.cs index d7501266a43a..cefb410c73ce 100644 --- a/src/Mvc/Mvc.RazorPages/src/Infrastructure/PageActionEndpointDataSource.cs +++ b/src/Mvc/Mvc.RazorPages/src/Infrastructure/PageActionEndpointDataSource.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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; @@ -8,18 +8,23 @@ using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.AspNetCore.Routing; namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure { internal class PageActionEndpointDataSource : ActionEndpointDataSourceBase { private readonly ActionEndpointFactory _endpointFactory; + private readonly OrderedEndpointsSequenceProvider _orderSequence; - public PageActionEndpointDataSource(IActionDescriptorCollectionProvider actions, ActionEndpointFactory endpointFactory) + public PageActionEndpointDataSource( + IActionDescriptorCollectionProvider actions, + ActionEndpointFactory endpointFactory, + OrderedEndpointsSequenceProvider orderedEndpoints) : base(actions) { _endpointFactory = endpointFactory; - + _orderSequence = orderedEndpoints; DefaultBuilder = new PageActionEndpointConventionBuilder(Lock, Conventions); // IMPORTANT: this needs to be the last thing we do in the constructor. @@ -47,6 +52,27 @@ protected override List CreateEndpoints(IReadOnlyList + { + throw new InvalidOperationException("This endpoint is not expected to be executed directly."); + }) + .Add(b => + { + ((RouteEndpointBuilder)b).Order = order; + b.Metadata.Add(new DynamicPageRouteValueTransformerMetadata(transformerType, state)); + }); + } + } } } diff --git a/src/Mvc/Mvc.RazorPages/test/Infrastructure/PageActionEndpointDataSourceTest.cs b/src/Mvc/Mvc.RazorPages/test/Infrastructure/PageActionEndpointDataSourceTest.cs index a74d388c7fc9..291b3a801e00 100644 --- a/src/Mvc/Mvc.RazorPages/test/Infrastructure/PageActionEndpointDataSourceTest.cs +++ b/src/Mvc/Mvc.RazorPages/test/Infrastructure/PageActionEndpointDataSourceTest.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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; @@ -92,7 +92,7 @@ public void Endpoints_AppliesConventions() private protected override ActionEndpointDataSourceBase CreateDataSource(IActionDescriptorCollectionProvider actions, ActionEndpointFactory endpointFactory) { - return new PageActionEndpointDataSource(actions, endpointFactory); + return new PageActionEndpointDataSource(actions, endpointFactory, new OrderedEndpointsSequenceProvider()); } protected override ActionDescriptor CreateActionDescriptor( diff --git a/src/Mvc/benchmarks/Microsoft.AspNetCore.Mvc.Performance/ControllerActionEndpointDatasourceBenchmark.cs b/src/Mvc/benchmarks/Microsoft.AspNetCore.Mvc.Performance/ControllerActionEndpointDatasourceBenchmark.cs index c551bedb2ecc..8b965022950b 100644 --- a/src/Mvc/benchmarks/Microsoft.AspNetCore.Mvc.Performance/ControllerActionEndpointDatasourceBenchmark.cs +++ b/src/Mvc/benchmarks/Microsoft.AspNetCore.Mvc.Performance/ControllerActionEndpointDatasourceBenchmark.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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; @@ -110,7 +110,8 @@ private ControllerActionEndpointDataSource CreateDataSource(IActionDescriptorCol { var dataSource = new ControllerActionEndpointDataSource( actionDescriptorCollectionProvider, - new ActionEndpointFactory(new MockRoutePatternTransformer())); + new ActionEndpointFactory(new MockRoutePatternTransformer()), + new OrderedEndpointsSequenceProvider()); return dataSource; } diff --git a/src/Mvc/test/Mvc.FunctionalTests/RoutingDynamicOrderTest.cs b/src/Mvc/test/Mvc.FunctionalTests/RoutingDynamicOrderTest.cs index d564e89309ee..ce0b22f21442 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/RoutingDynamicOrderTest.cs +++ b/src/Mvc/test/Mvc.FunctionalTests/RoutingDynamicOrderTest.cs @@ -28,7 +28,7 @@ public RoutingDynamicOrderTest(MvcTestFixture public WebApplicationFactory Factory { get; } [Fact] - public async Task PrefersAttributeRoutesOverDynamicRoutes() + public async Task PrefersAttributeRoutesOverDynamicControllerRoutes() { var factory = Factory .WithWebHostBuilder(b => b.UseSetting("Scenario", RoutingWebSite.StartupForDynamicOrder.DynamicOrderScenarios.AttributeRouteDynamicRoute)); @@ -67,8 +67,8 @@ public async Task DynamicRoutesAreMatchedInDefinitionOrderOverPrecedence() // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.True(content.RouteValues.TryGetValue("version", out var version)); - Assert.Equal("slug", version); + Assert.True(content.RouteValues.TryGetValue("identifier", out var identifier)); + Assert.Equal("slug", identifier); } [Fact] @@ -90,7 +90,7 @@ public async Task ConventionalRoutesDefinedEarlierWinOverDynamicControllerRoutes // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.False(content.RouteValues.TryGetValue("version", out var version)); + Assert.False(content.RouteValues.TryGetValue("identifier", out var identifier)); } [Fact] @@ -112,8 +112,52 @@ public async Task ConventionalRoutesDefinedLaterLooseToDynamicControllerRoutes() // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.True(content.RouteValues.TryGetValue("version", out var version)); - Assert.Equal("slug", version); + Assert.True(content.RouteValues.TryGetValue("identifier", out var identifier)); + Assert.Equal("slug", identifier); + } + + [Fact] + public async Task DynamicPagesDefinedEarlierWinOverDynamicControllers() + { + AppContext.SetSwitch("Microsoft.AspNetCore.Routing.UseCorrectCatchAllBehavior", isEnabled: true); + var factory = Factory + .WithWebHostBuilder(b => b.UseSetting("Scenario", RoutingWebSite.StartupForDynamicOrder.DynamicOrderScenarios.DynamicControllerAndPages)); + + var client = factory.CreateClient(); + // Arrange + var url = "http://localhost/dynamic-order-page-controller-before"; + var request = new HttpRequestMessage(HttpMethod.Get, url); + + // Act + var response = await client.SendAsync(request); + var content = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("Hello from dynamic page: /DynamicPagebefore", content); + } + + [Fact] + public async Task DynamicPagesDefinedLaterLooseOverDynamicControllers() + { + AppContext.SetSwitch("Microsoft.AspNetCore.Routing.UseCorrectCatchAllBehavior", isEnabled: true); + var factory = Factory + .WithWebHostBuilder(b => b.UseSetting("Scenario", RoutingWebSite.StartupForDynamicOrder.DynamicOrderScenarios.DynamicControllerAndPages)); + + var client = factory.CreateClient(); + + // Arrange + var url = "http://localhost/dynamic-order-page-controller-after"; + var request = new HttpRequestMessage(HttpMethod.Get, url); + + // Act + var response = await client.SendAsync(request); + var content = await response.Content.ReadFromJsonAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.True(content.RouteValues.TryGetValue("identifier", out var identifier)); + Assert.Equal("controller", identifier); } private record RouteInfo(string RouteName, IDictionary RouteValues); diff --git a/src/Mvc/test/WebSites/RoutingWebSite/Pages/DynamicPage.cshtml b/src/Mvc/test/WebSites/RoutingWebSite/Pages/DynamicPage.cshtml index f1d271bc6292..7580432bcbf9 100644 --- a/src/Mvc/test/WebSites/RoutingWebSite/Pages/DynamicPage.cshtml +++ b/src/Mvc/test/WebSites/RoutingWebSite/Pages/DynamicPage.cshtml @@ -1,3 +1,3 @@ @page @model RoutingWebSite.Pages.DynamicPageModel -Hello from dynamic page: @Url.Page("") \ No newline at end of file +Hello from dynamic page: @Url.Page("")@RouteData.Values["identifier"] \ No newline at end of file diff --git a/src/Mvc/test/WebSites/RoutingWebSite/StartupForDynamicOrder.cs b/src/Mvc/test/WebSites/RoutingWebSite/StartupForDynamicOrder.cs index 37b14a5cf563..2d3c96c57986 100644 --- a/src/Mvc/test/WebSites/RoutingWebSite/StartupForDynamicOrder.cs +++ b/src/Mvc/test/WebSites/RoutingWebSite/StartupForDynamicOrder.cs @@ -24,6 +24,7 @@ public static class DynamicOrderScenarios public const string AttributeRouteDynamicRoute = nameof(AttributeRouteDynamicRoute); public const string MultipleDynamicRoute = nameof(MultipleDynamicRoute); public const string ConventionalRouteDynamicRoute = nameof(ConventionalRouteDynamicRoute); + public const string DynamicControllerAndPages = nameof(DynamicControllerAndPages); } public IConfiguration Configuration { get; } @@ -58,17 +59,22 @@ public void Configure(IApplicationBuilder app) switch (scenario) { case DynamicOrderScenarios.AttributeRouteDynamicRoute: - endpoints.MapDynamicControllerRoute("attribute-dynamic-order/{**slug}", new DynamicVersion() { Version = "slug" }); + endpoints.MapDynamicControllerRoute("attribute-dynamic-order/{**slug}", new TransformerState() { Identifier = "slug" }); endpoints.MapControllers(); break; case DynamicOrderScenarios.ConventionalRouteDynamicRoute: - endpoints.MapControllerRoute(null, "conventional-dynamic-order-before", new { controller = "DynamicOrder", action = "Index" }); - endpoints.MapDynamicControllerRoute("{conventional-dynamic-order}", new DynamicVersion() { Version = "slug" }); + endpoints.MapControllerRoute(null, "{**conventional-dynamic-order-before:regex(^((?!conventional\\-dynamic\\-order\\-after).)*$)}", new { controller = "DynamicOrder", action = "Index" }); + endpoints.MapDynamicControllerRoute("{conventional-dynamic-order}", new TransformerState() { Identifier = "slug" }); endpoints.MapControllerRoute(null, "conventional-dynamic-order-after", new { controller = "DynamicOrder", action = "Index" }); break; case DynamicOrderScenarios.MultipleDynamicRoute: - endpoints.MapDynamicControllerRoute("dynamic-order/{**slug}", new DynamicVersion() { Version = "slug" }); - endpoints.MapDynamicControllerRoute("dynamic-order/specific/{**slug}", new DynamicVersion() { Version = "specific" }); + endpoints.MapDynamicControllerRoute("dynamic-order/{**slug}", new TransformerState() { Identifier = "slug" }); + endpoints.MapDynamicControllerRoute("dynamic-order/specific/{**slug}", new TransformerState() { Identifier = "specific" }); + break; + case DynamicOrderScenarios.DynamicControllerAndPages: + endpoints.MapDynamicPageRoute("{**dynamic-order-page-controller-before:regex(^((?!dynamic\\-order\\-page\\-controller\\-after).)*$)}", new TransformerState() { Identifier = "before", ForPages = true }); + endpoints.MapDynamicControllerRoute("{dynamic-order-page-controller}", new TransformerState() { Identifier = "controller" }); + endpoints.MapDynamicPageRoute("dynamic-order-page-controller-after", new TransformerState() { Identifier = "after", ForPages = true }); break; default: throw new InvalidOperationException("Invalid scenario configuration."); @@ -81,6 +87,12 @@ public void Configure(IApplicationBuilder app) })); } + private class TransformerState + { + public string Identifier { get; set; } + public bool ForPages { get; set; } + } + private class Transformer : DynamicRouteValueTransformer { // Turns a format like `controller=Home,action=Index` into an RVD @@ -90,9 +102,17 @@ public override ValueTask TransformAsync(HttpContext httpC // Go to index by default if the route doesn't follow the slug pattern, we want to make sure always match to // test the order is applied + var state = (TransformerState)State; var results = new RouteValueDictionary(); - results["controller"] = "Home"; - results["action"] = "Index"; + if (!state.ForPages) + { + results["controller"] = "Home"; + results["action"] = "Index"; + } + else + { + results["Page"] = "/DynamicPage"; + } foreach (var kvp in kvps) { @@ -103,7 +123,7 @@ public override ValueTask TransformAsync(HttpContext httpC } } - results["version"] = ((DynamicVersion)State).Version; + results["identifier"] = ((TransformerState)State).Identifier; return new ValueTask(results); }