[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:
parent
a17842a2e4
commit
b1e1aabc9d
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>());
|
||||
|
||||
//
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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" }));
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue