[Mvc] Fix global state in controller and action endpoint data sources.

* Create data sources "per router" instance.
* Make a global shared order sequence "per router" for conventional and
  controller and page routes.
* Create DynamicControllerEndpointSelector and DynamicPageEndpointSelector
  instances per data source.
This commit is contained in:
Javier Calvarro Nelson 2020-09-08 09:17:26 -07:00 committed by Kevin Pilch
parent a17842a2e4
commit b1e1aabc9d
31 changed files with 792 additions and 100 deletions

View File

@ -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);
}
/// <summary>
/// Adds a specialized <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that will
/// attempt to select a controller action using the route values produced by <typeparamref name="TTransformer"/>.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
/// <param name="pattern">The URL pattern of the route.</param>
/// <param name="state">A state object to provide to the <typeparamref name="TTransformer" /> instance.</param>
/// <param name="order">The matching order for the dynamic route.</param>
/// <typeparam name="TTransformer">The type of a <see cref="DynamicRouteValueTransformer"/>.</typeparam>
/// <remarks>
/// <para>
/// This method allows the registration of a <see cref="RouteEndpoint"/> and <see cref="DynamicRouteValueTransformer"/>
/// that combine to dynamically select a controller action using custom logic.
/// </para>
/// <para>
/// The instance of <typeparamref name="TTransformer"/> will be retrieved from the dependency injection container.
/// Register <typeparamref name="TTransformer"/> as transient in <c>ConfigureServices</c>. Using the transient lifetime
/// is required when using <paramref name="state" />.
/// </para>
/// </remarks>
public static void MapDynamicControllerRoute<TTransformer>(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<ControllerActionEndpointDataSource>().FirstOrDefault();
if (dataSource == null)
{
dataSource = endpoints.ServiceProvider.GetRequiredService<ControllerActionEndpointDataSource>();
var orderProvider = endpoints.ServiceProvider.GetRequiredService<OrderedEndpointsSequenceProviderCache>();
var factory = endpoints.ServiceProvider.GetRequiredService<ControllerActionEndpointDataSourceFactory>();
dataSource = factory.Create(orderProvider.GetOrCreateOrderedEndpointsSequenceProvider(endpoints));
endpoints.DataSources.Add(dataSource);
}
return dataSource;
}
private static void RegisterInCache(IServiceProvider serviceProvider, ControllerActionEndpointDataSource dataSource)
{
var cache = serviceProvider.GetRequiredService<DynamicControllerEndpointSelectorCache>();
cache.AddDataSource(dataSource);
}
}
}

View File

@ -269,10 +269,11 @@ namespace Microsoft.Extensions.DependencyInjection
//
// Endpoint Routing / Endpoints
//
services.TryAddSingleton<OrderedEndpointsSequenceProvider>();
services.TryAddSingleton<ControllerActionEndpointDataSource>();
services.TryAddSingleton<ControllerActionEndpointDataSourceFactory>();
services.TryAddSingleton<OrderedEndpointsSequenceProviderCache>();
services.TryAddSingleton<ControllerActionEndpointDataSourceIdProvider>();
services.TryAddSingleton<ActionEndpointFactory>();
services.TryAddSingleton<DynamicControllerEndpointSelector>();
services.TryAddSingleton<DynamicControllerEndpointSelectorCache>();
services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, DynamicControllerEndpointMatcherPolicy>());
//

View File

@ -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);
}
}
}

View File

@ -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<IEndpointRouteBuilder, OrderedEndpointsSequenceProvider> _sequenceProviderCache = new();
public OrderedEndpointsSequenceProvider GetOrCreateOrderedEndpointsSequenceProvider(IEndpointRouteBuilder endpoints)
{
return _sequenceProviderCache.GetOrAdd(endpoints, new OrderedEndpointsSequenceProvider());
}
}
}

View File

@ -19,13 +19,17 @@ namespace Microsoft.AspNetCore.Mvc.Routing
private readonly List<ConventionalRouteEntry> _routes;
public ControllerActionEndpointDataSource(
ControllerActionEndpointDataSourceIdProvider dataSourceIdProvider,
IActionDescriptorCollectionProvider actions,
ActionEndpointFactory endpointFactory,
OrderedEndpointsSequenceProvider orderSequence)
: base(actions)
{
_endpointFactory = endpointFactory;
DataSourceId = dataSourceIdProvider.CreateId();
_orderSequence = orderSequence;
_routes = new List<ConventionalRouteEntry>();
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));
});
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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; }
}
}

View File

@ -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;
}
}
}

View File

@ -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<ActionSelectionTable<Endpoint>> _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<ActionSelectionTable<Endpoint>>(dataSource, Initialize);
}

View File

@ -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<int, EndpointDataSource> _dataSourceCache = new();
private readonly ConcurrentDictionary<int, DynamicControllerEndpointSelector> _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<ControllerEndpointDataSourceIdMetadata>();
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);
}
}
}

View File

@ -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<ActionDescriptor>().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(

View File

@ -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);
}
}

View File

@ -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);
}
/// <summary>
/// Adds a specialized <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that will
/// attempt to select a page using the route values produced by <typeparamref name="TTransformer"/>.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
/// <param name="pattern">The URL pattern of the route.</param>
/// <param name="state">A state object to provide to the <typeparamref name="TTransformer" /> instance.</param>
/// <param name="order">The matching order for the dynamic route.</param>
/// <typeparam name="TTransformer">The type of a <see cref="DynamicRouteValueTransformer"/>.</typeparam>
/// <remarks>
/// <para>
/// This method allows the registration of a <see cref="RouteEndpoint"/> and <see cref="DynamicRouteValueTransformer"/>
/// that combine to dynamically select a page using custom logic.
/// </para>
/// <para>
/// The instance of <typeparamref name="TTransformer"/> will be retrieved from the dependency injection container.
/// Register <typeparamref name="TTransformer"/> with the desired service lifetime in <c>ConfigureServices</c>.
/// </para>
/// </remarks>
public static void MapDynamicPageRoute<TTransformer>(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<PageActionEndpointDataSource>();
var marker = endpoints.ServiceProvider.GetService<PageActionEndpointDataSourceFactory>();
if (marker == null)
{
throw new InvalidOperationException(Mvc.Core.Resources.FormatUnableToFindServices(
@ -368,11 +423,19 @@ namespace Microsoft.AspNetCore.Builder
var dataSource = endpoints.DataSources.OfType<PageActionEndpointDataSource>().FirstOrDefault();
if (dataSource == null)
{
dataSource = endpoints.ServiceProvider.GetRequiredService<PageActionEndpointDataSource>();
var orderProviderCache = endpoints.ServiceProvider.GetRequiredService<OrderedEndpointsSequenceProviderCache>();
var factory = endpoints.ServiceProvider.GetRequiredService<PageActionEndpointDataSourceFactory>();
dataSource = factory.Create(orderProviderCache.GetOrCreateOrderedEndpointsSequenceProvider(endpoints));
endpoints.DataSources.Add(dataSource);
}
return dataSource;
}
private static void RegisterInCache(IServiceProvider serviceProvider, PageActionEndpointDataSource dataSource)
{
var cache = serviceProvider.GetRequiredService<DynamicPageEndpointSelectorCache>();
cache.AddDataSource(dataSource);
}
}
}

View File

@ -89,15 +89,15 @@ namespace Microsoft.Extensions.DependencyInjection
// Routing
services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, PageLoaderMatcherPolicy>());
services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, DynamicPageEndpointMatcherPolicy>());
services.TryAddSingleton<DynamicPageEndpointSelector>();
services.TryAddSingleton<DynamicPageEndpointSelectorCache>();
services.TryAddSingleton<PageActionEndpointDataSourceIdProvider>();
// Action description and invocation
services.TryAddEnumerable(
ServiceDescriptor.Singleton<IActionDescriptorProvider, PageActionDescriptorProvider>());
services.TryAddEnumerable(
ServiceDescriptor.Singleton<IPageRouteModelProvider, CompiledPageRouteModelProvider>());
services.TryAddSingleton<PageActionEndpointDataSource>();
services.TryAddSingleton<DynamicPageEndpointSelector>();
services.TryAddSingleton<PageActionEndpointDataSourceFactory>();
services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, DynamicPageEndpointMatcherPolicy>());
services.TryAddEnumerable(

View File

@ -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;
}
}
}

View File

@ -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<ActionSelectionTable<Endpoint>> _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)
{

View File

@ -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<int, EndpointDataSource> _dataSourceCache = new();
private readonly ConcurrentDictionary<int, DynamicPageEndpointSelector> _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<PageEndpointDataSourceIdMetadata>();
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);
}
}
}

View File

@ -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));
});
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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; }
}
}

View File

@ -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);
}
}

View File

@ -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(

View File

@ -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());

View File

@ -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<MvcTestFixture<RoutingWebSite.StartupRoutingDifferentBranches>>
{
public RoutingAcrossPipelineBranchesTests(MvcTestFixture<RoutingWebSite.StartupRoutingDifferentBranches> fixture)
{
Factory = fixture.Factories.FirstOrDefault() ?? fixture.WithWebHostBuilder(ConfigureWebHostBuilder);
}
private static void ConfigureWebHostBuilder(IWebHostBuilder builder) => builder.UseStartup<RoutingWebSite.StartupRoutingDifferentBranches>();
public WebApplicationFactory<StartupRoutingDifferentBranches> 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<RouteInfo>();
var commonResponse = await client.SendAsync(commonRequest);
var commonContent = await commonResponse.Content.ReadFromJsonAsync<RouteInfo>();
var defaultResponse = await client.SendAsync(defaultRequest);
var defaultContent = await defaultResponse.Content.ReadFromJsonAsync<RouteInfo>();
// 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<RouteInfo>();
var commonResponse = await client.SendAsync(commonRequest);
var commonContent = await commonResponse.Content.ReadFromJsonAsync<RouteInfo>();
var defaultResponse = await client.SendAsync(defaultRequest);
var defaultContent = await defaultResponse.Content.ReadFromJsonAsync<RouteInfo>();
// 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<RouteOptions>>.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<RouteInfo>();
var defaultResponse = await client.SendAsync(defaultRequest);
var defaultContent = await defaultResponse.Content.ReadFromJsonAsync<RouteInfo>();
// 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<RouteInfo>();
// 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<string, string> RouteValues, string Link);
}
}

View File

@ -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;

View File

@ -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();
}
}
}

View File

@ -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;

View File

@ -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<Transformer>();
services.AddScoped<TestResponseGenerator>();
services.AddSingleton<IActionContextAccessor, ActionContextAccessor>();
// Used by some controllers defined in this project.
services.Configure<RouteOptions>(options => options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer));

View File

@ -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<TestResponseGenerator>();
// This is used by test response generator
services.AddSingleton<IActionContextAccessor, ActionContextAccessor>();
services.AddSingleton<BranchesTransformer>();
}
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<BranchesTransformer>("literal/dynamic/controller/{**slug}");
});
});
app.Map("/common", branch =>
{
branch.UseRouting();
branch.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(null, "{controller}/{action}/{common}/literal");
endpoints.MapDynamicControllerRoute<BranchesTransformer>("dynamic/controller/literal/{**slug}");
});
});
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapDynamicControllerRoute<BranchesTransformer>("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<RouteValueDictionary> TransformAsync(HttpContext httpContext, RouteValueDictionary values)
{
return new ValueTask<RouteValueDictionary>(new RouteValueDictionary(new { controller = "Branches", action = "Index" }));
}
}
}