diff --git a/src/Mvc/Mvc.Core/src/Builder/ControllerEndpointRouteBuilderExtensions.cs b/src/Mvc/Mvc.Core/src/Builder/ControllerEndpointRouteBuilderExtensions.cs
index 6c14cca67f..d2ce072066 100644
--- a/src/Mvc/Mvc.Core/src/Builder/ControllerEndpointRouteBuilderExtensions.cs
+++ b/src/Mvc/Mvc.Core/src/Builder/ControllerEndpointRouteBuilderExtensions.cs
@@ -5,10 +5,10 @@ using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Core;
+using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Constraints;
-using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Builder
@@ -213,7 +213,9 @@ namespace Microsoft.AspNetCore.Builder
EnsureControllerServices(endpoints);
// Called for side-effect to make sure that the data source is registered.
- GetOrCreateDataSource(endpoints).CreateInertEndpoints = true;
+ var dataSource = GetOrCreateDataSource(endpoints);
+ dataSource.CreateInertEndpoints = true;
+ RegisterInCache(endpoints.ServiceProvider, dataSource);
// Maps a fallback endpoint with an empty delegate. This is OK because
// we don't expect the delegate to run.
@@ -222,6 +224,7 @@ namespace Microsoft.AspNetCore.Builder
{
// MVC registers a policy that looks for this metadata.
b.Metadata.Add(CreateDynamicControllerMetadata(action, controller, area: null));
+ b.Metadata.Add(new ControllerEndpointDataSourceIdMetadata(dataSource.DataSourceId));
});
return builder;
}
@@ -289,7 +292,9 @@ namespace Microsoft.AspNetCore.Builder
EnsureControllerServices(endpoints);
// Called for side-effect to make sure that the data source is registered.
- GetOrCreateDataSource(endpoints).CreateInertEndpoints = true;
+ var dataSource = GetOrCreateDataSource(endpoints);
+ dataSource.CreateInertEndpoints = true;
+ RegisterInCache(endpoints.ServiceProvider, dataSource);
// Maps a fallback endpoint with an empty delegate. This is OK because
// we don't expect the delegate to run.
@@ -298,6 +303,7 @@ namespace Microsoft.AspNetCore.Builder
{
// MVC registers a policy that looks for this metadata.
b.Metadata.Add(CreateDynamicControllerMetadata(action, controller, area: null));
+ b.Metadata.Add(new ControllerEndpointDataSourceIdMetadata(dataSource.DataSourceId));
});
return builder;
}
@@ -357,7 +363,9 @@ namespace Microsoft.AspNetCore.Builder
EnsureControllerServices(endpoints);
// Called for side-effect to make sure that the data source is registered.
- GetOrCreateDataSource(endpoints).CreateInertEndpoints = true;
+ var dataSource = GetOrCreateDataSource(endpoints);
+ dataSource.CreateInertEndpoints = true;
+ RegisterInCache(endpoints.ServiceProvider, dataSource);
// Maps a fallback endpoint with an empty delegate. This is OK because
// we don't expect the delegate to run.
@@ -366,6 +374,7 @@ namespace Microsoft.AspNetCore.Builder
{
// MVC registers a policy that looks for this metadata.
b.Metadata.Add(CreateDynamicControllerMetadata(action, controller, area));
+ b.Metadata.Add(new ControllerEndpointDataSourceIdMetadata(dataSource.DataSourceId));
});
return builder;
}
@@ -435,7 +444,9 @@ namespace Microsoft.AspNetCore.Builder
EnsureControllerServices(endpoints);
// Called for side-effect to make sure that the data source is registered.
- GetOrCreateDataSource(endpoints).CreateInertEndpoints = true;
+ var dataSource = GetOrCreateDataSource(endpoints);
+ dataSource.CreateInertEndpoints = true;
+ RegisterInCache(endpoints.ServiceProvider, dataSource);
// Maps a fallback endpoint with an empty delegate. This is OK because
// we don't expect the delegate to run.
@@ -444,6 +455,7 @@ namespace Microsoft.AspNetCore.Builder
{
// MVC registers a policy that looks for this metadata.
b.Metadata.Add(CreateDynamicControllerMetadata(action, controller, area));
+ b.Metadata.Add(new ControllerEndpointDataSourceIdMetadata(dataSource.DataSourceId));
});
return builder;
}
@@ -507,11 +519,50 @@ namespace Microsoft.AspNetCore.Builder
// Called for side-effect to make sure that the data source is registered.
var controllerDataSource = GetOrCreateDataSource(endpoints);
-
+ RegisterInCache(endpoints.ServiceProvider, controllerDataSource);
+
// The data source is just used to share the common order with conventionally routed actions.
controllerDataSource.AddDynamicControllerEndpoint(endpoints, pattern, typeof(TTransformer), state);
}
+ ///
+ /// Adds a specialized to the that will
+ /// attempt to select a controller action using the route values produced by .
+ ///
+ /// The to add the route to.
+ /// The URL pattern of the route.
+ /// A state object to provide to the instance.
+ /// The matching order for the dynamic route.
+ /// The type of a .
+ ///
+ ///
+ /// This method allows the registration of a and
+ /// that combine to dynamically select a controller action using custom logic.
+ ///
+ ///
+ /// The instance of will be retrieved from the dependency injection container.
+ /// Register as transient in ConfigureServices. Using the transient lifetime
+ /// is required when using .
+ ///
+ ///
+ public static void MapDynamicControllerRoute(this IEndpointRouteBuilder endpoints, string pattern, object state, int order)
+ where TTransformer : DynamicRouteValueTransformer
+ {
+ if (endpoints == null)
+ {
+ throw new ArgumentNullException(nameof(endpoints));
+ }
+
+ EnsureControllerServices(endpoints);
+
+ // Called for side-effect to make sure that the data source is registered.
+ var controllerDataSource = GetOrCreateDataSource(endpoints);
+ RegisterInCache(endpoints.ServiceProvider, controllerDataSource);
+
+ // The data source is just used to share the common order with conventionally routed actions.
+ controllerDataSource.AddDynamicControllerEndpoint(endpoints, pattern, typeof(TTransformer), state, order);
+ }
+
private static DynamicControllerMetadata CreateDynamicControllerMetadata(string action, string controller, string area)
{
return new DynamicControllerMetadata(new RouteValueDictionary()
@@ -539,11 +590,19 @@ namespace Microsoft.AspNetCore.Builder
var dataSource = endpoints.DataSources.OfType().FirstOrDefault();
if (dataSource == null)
{
- dataSource = endpoints.ServiceProvider.GetRequiredService();
+ var orderProvider = endpoints.ServiceProvider.GetRequiredService();
+ var factory = endpoints.ServiceProvider.GetRequiredService();
+ dataSource = factory.Create(orderProvider.GetOrCreateOrderedEndpointsSequenceProvider(endpoints));
endpoints.DataSources.Add(dataSource);
}
return dataSource;
}
+
+ private static void RegisterInCache(IServiceProvider serviceProvider, ControllerActionEndpointDataSource dataSource)
+ {
+ var cache = serviceProvider.GetRequiredService();
+ cache.AddDataSource(dataSource);
+ }
}
}
diff --git a/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs b/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs
index 4f5593e86a..3c9233433f 100644
--- a/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs
+++ b/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs
@@ -269,10 +269,11 @@ namespace Microsoft.Extensions.DependencyInjection
//
// Endpoint Routing / Endpoints
//
- services.TryAddSingleton();
- services.TryAddSingleton();
+ services.TryAddSingleton();
+ services.TryAddSingleton();
+ services.TryAddSingleton();
services.TryAddSingleton();
- services.TryAddSingleton();
+ services.TryAddSingleton();
services.TryAddEnumerable(ServiceDescriptor.Singleton());
//
diff --git a/src/Mvc/Mvc.Core/src/DependencyInjection/OrderedEndpointsSequenceProvider.cs b/src/Mvc/Mvc.Core/src/Infrastructure/OrderedEndpointsSequenceProvider.cs
similarity index 83%
rename from src/Mvc/Mvc.Core/src/DependencyInjection/OrderedEndpointsSequenceProvider.cs
rename to src/Mvc/Mvc.Core/src/Infrastructure/OrderedEndpointsSequenceProvider.cs
index 132f7dfb6d..701a5ff2ef 100644
--- a/src/Mvc/Mvc.Core/src/DependencyInjection/OrderedEndpointsSequenceProvider.cs
+++ b/src/Mvc/Mvc.Core/src/Infrastructure/OrderedEndpointsSequenceProvider.cs
@@ -1,26 +1,23 @@
// 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.Threading;
+
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;
+ private int _current = 0;
public int GetNext()
{
- lock (Lock)
- {
- return _current++;
- }
+ return Interlocked.Increment(ref _current);
}
}
}
diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/OrderedEndpointsSequenceProviderCache.cs b/src/Mvc/Mvc.Core/src/Infrastructure/OrderedEndpointsSequenceProviderCache.cs
new file mode 100644
index 0000000000..e3795c4bdc
--- /dev/null
+++ b/src/Mvc/Mvc.Core/src/Infrastructure/OrderedEndpointsSequenceProviderCache.cs
@@ -0,0 +1,18 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Collections.Concurrent;
+using Microsoft.AspNetCore.Routing;
+
+namespace Microsoft.AspNetCore.Mvc.Infrastructure
+{
+ internal class OrderedEndpointsSequenceProviderCache
+ {
+ private readonly ConcurrentDictionary _sequenceProviderCache = new();
+
+ public OrderedEndpointsSequenceProvider GetOrCreateOrderedEndpointsSequenceProvider(IEndpointRouteBuilder endpoints)
+ {
+ return _sequenceProviderCache.GetOrAdd(endpoints, new OrderedEndpointsSequenceProvider());
+ }
+ }
+}
diff --git a/src/Mvc/Mvc.Core/src/Routing/ControllerActionEndpointDataSource.cs b/src/Mvc/Mvc.Core/src/Routing/ControllerActionEndpointDataSource.cs
index fb9288b9cf..93a05e2455 100644
--- a/src/Mvc/Mvc.Core/src/Routing/ControllerActionEndpointDataSource.cs
+++ b/src/Mvc/Mvc.Core/src/Routing/ControllerActionEndpointDataSource.cs
@@ -19,13 +19,17 @@ namespace Microsoft.AspNetCore.Mvc.Routing
private readonly List _routes;
public ControllerActionEndpointDataSource(
+ ControllerActionEndpointDataSourceIdProvider dataSourceIdProvider,
IActionDescriptorCollectionProvider actions,
ActionEndpointFactory endpointFactory,
OrderedEndpointsSequenceProvider orderSequence)
: base(actions)
{
_endpointFactory = endpointFactory;
+
+ DataSourceId = dataSourceIdProvider.CreateId();
_orderSequence = orderSequence;
+
_routes = new List();
DefaultBuilder = new ControllerActionEndpointConventionBuilder(Lock, Conventions);
@@ -35,6 +39,8 @@ namespace Microsoft.AspNetCore.Mvc.Routing
Subscribe();
}
+ public int DataSourceId { get; }
+
public ControllerActionEndpointConventionBuilder DefaultBuilder { get; }
// Used to control whether we create 'inert' (non-routable) endpoints for use in dynamic
@@ -101,12 +107,12 @@ namespace Microsoft.AspNetCore.Mvc.Routing
return endpoints;
}
- internal void AddDynamicControllerEndpoint(IEndpointRouteBuilder endpoints, string pattern, Type transformerType, object state)
+ internal void AddDynamicControllerEndpoint(IEndpointRouteBuilder endpoints, string pattern, Type transformerType, object state, int? order = null)
{
CreateInertEndpoints = true;
lock (Lock)
{
- var order = _orderSequence.GetNext();
+ order ??= _orderSequence.GetNext();
endpoints.Map(
pattern,
@@ -116,8 +122,9 @@ namespace Microsoft.AspNetCore.Mvc.Routing
})
.Add(b =>
{
- ((RouteEndpointBuilder)b).Order = order;
+ ((RouteEndpointBuilder)b).Order = order.Value;
b.Metadata.Add(new DynamicControllerRouteValueTransformerMetadata(transformerType, state));
+ b.Metadata.Add(new ControllerEndpointDataSourceIdMetadata(DataSourceId));
});
}
}
diff --git a/src/Mvc/Mvc.Core/src/Routing/ControllerActionEndpointDataSourceFactory.cs b/src/Mvc/Mvc.Core/src/Routing/ControllerActionEndpointDataSourceFactory.cs
new file mode 100644
index 0000000000..0bb0d591a5
--- /dev/null
+++ b/src/Mvc/Mvc.Core/src/Routing/ControllerActionEndpointDataSourceFactory.cs
@@ -0,0 +1,29 @@
+// 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 Microsoft.AspNetCore.Mvc.Routing;
+
+namespace Microsoft.AspNetCore.Mvc.Infrastructure
+{
+ internal class ControllerActionEndpointDataSourceFactory
+ {
+ private readonly ControllerActionEndpointDataSourceIdProvider _dataSourceIdProvider;
+ private readonly IActionDescriptorCollectionProvider _actions;
+ private readonly ActionEndpointFactory _factory;
+
+ public ControllerActionEndpointDataSourceFactory(
+ ControllerActionEndpointDataSourceIdProvider dataSourceIdProvider,
+ IActionDescriptorCollectionProvider actions,
+ ActionEndpointFactory factory)
+ {
+ _dataSourceIdProvider = dataSourceIdProvider;
+ _actions = actions;
+ _factory = factory;
+ }
+
+ public ControllerActionEndpointDataSource Create(OrderedEndpointsSequenceProvider orderProvider)
+ {
+ return new ControllerActionEndpointDataSource(_dataSourceIdProvider, _actions, _factory, orderProvider);
+ }
+ }
+}
diff --git a/src/Mvc/Mvc.Core/src/Routing/ControllerActionEndpointDataSourceIdProvider.cs b/src/Mvc/Mvc.Core/src/Routing/ControllerActionEndpointDataSourceIdProvider.cs
new file mode 100644
index 0000000000..0c9ec72b0d
--- /dev/null
+++ b/src/Mvc/Mvc.Core/src/Routing/ControllerActionEndpointDataSourceIdProvider.cs
@@ -0,0 +1,17 @@
+// 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.Threading;
+
+namespace Microsoft.AspNetCore.Mvc.Routing
+{
+ internal class ControllerActionEndpointDataSourceIdProvider
+ {
+ private int _nextId = 1;
+
+ internal int CreateId()
+ {
+ return Interlocked.Increment(ref _nextId);
+ }
+ }
+}
diff --git a/src/Mvc/Mvc.Core/src/Routing/ControllerEndpointDataSourceIdMetadata.cs b/src/Mvc/Mvc.Core/src/Routing/ControllerEndpointDataSourceIdMetadata.cs
new file mode 100644
index 0000000000..ab83ea4155
--- /dev/null
+++ b/src/Mvc/Mvc.Core/src/Routing/ControllerEndpointDataSourceIdMetadata.cs
@@ -0,0 +1,15 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace Microsoft.AspNetCore.Mvc.Routing
+{
+ internal class ControllerEndpointDataSourceIdMetadata
+ {
+ public ControllerEndpointDataSourceIdMetadata(int id)
+ {
+ Id = id;
+ }
+
+ public int Id { get; }
+ }
+}
diff --git a/src/Mvc/Mvc.Core/src/Routing/DynamicControllerEndpointMatcherPolicy.cs b/src/Mvc/Mvc.Core/src/Routing/DynamicControllerEndpointMatcherPolicy.cs
index 5f30a03eb7..bd9425ae5c 100644
--- a/src/Mvc/Mvc.Core/src/Routing/DynamicControllerEndpointMatcherPolicy.cs
+++ b/src/Mvc/Mvc.Core/src/Routing/DynamicControllerEndpointMatcherPolicy.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
@@ -15,14 +16,14 @@ namespace Microsoft.AspNetCore.Mvc.Routing
{
internal class DynamicControllerEndpointMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy
{
- private readonly DynamicControllerEndpointSelector _selector;
+ private readonly DynamicControllerEndpointSelectorCache _selectorCache;
private readonly EndpointMetadataComparer _comparer;
- public DynamicControllerEndpointMatcherPolicy(DynamicControllerEndpointSelector selector, EndpointMetadataComparer comparer)
+ public DynamicControllerEndpointMatcherPolicy(DynamicControllerEndpointSelectorCache selectorCache, EndpointMetadataComparer comparer)
{
- if (selector == null)
+ if (selectorCache == null)
{
- throw new ArgumentNullException(nameof(selector));
+ throw new ArgumentNullException(nameof(selectorCache));
}
if (comparer == null)
@@ -30,7 +31,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
throw new ArgumentNullException(nameof(comparer));
}
- _selector = selector;
+ _selectorCache = selectorCache;
_comparer = comparer;
}
@@ -79,6 +80,9 @@ namespace Microsoft.AspNetCore.Mvc.Routing
throw new ArgumentNullException(nameof(candidates));
}
+ // The per-route selector, must be the same for all the endpoints we are dealing with.
+ DynamicControllerEndpointSelector selector = null;
+
// There's no real benefit here from trying to avoid the async state machine.
// We only execute on nodes that contain a dynamic policy, and thus always have
// to await something.
@@ -127,7 +131,9 @@ namespace Microsoft.AspNetCore.Mvc.Routing
continue;
}
- var endpoints = _selector.SelectEndpoints(dynamicValues);
+ selector = ResolveSelector(selector, endpoint);
+
+ var endpoints = selector.SelectEndpoints(dynamicValues);
if (endpoints.Count == 0 && dynamicControllerMetadata != null)
{
// Naving no match for a fallback is a configuration error. We can't really check
@@ -172,5 +178,14 @@ namespace Microsoft.AspNetCore.Mvc.Routing
candidates.ExpandEndpoint(i, endpoints, _comparer);
}
}
+
+ private DynamicControllerEndpointSelector ResolveSelector(DynamicControllerEndpointSelector currentSelector, Endpoint endpoint)
+ {
+ var selector = _selectorCache.GetEndpointSelector(endpoint);
+
+ Debug.Assert(currentSelector == null || ReferenceEquals(currentSelector, selector));
+
+ return selector;
+ }
}
}
diff --git a/src/Mvc/Mvc.Core/src/Routing/DynamicControllerEndpointSelector.cs b/src/Mvc/Mvc.Core/src/Routing/DynamicControllerEndpointSelector.cs
index f07b22b94b..6281fc4bc2 100644
--- a/src/Mvc/Mvc.Core/src/Routing/DynamicControllerEndpointSelector.cs
+++ b/src/Mvc/Mvc.Core/src/Routing/DynamicControllerEndpointSelector.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;
@@ -11,25 +11,15 @@ namespace Microsoft.AspNetCore.Mvc.Routing
{
internal class DynamicControllerEndpointSelector : IDisposable
{
- private readonly EndpointDataSource _dataSource;
private readonly DataSourceDependentCache> _cache;
- public DynamicControllerEndpointSelector(ControllerActionEndpointDataSource dataSource)
- : this((EndpointDataSource)dataSource)
- {
- }
-
- // Exposed for tests. We need to accept a more specific type in the constructor for DI
- // to work.
- protected DynamicControllerEndpointSelector(EndpointDataSource dataSource)
+ public DynamicControllerEndpointSelector(EndpointDataSource dataSource)
{
if (dataSource == null)
{
throw new ArgumentNullException(nameof(dataSource));
}
- _dataSource = dataSource;
-
_cache = new DataSourceDependentCache>(dataSource, Initialize);
}
diff --git a/src/Mvc/Mvc.Core/src/Routing/DynamicControllerEndpointSelectorCache.cs b/src/Mvc/Mvc.Core/src/Routing/DynamicControllerEndpointSelectorCache.cs
new file mode 100644
index 0000000000..9d14984312
--- /dev/null
+++ b/src/Mvc/Mvc.Core/src/Routing/DynamicControllerEndpointSelectorCache.cs
@@ -0,0 +1,46 @@
+// 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.Concurrent;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing;
+
+namespace Microsoft.AspNetCore.Mvc.Routing
+{
+ internal class DynamicControllerEndpointSelectorCache
+ {
+ private readonly ConcurrentDictionary _dataSourceCache = new();
+ private readonly ConcurrentDictionary _endpointSelectorCache = new();
+
+ public void AddDataSource(ControllerActionEndpointDataSource dataSource)
+ {
+ _dataSourceCache.GetOrAdd(dataSource.DataSourceId, dataSource);
+ }
+
+ // For testing purposes only
+ internal void AddDataSource(EndpointDataSource dataSource, int key) =>
+ _dataSourceCache.GetOrAdd(key, dataSource);
+
+ public DynamicControllerEndpointSelector GetEndpointSelector(Endpoint endpoint)
+ {
+ if (endpoint?.Metadata == null)
+ {
+ return null;
+ }
+
+ var dataSourceId = endpoint.Metadata.GetMetadata();
+ return _endpointSelectorCache.GetOrAdd(dataSourceId.Id, key => EnsureDataSource(key));
+ }
+
+ private DynamicControllerEndpointSelector EnsureDataSource(int key)
+ {
+ if (!_dataSourceCache.TryGetValue(key, out var dataSource))
+ {
+ throw new InvalidOperationException($"Data source with key '{key}' not registered.");
+ }
+
+ return new DynamicControllerEndpointSelector(dataSource);
+ }
+ }
+}
diff --git a/src/Mvc/Mvc.Core/test/Routing/ControllerActionEndpointDataSourceTest.cs b/src/Mvc/Mvc.Core/test/Routing/ControllerActionEndpointDataSourceTest.cs
index 6001dab415..8ee740f2c1 100644
--- a/src/Mvc/Mvc.Core/test/Routing/ControllerActionEndpointDataSourceTest.cs
+++ b/src/Mvc/Mvc.Core/test/Routing/ControllerActionEndpointDataSourceTest.cs
@@ -245,8 +245,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
var dataSource = (ControllerActionEndpointDataSource)CreateDataSource(mockDescriptorProvider.Object);
dataSource.AddRoute("1", "/1/{controller}/{action}/{id?}", null, null, null);
dataSource.AddRoute("2", "/2/{controller}/{action}/{id?}", null, null, null);
-
-
+
dataSource.DefaultBuilder.Add(b =>
{
if (b.Metadata.OfType().FirstOrDefault()?.AttributeRouteInfo != null)
@@ -385,7 +384,11 @@ namespace Microsoft.AspNetCore.Mvc.Routing
private protected override ActionEndpointDataSourceBase CreateDataSource(IActionDescriptorCollectionProvider actions, ActionEndpointFactory endpointFactory)
{
- return new ControllerActionEndpointDataSource(actions, endpointFactory, new OrderedEndpointsSequenceProvider());
+ return new ControllerActionEndpointDataSource(
+ new ControllerActionEndpointDataSourceIdProvider(),
+ actions,
+ endpointFactory,
+ new OrderedEndpointsSequenceProvider());
}
protected override ActionDescriptor CreateActionDescriptor(
diff --git a/src/Mvc/Mvc.Core/test/Routing/DynamicControllerEndpointMatcherPolicyTest.cs b/src/Mvc/Mvc.Core/test/Routing/DynamicControllerEndpointMatcherPolicyTest.cs
index 03dbeab793..44a8232b07 100644
--- a/src/Mvc/Mvc.Core/test/Routing/DynamicControllerEndpointMatcherPolicyTest.cs
+++ b/src/Mvc/Mvc.Core/test/Routing/DynamicControllerEndpointMatcherPolicyTest.cs
@@ -19,6 +19,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
{
public DynamicControllerEndpointMatcherPolicyTest()
{
+ var dataSourceKey = new ControllerEndpointDataSourceIdMetadata(1);
var actions = new ActionDescriptor[]
{
new ControllerActionDescriptor()
@@ -59,12 +60,13 @@ namespace Microsoft.AspNetCore.Mvc.Routing
new EndpointMetadataCollection(new object[]
{
new DynamicControllerRouteValueTransformerMetadata(typeof(CustomTransformer), State),
+ dataSourceKey
}),
"dynamic");
DataSource = new DefaultEndpointDataSource(ControllerEndpoints);
- Selector = new TestDynamicControllerEndpointSelector(DataSource);
+ SelectorCache = new TestDynamicControllerEndpointSelectorCache(DataSource, 1);
var services = new ServiceCollection();
services.AddRouting();
@@ -88,7 +90,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
private Endpoint DynamicEndpoint { get; }
- private DynamicControllerEndpointSelector Selector { get; }
+ private DynamicControllerEndpointSelectorCache SelectorCache { get; }
private IServiceProvider Services { get; }
@@ -102,7 +104,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
public async Task ApplyAsync_NoMatch()
{
// Arrange
- var policy = new DynamicControllerEndpointMatcherPolicy(Selector, Comparer);
+ var policy = new DynamicControllerEndpointMatcherPolicy(SelectorCache, Comparer);
var endpoints = new[] { DynamicEndpoint, };
var values = new RouteValueDictionary[] { null, };
@@ -132,7 +134,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
public async Task ApplyAsync_HasMatchNoEndpointFound()
{
// Arrange
- var policy = new DynamicControllerEndpointMatcherPolicy(Selector, Comparer);
+ var policy = new DynamicControllerEndpointMatcherPolicy(SelectorCache, Comparer);
var endpoints = new[] { DynamicEndpoint, };
var values = new RouteValueDictionary[] { null, };
@@ -163,7 +165,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
public async Task ApplyAsync_HasMatchFindsEndpoint_WithoutRouteValues()
{
// Arrange
- var policy = new DynamicControllerEndpointMatcherPolicy(Selector, Comparer);
+ var policy = new DynamicControllerEndpointMatcherPolicy(SelectorCache, Comparer);
var endpoints = new[] { DynamicEndpoint, };
var values = new RouteValueDictionary[] { null, };
@@ -209,7 +211,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
public async Task ApplyAsync_ThrowsForTransformerWithInvalidLifetime()
{
// Arrange
- var policy = new DynamicControllerEndpointMatcherPolicy(Selector, Comparer);
+ var policy = new DynamicControllerEndpointMatcherPolicy(SelectorCache, Comparer);
var endpoints = new[] { DynamicEndpoint, };
var values = new RouteValueDictionary[] { new RouteValueDictionary(new { slug = "test", }), };
@@ -240,7 +242,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
public async Task ApplyAsync_HasMatchFindsEndpoint_WithRouteValues()
{
// Arrange
- var policy = new DynamicControllerEndpointMatcherPolicy(Selector, Comparer);
+ var policy = new DynamicControllerEndpointMatcherPolicy(SelectorCache, Comparer);
var endpoints = new[] { DynamicEndpoint, };
var values = new RouteValueDictionary[] { new RouteValueDictionary(new { slug = "test", }), };
@@ -297,7 +299,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
public async Task ApplyAsync_CanDiscardFoundEndpoints()
{
// Arrange
- var policy = new DynamicControllerEndpointMatcherPolicy(Selector, Comparer);
+ var policy = new DynamicControllerEndpointMatcherPolicy(SelectorCache, Comparer);
var endpoints = new[] { DynamicEndpoint, };
var values = new RouteValueDictionary[] { new RouteValueDictionary(new { slug = "test", }), };
@@ -336,7 +338,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
public async Task ApplyAsync_CanReplaceFoundEndpoints()
{
// Arrange
- var policy = new DynamicControllerEndpointMatcherPolicy(Selector, Comparer);
+ var policy = new DynamicControllerEndpointMatcherPolicy(SelectorCache, Comparer);
var endpoints = new[] { DynamicEndpoint, };
var values = new RouteValueDictionary[] { new RouteValueDictionary(new { slug = "test", }), };
@@ -398,7 +400,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
public async Task ApplyAsync_CanExpandTheListOfFoundEndpoints()
{
// Arrange
- var policy = new DynamicControllerEndpointMatcherPolicy(Selector, Comparer);
+ var policy = new DynamicControllerEndpointMatcherPolicy(SelectorCache, Comparer);
var endpoints = new[] { DynamicEndpoint, };
var values = new RouteValueDictionary[] { new RouteValueDictionary(new { slug = "test", }), };
@@ -437,11 +439,11 @@ namespace Microsoft.AspNetCore.Mvc.Routing
Assert.Same(ControllerEndpoints[2], candidates[1].Endpoint);
}
- private class TestDynamicControllerEndpointSelector : DynamicControllerEndpointSelector
+ private class TestDynamicControllerEndpointSelectorCache : DynamicControllerEndpointSelectorCache
{
- public TestDynamicControllerEndpointSelector(EndpointDataSource dataSource)
- : base(dataSource)
+ public TestDynamicControllerEndpointSelectorCache(EndpointDataSource dataSource, int key)
{
+ AddDataSource(dataSource, key);
}
}
diff --git a/src/Mvc/Mvc.RazorPages/src/Builder/RazorPagesEndpointRouteBuilderExtensions.cs b/src/Mvc/Mvc.RazorPages/src/Builder/RazorPagesEndpointRouteBuilderExtensions.cs
index bd0c27186b..f8258196b8 100644
--- a/src/Mvc/Mvc.RazorPages/src/Builder/RazorPagesEndpointRouteBuilderExtensions.cs
+++ b/src/Mvc/Mvc.RazorPages/src/Builder/RazorPagesEndpointRouteBuilderExtensions.cs
@@ -5,6 +5,7 @@ using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
+using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Routing;
@@ -75,7 +76,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 pageDataSource = GetOrCreateDataSource(endpoints);
+ pageDataSource.CreateInertEndpoints = true;
+ RegisterInCache(endpoints.ServiceProvider, pageDataSource);
// Maps a fallback endpoint with an empty delegate. This is OK because
// we don't expect the delegate to run.
@@ -84,6 +87,7 @@ namespace Microsoft.AspNetCore.Builder
{
// MVC registers a policy that looks for this metadata.
b.Metadata.Add(CreateDynamicPageMetadata(page, area: null));
+ b.Metadata.Add(new PageEndpointDataSourceIdMetadata(pageDataSource.DataSourceId));
});
return builder;
}
@@ -141,7 +145,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 pageDataSource = GetOrCreateDataSource(endpoints);
+ pageDataSource.CreateInertEndpoints = true;
+ RegisterInCache(endpoints.ServiceProvider, pageDataSource);
// Maps a fallback endpoint with an empty delegate. This is OK because
// we don't expect the delegate to run.
@@ -150,6 +156,7 @@ namespace Microsoft.AspNetCore.Builder
{
// MVC registers a policy that looks for this metadata.
b.Metadata.Add(CreateDynamicPageMetadata(page, area: null));
+ b.Metadata.Add(new PageEndpointDataSourceIdMetadata(pageDataSource.DataSourceId));
});
return builder;
}
@@ -199,7 +206,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 pageDataSource = GetOrCreateDataSource(endpoints);
+ pageDataSource.CreateInertEndpoints = true;
+ RegisterInCache(endpoints.ServiceProvider, pageDataSource);
// Maps a fallback endpoint with an empty delegate. This is OK because
// we don't expect the delegate to run.
@@ -208,6 +217,7 @@ namespace Microsoft.AspNetCore.Builder
{
// MVC registers a policy that looks for this metadata.
b.Metadata.Add(CreateDynamicPageMetadata(page, area));
+ b.Metadata.Add(new PageEndpointDataSourceIdMetadata(pageDataSource.DataSourceId));
});
return builder;
}
@@ -267,7 +277,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 pageDataSource = GetOrCreateDataSource(endpoints);
+ pageDataSource.CreateInertEndpoints = true;
+ RegisterInCache(endpoints.ServiceProvider, pageDataSource);
// Maps a fallback endpoint with an empty delegate. This is OK because
// we don't expect the delegate to run.
@@ -276,6 +288,7 @@ namespace Microsoft.AspNetCore.Builder
{
// MVC registers a policy that looks for this metadata.
b.Metadata.Add(CreateDynamicPageMetadata(page, area));
+ b.Metadata.Add(new PageEndpointDataSourceIdMetadata(pageDataSource.DataSourceId));
});
return builder;
}
@@ -337,9 +350,51 @@ namespace Microsoft.AspNetCore.Builder
EnsureRazorPagesServices(endpoints);
// Called for side-effect to make sure that the data source is registered.
- var dataSource = GetOrCreateDataSource(endpoints);
+ var pageDataSource = GetOrCreateDataSource(endpoints);
+ RegisterInCache(endpoints.ServiceProvider, pageDataSource);
- dataSource.AddDynamicPageEndpoint(endpoints, pattern, typeof(TTransformer), state);
+ pageDataSource.AddDynamicPageEndpoint(endpoints, pattern, typeof(TTransformer), state);
+ }
+
+ ///
+ /// Adds a specialized to the that will
+ /// attempt to select a page using the route values produced by .
+ ///
+ /// The to add the route to.
+ /// The URL pattern of the route.
+ /// A state object to provide to the instance.
+ /// The matching order for the dynamic route.
+ /// The type of a .
+ ///
+ ///
+ /// This method allows the registration of a and
+ /// that combine to dynamically select a page using custom logic.
+ ///
+ ///
+ /// The instance of will be retrieved from the dependency injection container.
+ /// Register with the desired service lifetime in ConfigureServices.
+ ///
+ ///
+ public static void MapDynamicPageRoute(this IEndpointRouteBuilder endpoints, string pattern, object state, int order)
+ where TTransformer : DynamicRouteValueTransformer
+ {
+ if (endpoints == null)
+ {
+ throw new ArgumentNullException(nameof(endpoints));
+ }
+
+ if (pattern == null)
+ {
+ throw new ArgumentNullException(nameof(pattern));
+ }
+
+ EnsureRazorPagesServices(endpoints);
+
+ // Called for side-effect to make sure that the data source is registered.
+ var pageDataSource = GetOrCreateDataSource(endpoints);
+ RegisterInCache(endpoints.ServiceProvider, pageDataSource);
+
+ pageDataSource.AddDynamicPageEndpoint(endpoints, pattern, typeof(TTransformer), state, order);
}
private static DynamicPageMetadata CreateDynamicPageMetadata(string page, string area)
@@ -353,7 +408,7 @@ namespace Microsoft.AspNetCore.Builder
private static void EnsureRazorPagesServices(IEndpointRouteBuilder endpoints)
{
- var marker = endpoints.ServiceProvider.GetService();
+ var marker = endpoints.ServiceProvider.GetService();
if (marker == null)
{
throw new InvalidOperationException(Mvc.Core.Resources.FormatUnableToFindServices(
@@ -368,11 +423,19 @@ namespace Microsoft.AspNetCore.Builder
var dataSource = endpoints.DataSources.OfType().FirstOrDefault();
if (dataSource == null)
{
- dataSource = endpoints.ServiceProvider.GetRequiredService();
+ var orderProviderCache = endpoints.ServiceProvider.GetRequiredService();
+ var factory = endpoints.ServiceProvider.GetRequiredService();
+ dataSource = factory.Create(orderProviderCache.GetOrCreateOrderedEndpointsSequenceProvider(endpoints));
endpoints.DataSources.Add(dataSource);
}
return dataSource;
}
+
+ private static void RegisterInCache(IServiceProvider serviceProvider, PageActionEndpointDataSource dataSource)
+ {
+ var cache = serviceProvider.GetRequiredService();
+ cache.AddDataSource(dataSource);
+ }
}
}
diff --git a/src/Mvc/Mvc.RazorPages/src/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs b/src/Mvc/Mvc.RazorPages/src/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs
index c17c80927a..81e7fbbb30 100644
--- a/src/Mvc/Mvc.RazorPages/src/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs
+++ b/src/Mvc/Mvc.RazorPages/src/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs
@@ -89,15 +89,15 @@ namespace Microsoft.Extensions.DependencyInjection
// Routing
services.TryAddEnumerable(ServiceDescriptor.Singleton());
services.TryAddEnumerable(ServiceDescriptor.Singleton());
- services.TryAddSingleton();
+ services.TryAddSingleton();
+ services.TryAddSingleton();
// Action description and invocation
services.TryAddEnumerable(
ServiceDescriptor.Singleton());
services.TryAddEnumerable(
ServiceDescriptor.Singleton());
- services.TryAddSingleton();
- services.TryAddSingleton();
+ services.TryAddSingleton();
services.TryAddEnumerable(ServiceDescriptor.Singleton());
services.TryAddEnumerable(
diff --git a/src/Mvc/Mvc.RazorPages/src/Infrastructure/DynamicPageEndpointMatcherPolicy.cs b/src/Mvc/Mvc.RazorPages/src/Infrastructure/DynamicPageEndpointMatcherPolicy.cs
index 337fe3e5f0..eb01da5205 100644
--- a/src/Mvc/Mvc.RazorPages/src/Infrastructure/DynamicPageEndpointMatcherPolicy.cs
+++ b/src/Mvc/Mvc.RazorPages/src/Infrastructure/DynamicPageEndpointMatcherPolicy.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
@@ -15,15 +16,15 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
{
internal class DynamicPageEndpointMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy
{
- private readonly DynamicPageEndpointSelector _selector;
+ private readonly DynamicPageEndpointSelectorCache _selectorCache;
private readonly PageLoader _loader;
private readonly EndpointMetadataComparer _comparer;
- public DynamicPageEndpointMatcherPolicy(DynamicPageEndpointSelector selector, PageLoader loader, EndpointMetadataComparer comparer)
+ public DynamicPageEndpointMatcherPolicy(DynamicPageEndpointSelectorCache selectorCache, PageLoader loader, EndpointMetadataComparer comparer)
{
- if (selector == null)
+ if (selectorCache == null)
{
- throw new ArgumentNullException(nameof(selector));
+ throw new ArgumentNullException(nameof(selectorCache));
}
if (loader == null)
@@ -36,7 +37,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
throw new ArgumentNullException(nameof(comparer));
}
- _selector = selector;
+ _selectorCache = selectorCache;
_loader = loader;
_comparer = comparer;
}
@@ -86,6 +87,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
throw new ArgumentNullException(nameof(candidates));
}
+ DynamicPageEndpointSelector selector = null;
+
// There's no real benefit here from trying to avoid the async state machine.
// We only execute on nodes that contain a dynamic policy, and thus always have
// to await something.
@@ -132,7 +135,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
continue;
}
- var endpoints = _selector.SelectEndpoints(dynamicValues);
+ selector = ResolveSelector(selector, endpoint);
+ var endpoints = selector.SelectEndpoints(dynamicValues);
if (endpoints.Count == 0 && dynamicPageMetadata != null)
{
// Having no match for a fallback is a configuration error. We can't really check
@@ -196,5 +200,15 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
candidates.ExpandEndpoint(i, loadedEndpoints, _comparer);
}
}
+
+ private DynamicPageEndpointSelector ResolveSelector(DynamicPageEndpointSelector currentSelector, Endpoint endpoint)
+ {
+ var selector = _selectorCache.GetEndpointSelector(endpoint);
+
+ Debug.Assert(currentSelector == null || ReferenceEquals(currentSelector, selector));
+
+ return selector;
+ }
+
}
}
diff --git a/src/Mvc/Mvc.RazorPages/src/Infrastructure/DynamicPageEndpointSelector.cs b/src/Mvc/Mvc.RazorPages/src/Infrastructure/DynamicPageEndpointSelector.cs
index b333f2947b..ac8c9af69c 100644
--- a/src/Mvc/Mvc.RazorPages/src/Infrastructure/DynamicPageEndpointSelector.cs
+++ b/src/Mvc/Mvc.RazorPages/src/Infrastructure/DynamicPageEndpointSelector.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;
@@ -14,14 +14,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
private readonly EndpointDataSource _dataSource;
private readonly DataSourceDependentCache> _cache;
- public DynamicPageEndpointSelector(PageActionEndpointDataSource dataSource)
- : this((EndpointDataSource)dataSource)
- {
- }
-
- // Exposed for tests. We need to accept a more specific type in the constructor for DI
- // to work.
- protected DynamicPageEndpointSelector(EndpointDataSource dataSource)
+ public DynamicPageEndpointSelector(EndpointDataSource dataSource)
{
if (dataSource == null)
{
diff --git a/src/Mvc/Mvc.RazorPages/src/Infrastructure/DynamicPageEndpointSelectorCache.cs b/src/Mvc/Mvc.RazorPages/src/Infrastructure/DynamicPageEndpointSelectorCache.cs
new file mode 100644
index 0000000000..5eb8c9ab1f
--- /dev/null
+++ b/src/Mvc/Mvc.RazorPages/src/Infrastructure/DynamicPageEndpointSelectorCache.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing;
+
+namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
+{
+ internal class DynamicPageEndpointSelectorCache
+ {
+ private readonly ConcurrentDictionary _dataSourceCache = new();
+ private readonly ConcurrentDictionary _endpointSelectorCache = new();
+
+ public void AddDataSource(PageActionEndpointDataSource dataSource)
+ {
+ _dataSourceCache.GetOrAdd(dataSource.DataSourceId, dataSource);
+ }
+
+ // For testing purposes only
+ internal void AddDataSource(EndpointDataSource dataSource, int key) =>
+ _dataSourceCache.GetOrAdd(key, dataSource);
+
+ public DynamicPageEndpointSelector GetEndpointSelector(Endpoint endpoint)
+ {
+ if (endpoint?.Metadata == null)
+ {
+ return null;
+ }
+
+ var dataSourceId = endpoint.Metadata.GetMetadata();
+ return _endpointSelectorCache.GetOrAdd(dataSourceId.Id, key => EnsureDataSource(key));
+ }
+
+ private DynamicPageEndpointSelector EnsureDataSource(int key)
+ {
+ if (!_dataSourceCache.TryGetValue(key, out var dataSource))
+ {
+ throw new InvalidOperationException($"Data source with key '{key}' not registered.");
+ }
+
+ return new DynamicPageEndpointSelector(dataSource);
+ }
+ }
+}
diff --git a/src/Mvc/Mvc.RazorPages/src/Infrastructure/PageActionEndpointDataSource.cs b/src/Mvc/Mvc.RazorPages/src/Infrastructure/PageActionEndpointDataSource.cs
index cefb410c73..f3ae54a251 100644
--- a/src/Mvc/Mvc.RazorPages/src/Infrastructure/PageActionEndpointDataSource.cs
+++ b/src/Mvc/Mvc.RazorPages/src/Infrastructure/PageActionEndpointDataSource.cs
@@ -18,11 +18,13 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
private readonly OrderedEndpointsSequenceProvider _orderSequence;
public PageActionEndpointDataSource(
+ PageActionEndpointDataSourceIdProvider dataSourceIdProvider,
IActionDescriptorCollectionProvider actions,
ActionEndpointFactory endpointFactory,
OrderedEndpointsSequenceProvider orderedEndpoints)
: base(actions)
{
+ DataSourceId = dataSourceIdProvider.CreateId();
_endpointFactory = endpointFactory;
_orderSequence = orderedEndpoints;
DefaultBuilder = new PageActionEndpointConventionBuilder(Lock, Conventions);
@@ -32,6 +34,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
Subscribe();
}
+ public int DataSourceId { get; }
+
public PageActionEndpointConventionBuilder DefaultBuilder { get; }
// Used to control whether we create 'inert' (non-routable) endpoints for use in dynamic
@@ -53,12 +57,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
return endpoints;
}
- internal void AddDynamicPageEndpoint(IEndpointRouteBuilder endpoints, string pattern, Type transformerType, object state)
+ internal void AddDynamicPageEndpoint(IEndpointRouteBuilder endpoints, string pattern, Type transformerType, object state, int? order = null)
{
CreateInertEndpoints = true;
lock (Lock)
{
- var order = _orderSequence.GetNext();
+ order ??= _orderSequence.GetNext();
endpoints.Map(
pattern,
@@ -68,11 +72,11 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
})
.Add(b =>
{
- ((RouteEndpointBuilder)b).Order = order;
+ ((RouteEndpointBuilder)b).Order = order.Value;
b.Metadata.Add(new DynamicPageRouteValueTransformerMetadata(transformerType, state));
+ b.Metadata.Add(new PageEndpointDataSourceIdMetadata(DataSourceId));
});
}
}
}
}
-
diff --git a/src/Mvc/Mvc.RazorPages/src/Infrastructure/PageActionEndpointDataSourceFactory.cs b/src/Mvc/Mvc.RazorPages/src/Infrastructure/PageActionEndpointDataSourceFactory.cs
new file mode 100644
index 0000000000..275961d192
--- /dev/null
+++ b/src/Mvc/Mvc.RazorPages/src/Infrastructure/PageActionEndpointDataSourceFactory.cs
@@ -0,0 +1,30 @@
+// 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 Microsoft.AspNetCore.Mvc.Infrastructure;
+using Microsoft.AspNetCore.Mvc.Routing;
+
+namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
+{
+ internal class PageActionEndpointDataSourceFactory
+ {
+ private readonly PageActionEndpointDataSourceIdProvider _dataSourceIdProvider;
+ private readonly IActionDescriptorCollectionProvider _actions;
+ private readonly ActionEndpointFactory _endpointFactory;
+
+ public PageActionEndpointDataSourceFactory(
+ PageActionEndpointDataSourceIdProvider dataSourceIdProvider,
+ IActionDescriptorCollectionProvider actions,
+ ActionEndpointFactory endpointFactory)
+ {
+ _dataSourceIdProvider = dataSourceIdProvider;
+ _actions = actions;
+ _endpointFactory = endpointFactory;
+ }
+
+ public PageActionEndpointDataSource Create(OrderedEndpointsSequenceProvider orderProvider)
+ {
+ return new PageActionEndpointDataSource(_dataSourceIdProvider, _actions, _endpointFactory, orderProvider);
+ }
+ }
+}
diff --git a/src/Mvc/Mvc.RazorPages/src/Infrastructure/PageActionEndpointDataSourceIdProvider.cs b/src/Mvc/Mvc.RazorPages/src/Infrastructure/PageActionEndpointDataSourceIdProvider.cs
new file mode 100644
index 0000000000..23aed3fae1
--- /dev/null
+++ b/src/Mvc/Mvc.RazorPages/src/Infrastructure/PageActionEndpointDataSourceIdProvider.cs
@@ -0,0 +1,17 @@
+// 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.Threading;
+
+namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
+{
+ internal class PageActionEndpointDataSourceIdProvider
+ {
+ private int _nextId = 1;
+
+ internal int CreateId()
+ {
+ return Interlocked.Increment(ref _nextId);
+ }
+ }
+}
diff --git a/src/Mvc/Mvc.RazorPages/src/Infrastructure/PageEndpointDataSourceIdMetadata.cs b/src/Mvc/Mvc.RazorPages/src/Infrastructure/PageEndpointDataSourceIdMetadata.cs
new file mode 100644
index 0000000000..11353b6932
--- /dev/null
+++ b/src/Mvc/Mvc.RazorPages/src/Infrastructure/PageEndpointDataSourceIdMetadata.cs
@@ -0,0 +1,15 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
+{
+ internal class PageEndpointDataSourceIdMetadata
+ {
+ public PageEndpointDataSourceIdMetadata(int id)
+ {
+ Id = id;
+ }
+
+ public int Id { get; }
+ }
+}
diff --git a/src/Mvc/Mvc.RazorPages/test/Infrastructure/DynamicPageEndpointMatcherPolicyTest.cs b/src/Mvc/Mvc.RazorPages/test/Infrastructure/DynamicPageEndpointMatcherPolicyTest.cs
index 7781158e04..8273635588 100644
--- a/src/Mvc/Mvc.RazorPages/test/Infrastructure/DynamicPageEndpointMatcherPolicyTest.cs
+++ b/src/Mvc/Mvc.RazorPages/test/Infrastructure/DynamicPageEndpointMatcherPolicyTest.cs
@@ -51,12 +51,13 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
new EndpointMetadataCollection(new object[]
{
new DynamicPageRouteValueTransformerMetadata(typeof(CustomTransformer), State),
+ new PageEndpointDataSourceIdMetadata(1),
}),
"dynamic");
DataSource = new DefaultEndpointDataSource(PageEndpoints);
- Selector = new TestDynamicPageEndpointSelector(DataSource);
+ SelectorCache = new TestDynamicPageEndpointSelectorCache(DataSource);
var services = new ServiceCollection();
services.AddRouting();
@@ -106,7 +107,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
private PageLoader Loader { get; }
- private DynamicPageEndpointSelector Selector { get; }
+ private DynamicPageEndpointSelectorCache SelectorCache { get; }
private object State { get; }
@@ -120,7 +121,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
public async Task ApplyAsync_NoMatch()
{
// Arrange
- var policy = new DynamicPageEndpointMatcherPolicy(Selector, Loader, Comparer);
+ var policy = new DynamicPageEndpointMatcherPolicy(SelectorCache, Loader, Comparer);
var endpoints = new[] { DynamicEndpoint, };
var values = new RouteValueDictionary[] { null, };
@@ -150,7 +151,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
public async Task ApplyAsync_HasMatchNoEndpointFound()
{
// Arrange
- var policy = new DynamicPageEndpointMatcherPolicy(Selector, Loader, Comparer);
+ var policy = new DynamicPageEndpointMatcherPolicy(SelectorCache, Loader, Comparer);
var endpoints = new[] { DynamicEndpoint, };
var values = new RouteValueDictionary[] { null, };
@@ -181,7 +182,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
public async Task ApplyAsync_HasMatchFindsEndpoint_WithoutRouteValues()
{
// Arrange
- var policy = new DynamicPageEndpointMatcherPolicy(Selector, Loader, Comparer);
+ var policy = new DynamicPageEndpointMatcherPolicy(SelectorCache, Loader, Comparer);
var endpoints = new[] { DynamicEndpoint, };
var values = new RouteValueDictionary[] { null, };
@@ -221,7 +222,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
public async Task ApplyAsync_HasMatchFindsEndpoint_WithRouteValues()
{
// Arrange
- var policy = new DynamicPageEndpointMatcherPolicy(Selector, Loader, Comparer);
+ var policy = new DynamicPageEndpointMatcherPolicy(SelectorCache, Loader, Comparer);
var endpoints = new[] { DynamicEndpoint, };
var values = new RouteValueDictionary[] { new RouteValueDictionary(new { slug = "test", }), };
@@ -272,7 +273,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
public async Task ApplyAsync_Throws_ForTransformersWithInvalidLifetime()
{
// Arrange
- var policy = new DynamicPageEndpointMatcherPolicy(Selector, Loader, Comparer);
+ var policy = new DynamicPageEndpointMatcherPolicy(SelectorCache, Loader, Comparer);
var endpoints = new[] { DynamicEndpoint, };
var values = new RouteValueDictionary[] { new RouteValueDictionary(new { slug = "test", }), };
@@ -302,7 +303,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
public async Task ApplyAsync_CanDiscardFoundEndpoints()
{
// Arrange
- var policy = new DynamicPageEndpointMatcherPolicy(Selector, Loader, Comparer);
+ var policy = new DynamicPageEndpointMatcherPolicy(SelectorCache, Loader, Comparer);
var endpoints = new[] { DynamicEndpoint, };
var values = new RouteValueDictionary[] { new RouteValueDictionary(new { slug = "test", }), };
@@ -340,7 +341,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
public async Task ApplyAsync_CanReplaceFoundEndpoints()
{
// Arrange
- var policy = new DynamicPageEndpointMatcherPolicy(Selector, Loader, Comparer);
+ var policy = new DynamicPageEndpointMatcherPolicy(SelectorCache, Loader, Comparer);
var endpoints = new[] { DynamicEndpoint, };
var values = new RouteValueDictionary[] { new RouteValueDictionary(new { slug = "test", }), };
@@ -400,7 +401,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
public async Task ApplyAsync_CanExpandTheListOfFoundEndpoints()
{
// Arrange
- var policy = new DynamicPageEndpointMatcherPolicy(Selector, Loader, Comparer);
+ var policy = new DynamicPageEndpointMatcherPolicy(SelectorCache, Loader, Comparer);
var endpoints = new[] { DynamicEndpoint, };
var values = new RouteValueDictionary[] { new RouteValueDictionary(new { slug = "test", }), };
@@ -438,11 +439,11 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
Assert.Same(LoadedEndpoints[1], candidates[1].Endpoint);
}
- private class TestDynamicPageEndpointSelector : DynamicPageEndpointSelector
+ private class TestDynamicPageEndpointSelectorCache : DynamicPageEndpointSelectorCache
{
- public TestDynamicPageEndpointSelector(EndpointDataSource dataSource)
- : base(dataSource)
+ public TestDynamicPageEndpointSelectorCache(EndpointDataSource dataSource)
{
+ AddDataSource(dataSource, 1);
}
}
diff --git a/src/Mvc/Mvc.RazorPages/test/Infrastructure/PageActionEndpointDataSourceTest.cs b/src/Mvc/Mvc.RazorPages/test/Infrastructure/PageActionEndpointDataSourceTest.cs
index 291b3a801e..2e3ef71e05 100644
--- a/src/Mvc/Mvc.RazorPages/test/Infrastructure/PageActionEndpointDataSourceTest.cs
+++ b/src/Mvc/Mvc.RazorPages/test/Infrastructure/PageActionEndpointDataSourceTest.cs
@@ -92,7 +92,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
private protected override ActionEndpointDataSourceBase CreateDataSource(IActionDescriptorCollectionProvider actions, ActionEndpointFactory endpointFactory)
{
- return new PageActionEndpointDataSource(actions, endpointFactory, new OrderedEndpointsSequenceProvider());
+ return new PageActionEndpointDataSource(new PageActionEndpointDataSourceIdProvider(), 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 8b96502295..fc06826ccc 100644
--- a/src/Mvc/benchmarks/Microsoft.AspNetCore.Mvc.Performance/ControllerActionEndpointDatasourceBenchmark.cs
+++ b/src/Mvc/benchmarks/Microsoft.AspNetCore.Mvc.Performance/ControllerActionEndpointDatasourceBenchmark.cs
@@ -109,6 +109,7 @@ namespace Microsoft.AspNetCore.Mvc.Performance
private ControllerActionEndpointDataSource CreateDataSource(IActionDescriptorCollectionProvider actionDescriptorCollectionProvider)
{
var dataSource = new ControllerActionEndpointDataSource(
+ new ControllerActionEndpointDataSourceIdProvider(),
actionDescriptorCollectionProvider,
new ActionEndpointFactory(new MockRoutePatternTransformer()),
new OrderedEndpointsSequenceProvider());
diff --git a/src/Mvc/test/Mvc.FunctionalTests/RoutingAcrossPipelineBranchesTest.cs b/src/Mvc/test/Mvc.FunctionalTests/RoutingAcrossPipelineBranchesTest.cs
new file mode 100644
index 0000000000..431622df9f
--- /dev/null
+++ b/src/Mvc/test/Mvc.FunctionalTests/RoutingAcrossPipelineBranchesTest.cs
@@ -0,0 +1,172 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Collections.Generic;
+using 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 RoutingAcrossPipelineBranchesTests : IClassFixture>
+ {
+ public RoutingAcrossPipelineBranchesTests(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 MatchesConventionalRoutesInTheirBranches()
+ {
+ var client = Factory.CreateClient();
+
+ // Arrange
+ var subdirRequest = new HttpRequestMessage(HttpMethod.Get, "subdir/literal/Branches/Index/s");
+ var commonRequest = new HttpRequestMessage(HttpMethod.Get, "common/Branches/Index/c/literal");
+ var defaultRequest = new HttpRequestMessage(HttpMethod.Get, "Branches/literal/Index/d");
+
+ // Act
+ var subdirResponse = await client.SendAsync(subdirRequest);
+ var subdirContent = await subdirResponse.Content.ReadFromJsonAsync();
+
+ var commonResponse = await client.SendAsync(commonRequest);
+ var commonContent = await commonResponse.Content.ReadFromJsonAsync();
+
+ var defaultResponse = await client.SendAsync(defaultRequest);
+ var defaultContent = await defaultResponse.Content.ReadFromJsonAsync();
+
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, subdirResponse.StatusCode);
+ Assert.True(subdirContent.RouteValues.TryGetValue("subdir", out var subdir));
+ Assert.Equal("s", subdir);
+
+ Assert.Equal(HttpStatusCode.OK, commonResponse.StatusCode);
+ Assert.True(commonContent.RouteValues.TryGetValue("common", out var common));
+ Assert.Equal("c", common);
+
+ Assert.Equal(HttpStatusCode.OK, defaultResponse.StatusCode);
+ Assert.True(defaultContent.RouteValues.TryGetValue("default", out var @default));
+ Assert.Equal("d", @default);
+ }
+
+ [Fact]
+ public async Task LinkGenerationWorksOnEachBranch()
+ {
+ var client = Factory.CreateClient();
+ var linkQuery = "?link";
+
+ // Arrange
+ var subdirRequest = new HttpRequestMessage(HttpMethod.Get, "subdir/literal/Branches/Index/s" + linkQuery);
+ var commonRequest = new HttpRequestMessage(HttpMethod.Get, "common/Branches/Index/c/literal" + linkQuery);
+ var defaultRequest = new HttpRequestMessage(HttpMethod.Get, "Branches/literal/Index/d" + linkQuery);
+
+ // Act
+ var subdirResponse = await client.SendAsync(subdirRequest);
+ var subdirContent = await subdirResponse.Content.ReadFromJsonAsync();
+
+ var commonResponse = await client.SendAsync(commonRequest);
+ var commonContent = await commonResponse.Content.ReadFromJsonAsync();
+
+ var defaultResponse = await client.SendAsync(defaultRequest);
+ var defaultContent = await defaultResponse.Content.ReadFromJsonAsync();
+
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, subdirResponse.StatusCode);
+ Assert.Equal("/subdir/literal/Branches/Index/s", subdirContent.Link);
+
+ Assert.Equal(HttpStatusCode.OK, commonResponse.StatusCode);
+ Assert.Equal("/common/Branches/Index/c/literal", commonContent.Link);
+
+ Assert.Equal(HttpStatusCode.OK, defaultResponse.StatusCode);
+ Assert.Equal("/Branches/literal/Index/d", defaultContent.Link);
+ }
+
+ // This still works because even though each middleware now gets its own data source,
+ // those data sources still get added to a global collection in IOptions>.DataSources
+ [Fact]
+ public async Task LinkGenerationStillWorksAcrossBranches()
+ {
+ var client = Factory.CreateClient();
+ var linkQuery = "?link";
+
+ // Arrange
+ var subdirRequest = new HttpRequestMessage(HttpMethod.Get, "subdir/literal/Branches/Index/s" + linkQuery + "&link_common=c&link_subdir");
+ var defaultRequest = new HttpRequestMessage(HttpMethod.Get, "Branches/literal/Index/d" + linkQuery + "&link_subdir=s");
+
+ // Act
+ var subdirResponse = await client.SendAsync(subdirRequest);
+ var subdirContent = await subdirResponse.Content.ReadFromJsonAsync();
+
+ var defaultResponse = await client.SendAsync(defaultRequest);
+ var defaultContent = await defaultResponse.Content.ReadFromJsonAsync();
+
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, subdirResponse.StatusCode);
+ // Note that this link and the one below don't account for the path base being in a different branch.
+ // The recommendation for customers doing link generation across branches will be to always generate absolute
+ // URIs by explicitly passing the path base to the link generator.
+ // In the future there are improvements we might be able to do in most common cases to lift this limitation if we receive
+ // feedback about it.
+ Assert.Equal("/subdir/Branches/Index/c/literal", subdirContent.Link);
+
+ Assert.Equal(HttpStatusCode.OK, defaultResponse.StatusCode);
+ Assert.Equal("/literal/Branches/Index/s", defaultContent.Link);
+ }
+
+ [Fact]
+ public async Task DoesNotMatchConventionalRoutesDefinedInOtherBranches()
+ {
+ var client = Factory.CreateClient();
+
+ // Arrange
+ var commonRequest = new HttpRequestMessage(HttpMethod.Get, "common/literal/Branches/Index/s");
+ var subdirRequest = new HttpRequestMessage(HttpMethod.Get, "subdir/Branches/Index/c/literal");
+ var defaultRequest = new HttpRequestMessage(HttpMethod.Get, "common/Branches/literal/Index/d");
+
+ // Act
+ var commonResponse = await client.SendAsync(commonRequest);
+
+ var subdirResponse = await client.SendAsync(subdirRequest);
+
+ var defaultResponse = await client.SendAsync(defaultRequest);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.NotFound, commonResponse.StatusCode);
+ Assert.Equal(HttpStatusCode.NotFound, subdirResponse.StatusCode);
+ Assert.Equal(HttpStatusCode.NotFound, defaultResponse.StatusCode);
+ }
+
+ [Fact]
+ public async Task ConventionalAndDynamicRouteOrdersAreScopedPerBranch()
+ {
+ var client = Factory.CreateClient();
+
+ // Arrange
+ var request = new HttpRequestMessage(HttpMethod.Get, "dynamicattributeorder/dynamic/route/rest");
+
+ // 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("action", out var action));
+
+ // The dynamic route wins because it has Order 1 (scope to that router) and
+ // has higher precedence.
+ Assert.Equal("Index", action);
+ }
+
+ private record RouteInfo(string RouteName, IDictionary RouteValues, string Link);
+ }
+}
diff --git a/src/Mvc/test/WebSites/Common/TestResponseGenerator.cs b/src/Mvc/test/WebSites/Common/TestResponseGenerator.cs
index e2492fbfec..21d97865c5 100644
--- a/src/Mvc/test/WebSites/Common/TestResponseGenerator.cs
+++ b/src/Mvc/test/WebSites/Common/TestResponseGenerator.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;
diff --git a/src/Mvc/test/WebSites/RoutingWebSite/Controllers/BranchesController.cs b/src/Mvc/test/WebSites/RoutingWebSite/Controllers/BranchesController.cs
new file mode 100644
index 0000000000..847d30dad0
--- /dev/null
+++ b/src/Mvc/test/WebSites/RoutingWebSite/Controllers/BranchesController.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Mvc.RoutingWebSite.Controllers
+{
+ public class BranchesController : Controller
+ {
+ private readonly TestResponseGenerator _generator;
+
+ public BranchesController(TestResponseGenerator generator)
+ {
+ _generator = generator;
+ }
+
+ public IActionResult Index()
+ {
+ return _generator.Generate();
+ }
+
+ [HttpGet("dynamicattributeorder/{some}/{value}/{**slug}", Order = 1)]
+ public IActionResult Attribute()
+ {
+ return _generator.Generate();
+ }
+ }
+}
diff --git a/src/Mvc/test/WebSites/RoutingWebSite/Program.cs b/src/Mvc/test/WebSites/RoutingWebSite/Program.cs
index 83b2991269..a5d1a83121 100644
--- a/src/Mvc/test/WebSites/RoutingWebSite/Program.cs
+++ b/src/Mvc/test/WebSites/RoutingWebSite/Program.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.IO;
diff --git a/src/Mvc/test/WebSites/RoutingWebSite/StartupForDynamic.cs b/src/Mvc/test/WebSites/RoutingWebSite/StartupForDynamic.cs
index 7b22a48dce..92d5522051 100644
--- a/src/Mvc/test/WebSites/RoutingWebSite/StartupForDynamic.cs
+++ b/src/Mvc/test/WebSites/RoutingWebSite/StartupForDynamic.cs
@@ -7,6 +7,7 @@ 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.DependencyInjection;
@@ -24,6 +25,8 @@ namespace RoutingWebSite
.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));
diff --git a/src/Mvc/test/WebSites/RoutingWebSite/StartupRoutingDifferentBranches.cs b/src/Mvc/test/WebSites/RoutingWebSite/StartupRoutingDifferentBranches.cs
new file mode 100644
index 0000000000..d118d7d12a
--- /dev/null
+++ b/src/Mvc/test/WebSites/RoutingWebSite/StartupRoutingDifferentBranches.cs
@@ -0,0 +1,104 @@
+// 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.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.ApplicationModels;
+using Microsoft.AspNetCore.Mvc.Infrastructure;
+using Microsoft.AspNetCore.Mvc.Routing;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace RoutingWebSite
+{
+ public class StartupRoutingDifferentBranches
+ {
+ // Set up application services
+ public void ConfigureServices(IServiceCollection services)
+ {
+ var pageRouteTransformerConvention = new PageRouteTransformerConvention(new SlugifyParameterTransformer());
+
+ services
+ .AddMvc(ConfigureMvcOptions)
+ .AddNewtonsoftJson()
+ .AddRazorPagesOptions(options =>
+ {
+ options.Conventions.AddPageRoute("/PageRouteTransformer/PageWithConfiguredRoute", "/PageRouteTransformer/NewConventionRoute/{id?}");
+ options.Conventions.AddFolderRouteModelConvention("/PageRouteTransformer", model =>
+ {
+ pageRouteTransformerConvention.Apply(model);
+ });
+ })
+ .SetCompatibilityVersion(CompatibilityVersion.Latest);
+
+ ConfigureRoutingServices(services);
+
+ services.AddScoped();
+ // This is used by test response generator
+ services.AddSingleton();
+ services.AddSingleton();
+ }
+
+ public virtual void Configure(IApplicationBuilder app)
+ {
+ app.Map("/subdir", branch =>
+ {
+ branch.UseRouting();
+
+ branch.UseEndpoints(endpoints =>
+ {
+ endpoints.MapRazorPages();
+ endpoints.MapControllerRoute(null, "literal/{controller}/{action}/{subdir}");
+ endpoints.MapDynamicControllerRoute("literal/dynamic/controller/{**slug}");
+ });
+ });
+
+ app.Map("/common", branch =>
+ {
+ branch.UseRouting();
+
+ branch.UseEndpoints(endpoints =>
+ {
+ endpoints.MapControllerRoute(null, "{controller}/{action}/{common}/literal");
+ endpoints.MapDynamicControllerRoute("dynamic/controller/literal/{**slug}");
+ });
+ });
+
+ app.UseRouting();
+ app.UseEndpoints(endpoints =>
+ {
+ endpoints.MapControllers();
+ endpoints.MapDynamicControllerRoute("dynamicattributeorder/dynamic/route/{**slug}");
+ endpoints.MapControllerRoute(null, "{controller}/literal/{action}/{default}");
+ });
+
+ app.Run(c =>
+ {
+ return c.Response.WriteAsync("Hello from middleware after routing");
+ });
+ }
+
+ protected virtual void ConfigureMvcOptions(MvcOptions options)
+ {
+ // Add route token transformer to one controller
+ options.Conventions.Add(new ControllerRouteTokenTransformerConvention(
+ typeof(ParameterTransformerController),
+ new SlugifyParameterTransformer()));
+ }
+
+ protected virtual void ConfigureRoutingServices(IServiceCollection services)
+ {
+ services.AddRouting(options => options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer));
+ }
+ }
+
+ public class BranchesTransformer : DynamicRouteValueTransformer
+ {
+ public override ValueTask TransformAsync(HttpContext httpContext, RouteValueDictionary values)
+ {
+ return new ValueTask(new RouteValueDictionary(new { controller = "Branches", action = "Index" }));
+ }
+ }
+}