From a17842a2e4749c9d9acc0d645049847aab6c9041 Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Tue, 25 Aug 2020 08:53:42 +0200 Subject: [PATCH] [Mvc] Add support for order in dynamic controller routes (#25073) * Order defaults to 1 same as conventional routes * An incremental order is applied to dynamic routes as they are defined. --- ...ontrollerEndpointRouteBuilderExtensions.cs | 18 +- .../MvcCoreServiceCollectionExtensions.cs | 1 + .../OrderedEndpointsSequenceProvider.cs | 26 +++ .../ControllerActionEndpointDataSource.cs | 41 +++-- .../ControllerActionEndpointDataSourceTest.cs | 4 +- ...azorPagesEndpointRouteBuilderExtensions.cs | 13 +- .../PageActionEndpointDataSource.cs | 32 +++- .../PageActionEndpointDataSourceTest.cs | 4 +- ...rollerActionEndpointDatasourceBenchmark.cs | 5 +- .../RoutingDynamicOrderTest.cs | 165 ++++++++++++++++++ .../Controllers/DynamicOrderController.cs | 30 ++++ .../RoutingWebSite/Pages/DynamicPage.cshtml | 2 +- .../RoutingWebSite/StartupForDynamicOrder.cs | 132 ++++++++++++++ 13 files changed, 425 insertions(+), 48 deletions(-) create mode 100644 src/Mvc/Mvc.Core/src/DependencyInjection/OrderedEndpointsSequenceProvider.cs 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 4defbc5d2a..6c14cca67f 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 @@ namespace Microsoft.AspNetCore.Builder 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/DependencyInjection/MvcCoreServiceCollectionExtensions.cs b/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs index 82181cd034..4f5593e86a 100644 --- a/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs +++ b/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs @@ -269,6 +269,7 @@ namespace Microsoft.Extensions.DependencyInjection // // 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 0000000000..132f7dfb6d --- /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 8a27c982dc..fb9288b9cf 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; @@ -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 @@ namespace Microsoft.AspNetCore.Mvc.Routing 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); } } @@ -108,6 +100,27 @@ namespace Microsoft.AspNetCore.Mvc.Routing return endpoints; } + + internal void AddDynamicControllerEndpoint(IEndpointRouteBuilder endpoints, string pattern, Type transformerType, object state) + { + CreateInertEndpoints = true; + lock (Lock) + { + var order = _orderSequence.GetNext(); + + 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 deb97532ce..6001dab415 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 @@ namespace Microsoft.AspNetCore.Mvc.Routing 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 a1f48fc00e..bd0c27186b 100644 --- a/src/Mvc/Mvc.RazorPages/src/Builder/RazorPagesEndpointRouteBuilderExtensions.cs +++ b/src/Mvc/Mvc.RazorPages/src/Builder/RazorPagesEndpointRouteBuilderExtensions.cs @@ -337,18 +337,9 @@ namespace Microsoft.AspNetCore.Builder 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 d7501266a4..cefb410c73 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.Http; 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 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure return endpoints; } + + internal void AddDynamicPageEndpoint(IEndpointRouteBuilder endpoints, string pattern, Type transformerType, object state) + { + CreateInertEndpoints = true; + lock (Lock) + { + var order = _orderSequence.GetNext(); + + 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 DynamicPageRouteValueTransformerMetadata(transformerType, state)); + }); + } + } } } diff --git a/src/Mvc/Mvc.RazorPages/test/Infrastructure/PageActionEndpointDataSourceTest.cs b/src/Mvc/Mvc.RazorPages/test/Infrastructure/PageActionEndpointDataSourceTest.cs index a74d388c7f..291b3a801e 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 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure 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 c551bedb2e..8b96502295 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 @@ namespace Microsoft.AspNetCore.Mvc.Performance { 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 new file mode 100644 index 0000000000..ce0b22f214 --- /dev/null +++ b/src/Mvc/test/Mvc.FunctionalTests/RoutingDynamicOrderTest.cs @@ -0,0 +1,165 @@ +// 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 PrefersAttributeRoutesOverDynamicControllerRoutes() + { + 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("identifier", out var identifier)); + Assert.Equal("slug", identifier); + } + + [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("identifier", out var identifier)); + } + + [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("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/Controllers/DynamicOrderController.cs b/src/Mvc/test/WebSites/RoutingWebSite/Controllers/DynamicOrderController.cs new file mode 100644 index 0000000000..d58b9fc24c --- /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/Pages/DynamicPage.cshtml b/src/Mvc/test/WebSites/RoutingWebSite/Pages/DynamicPage.cshtml index f1d271bc62..7580432bcb 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 new file mode 100644 index 0000000000..2d3c96c579 --- /dev/null +++ b/src/Mvc/test/WebSites/RoutingWebSite/StartupForDynamicOrder.cs @@ -0,0 +1,132 @@ +// 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 const string DynamicControllerAndPages = nameof(DynamicControllerAndPages); + } + + 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 TransformerState() { Identifier = "slug" }); + endpoints.MapControllers(); + break; + case DynamicOrderScenarios.ConventionalRouteDynamicRoute: + 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 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."); + } + }); + + app.Map("/afterrouting", b => b.Run(c => + { + return c.Response.WriteAsync("Hello from middleware after routing"); + })); + } + + 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 + 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 state = (TransformerState)State; + var results = new RouteValueDictionary(); + if (!state.ForPages) + { + results["controller"] = "Home"; + results["action"] = "Index"; + } + else + { + results["Page"] = "/DynamicPage"; + } + + foreach (var kvp in kvps) + { + var split = kvp.Split("="); + if (split.Length == 2) + { + results[split[0]] = split[1]; + } + } + + results["identifier"] = ((TransformerState)State).Identifier; + + return new ValueTask(results); + } + } + } +}