[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.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Mvc.Core;
|
using Microsoft.AspNetCore.Mvc.Core;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||||
using Microsoft.AspNetCore.Mvc.Routing;
|
using Microsoft.AspNetCore.Mvc.Routing;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
using Microsoft.AspNetCore.Routing.Constraints;
|
using Microsoft.AspNetCore.Routing.Constraints;
|
||||||
using Microsoft.AspNetCore.Routing.Patterns;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Builder
|
namespace Microsoft.AspNetCore.Builder
|
||||||
|
|
@ -213,7 +213,9 @@ namespace Microsoft.AspNetCore.Builder
|
||||||
EnsureControllerServices(endpoints);
|
EnsureControllerServices(endpoints);
|
||||||
|
|
||||||
// Called for side-effect to make sure that the data source is registered.
|
// 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
|
// Maps a fallback endpoint with an empty delegate. This is OK because
|
||||||
// we don't expect the delegate to run.
|
// 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.
|
// MVC registers a policy that looks for this metadata.
|
||||||
b.Metadata.Add(CreateDynamicControllerMetadata(action, controller, area: null));
|
b.Metadata.Add(CreateDynamicControllerMetadata(action, controller, area: null));
|
||||||
|
b.Metadata.Add(new ControllerEndpointDataSourceIdMetadata(dataSource.DataSourceId));
|
||||||
});
|
});
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
@ -289,7 +292,9 @@ namespace Microsoft.AspNetCore.Builder
|
||||||
EnsureControllerServices(endpoints);
|
EnsureControllerServices(endpoints);
|
||||||
|
|
||||||
// Called for side-effect to make sure that the data source is registered.
|
// 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
|
// Maps a fallback endpoint with an empty delegate. This is OK because
|
||||||
// we don't expect the delegate to run.
|
// 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.
|
// MVC registers a policy that looks for this metadata.
|
||||||
b.Metadata.Add(CreateDynamicControllerMetadata(action, controller, area: null));
|
b.Metadata.Add(CreateDynamicControllerMetadata(action, controller, area: null));
|
||||||
|
b.Metadata.Add(new ControllerEndpointDataSourceIdMetadata(dataSource.DataSourceId));
|
||||||
});
|
});
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
@ -357,7 +363,9 @@ namespace Microsoft.AspNetCore.Builder
|
||||||
EnsureControllerServices(endpoints);
|
EnsureControllerServices(endpoints);
|
||||||
|
|
||||||
// Called for side-effect to make sure that the data source is registered.
|
// 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
|
// Maps a fallback endpoint with an empty delegate. This is OK because
|
||||||
// we don't expect the delegate to run.
|
// 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.
|
// MVC registers a policy that looks for this metadata.
|
||||||
b.Metadata.Add(CreateDynamicControllerMetadata(action, controller, area));
|
b.Metadata.Add(CreateDynamicControllerMetadata(action, controller, area));
|
||||||
|
b.Metadata.Add(new ControllerEndpointDataSourceIdMetadata(dataSource.DataSourceId));
|
||||||
});
|
});
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
@ -435,7 +444,9 @@ namespace Microsoft.AspNetCore.Builder
|
||||||
EnsureControllerServices(endpoints);
|
EnsureControllerServices(endpoints);
|
||||||
|
|
||||||
// Called for side-effect to make sure that the data source is registered.
|
// 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
|
// Maps a fallback endpoint with an empty delegate. This is OK because
|
||||||
// we don't expect the delegate to run.
|
// 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.
|
// MVC registers a policy that looks for this metadata.
|
||||||
b.Metadata.Add(CreateDynamicControllerMetadata(action, controller, area));
|
b.Metadata.Add(CreateDynamicControllerMetadata(action, controller, area));
|
||||||
|
b.Metadata.Add(new ControllerEndpointDataSourceIdMetadata(dataSource.DataSourceId));
|
||||||
});
|
});
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
@ -507,11 +519,50 @@ namespace Microsoft.AspNetCore.Builder
|
||||||
|
|
||||||
// Called for side-effect to make sure that the data source is registered.
|
// Called for side-effect to make sure that the data source is registered.
|
||||||
var controllerDataSource = GetOrCreateDataSource(endpoints);
|
var controllerDataSource = GetOrCreateDataSource(endpoints);
|
||||||
|
RegisterInCache(endpoints.ServiceProvider, controllerDataSource);
|
||||||
|
|
||||||
// The data source is just used to share the common order with conventionally routed actions.
|
// The data source is just used to share the common order with conventionally routed actions.
|
||||||
controllerDataSource.AddDynamicControllerEndpoint(endpoints, pattern, typeof(TTransformer), state);
|
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)
|
private static DynamicControllerMetadata CreateDynamicControllerMetadata(string action, string controller, string area)
|
||||||
{
|
{
|
||||||
return new DynamicControllerMetadata(new RouteValueDictionary()
|
return new DynamicControllerMetadata(new RouteValueDictionary()
|
||||||
|
|
@ -539,11 +590,19 @@ namespace Microsoft.AspNetCore.Builder
|
||||||
var dataSource = endpoints.DataSources.OfType<ControllerActionEndpointDataSource>().FirstOrDefault();
|
var dataSource = endpoints.DataSources.OfType<ControllerActionEndpointDataSource>().FirstOrDefault();
|
||||||
if (dataSource == null)
|
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);
|
endpoints.DataSources.Add(dataSource);
|
||||||
}
|
}
|
||||||
|
|
||||||
return 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
|
// Endpoint Routing / Endpoints
|
||||||
//
|
//
|
||||||
services.TryAddSingleton<OrderedEndpointsSequenceProvider>();
|
services.TryAddSingleton<ControllerActionEndpointDataSourceFactory>();
|
||||||
services.TryAddSingleton<ControllerActionEndpointDataSource>();
|
services.TryAddSingleton<OrderedEndpointsSequenceProviderCache>();
|
||||||
|
services.TryAddSingleton<ControllerActionEndpointDataSourceIdProvider>();
|
||||||
services.TryAddSingleton<ActionEndpointFactory>();
|
services.TryAddSingleton<ActionEndpointFactory>();
|
||||||
services.TryAddSingleton<DynamicControllerEndpointSelector>();
|
services.TryAddSingleton<DynamicControllerEndpointSelectorCache>();
|
||||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, DynamicControllerEndpointMatcherPolicy>());
|
services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, DynamicControllerEndpointMatcherPolicy>());
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,23 @@
|
||||||
// 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.
|
// 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
|
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
{
|
{
|
||||||
internal class OrderedEndpointsSequenceProvider
|
internal class OrderedEndpointsSequenceProvider
|
||||||
{
|
{
|
||||||
private object Lock = new object();
|
|
||||||
|
|
||||||
// In traditional conventional routing setup, the routes defined by a user have a order
|
// 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
|
// defined by how they are added into the list. We would like to maintain the same order when building
|
||||||
// up the endpoints too.
|
// up the endpoints too.
|
||||||
//
|
//
|
||||||
// Start with an order of '1' for conventional routes as attribute routes have a default order of '0'.
|
// 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.
|
// 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()
|
public int GetNext()
|
||||||
{
|
{
|
||||||
lock (Lock)
|
return Interlocked.Increment(ref _current);
|
||||||
{
|
|
||||||
return _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;
|
private readonly List<ConventionalRouteEntry> _routes;
|
||||||
|
|
||||||
public ControllerActionEndpointDataSource(
|
public ControllerActionEndpointDataSource(
|
||||||
|
ControllerActionEndpointDataSourceIdProvider dataSourceIdProvider,
|
||||||
IActionDescriptorCollectionProvider actions,
|
IActionDescriptorCollectionProvider actions,
|
||||||
ActionEndpointFactory endpointFactory,
|
ActionEndpointFactory endpointFactory,
|
||||||
OrderedEndpointsSequenceProvider orderSequence)
|
OrderedEndpointsSequenceProvider orderSequence)
|
||||||
: base(actions)
|
: base(actions)
|
||||||
{
|
{
|
||||||
_endpointFactory = endpointFactory;
|
_endpointFactory = endpointFactory;
|
||||||
|
|
||||||
|
DataSourceId = dataSourceIdProvider.CreateId();
|
||||||
_orderSequence = orderSequence;
|
_orderSequence = orderSequence;
|
||||||
|
|
||||||
_routes = new List<ConventionalRouteEntry>();
|
_routes = new List<ConventionalRouteEntry>();
|
||||||
|
|
||||||
DefaultBuilder = new ControllerActionEndpointConventionBuilder(Lock, Conventions);
|
DefaultBuilder = new ControllerActionEndpointConventionBuilder(Lock, Conventions);
|
||||||
|
|
@ -35,6 +39,8 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
Subscribe();
|
Subscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int DataSourceId { get; }
|
||||||
|
|
||||||
public ControllerActionEndpointConventionBuilder DefaultBuilder { get; }
|
public ControllerActionEndpointConventionBuilder DefaultBuilder { get; }
|
||||||
|
|
||||||
// Used to control whether we create 'inert' (non-routable) endpoints for use in dynamic
|
// 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;
|
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;
|
CreateInertEndpoints = true;
|
||||||
lock (Lock)
|
lock (Lock)
|
||||||
{
|
{
|
||||||
var order = _orderSequence.GetNext();
|
order ??= _orderSequence.GetNext();
|
||||||
|
|
||||||
endpoints.Map(
|
endpoints.Map(
|
||||||
pattern,
|
pattern,
|
||||||
|
|
@ -116,8 +122,9 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
})
|
})
|
||||||
.Add(b =>
|
.Add(b =>
|
||||||
{
|
{
|
||||||
((RouteEndpointBuilder)b).Order = order;
|
((RouteEndpointBuilder)b).Order = order.Value;
|
||||||
b.Metadata.Add(new DynamicControllerRouteValueTransformerMetadata(transformerType, state));
|
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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
@ -15,14 +16,14 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
{
|
{
|
||||||
internal class DynamicControllerEndpointMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy
|
internal class DynamicControllerEndpointMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy
|
||||||
{
|
{
|
||||||
private readonly DynamicControllerEndpointSelector _selector;
|
private readonly DynamicControllerEndpointSelectorCache _selectorCache;
|
||||||
private readonly EndpointMetadataComparer _comparer;
|
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)
|
if (comparer == null)
|
||||||
|
|
@ -30,7 +31,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
throw new ArgumentNullException(nameof(comparer));
|
throw new ArgumentNullException(nameof(comparer));
|
||||||
}
|
}
|
||||||
|
|
||||||
_selector = selector;
|
_selectorCache = selectorCache;
|
||||||
_comparer = comparer;
|
_comparer = comparer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -79,6 +80,9 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
throw new ArgumentNullException(nameof(candidates));
|
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.
|
// 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
|
// We only execute on nodes that contain a dynamic policy, and thus always have
|
||||||
// to await something.
|
// to await something.
|
||||||
|
|
@ -127,7 +131,9 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var endpoints = _selector.SelectEndpoints(dynamicValues);
|
selector = ResolveSelector(selector, endpoint);
|
||||||
|
|
||||||
|
var endpoints = selector.SelectEndpoints(dynamicValues);
|
||||||
if (endpoints.Count == 0 && dynamicControllerMetadata != null)
|
if (endpoints.Count == 0 && dynamicControllerMetadata != null)
|
||||||
{
|
{
|
||||||
// Naving no match for a fallback is a configuration error. We can't really check
|
// 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);
|
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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
@ -11,25 +11,15 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
{
|
{
|
||||||
internal class DynamicControllerEndpointSelector : IDisposable
|
internal class DynamicControllerEndpointSelector : IDisposable
|
||||||
{
|
{
|
||||||
private readonly EndpointDataSource _dataSource;
|
|
||||||
private readonly DataSourceDependentCache<ActionSelectionTable<Endpoint>> _cache;
|
private readonly DataSourceDependentCache<ActionSelectionTable<Endpoint>> _cache;
|
||||||
|
|
||||||
public DynamicControllerEndpointSelector(ControllerActionEndpointDataSource dataSource)
|
public DynamicControllerEndpointSelector(EndpointDataSource 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)
|
|
||||||
{
|
{
|
||||||
if (dataSource == null)
|
if (dataSource == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(dataSource));
|
throw new ArgumentNullException(nameof(dataSource));
|
||||||
}
|
}
|
||||||
|
|
||||||
_dataSource = dataSource;
|
|
||||||
|
|
||||||
_cache = new DataSourceDependentCache<ActionSelectionTable<Endpoint>>(dataSource, Initialize);
|
_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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -246,7 +246,6 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
dataSource.AddRoute("1", "/1/{controller}/{action}/{id?}", null, null, null);
|
dataSource.AddRoute("1", "/1/{controller}/{action}/{id?}", null, null, null);
|
||||||
dataSource.AddRoute("2", "/2/{controller}/{action}/{id?}", null, null, null);
|
dataSource.AddRoute("2", "/2/{controller}/{action}/{id?}", null, null, null);
|
||||||
|
|
||||||
|
|
||||||
dataSource.DefaultBuilder.Add(b =>
|
dataSource.DefaultBuilder.Add(b =>
|
||||||
{
|
{
|
||||||
if (b.Metadata.OfType<ActionDescriptor>().FirstOrDefault()?.AttributeRouteInfo != null)
|
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)
|
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(
|
protected override ActionDescriptor CreateActionDescriptor(
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
{
|
{
|
||||||
public DynamicControllerEndpointMatcherPolicyTest()
|
public DynamicControllerEndpointMatcherPolicyTest()
|
||||||
{
|
{
|
||||||
|
var dataSourceKey = new ControllerEndpointDataSourceIdMetadata(1);
|
||||||
var actions = new ActionDescriptor[]
|
var actions = new ActionDescriptor[]
|
||||||
{
|
{
|
||||||
new ControllerActionDescriptor()
|
new ControllerActionDescriptor()
|
||||||
|
|
@ -59,12 +60,13 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
new EndpointMetadataCollection(new object[]
|
new EndpointMetadataCollection(new object[]
|
||||||
{
|
{
|
||||||
new DynamicControllerRouteValueTransformerMetadata(typeof(CustomTransformer), State),
|
new DynamicControllerRouteValueTransformerMetadata(typeof(CustomTransformer), State),
|
||||||
|
dataSourceKey
|
||||||
}),
|
}),
|
||||||
"dynamic");
|
"dynamic");
|
||||||
|
|
||||||
DataSource = new DefaultEndpointDataSource(ControllerEndpoints);
|
DataSource = new DefaultEndpointDataSource(ControllerEndpoints);
|
||||||
|
|
||||||
Selector = new TestDynamicControllerEndpointSelector(DataSource);
|
SelectorCache = new TestDynamicControllerEndpointSelectorCache(DataSource, 1);
|
||||||
|
|
||||||
var services = new ServiceCollection();
|
var services = new ServiceCollection();
|
||||||
services.AddRouting();
|
services.AddRouting();
|
||||||
|
|
@ -88,7 +90,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
|
|
||||||
private Endpoint DynamicEndpoint { get; }
|
private Endpoint DynamicEndpoint { get; }
|
||||||
|
|
||||||
private DynamicControllerEndpointSelector Selector { get; }
|
private DynamicControllerEndpointSelectorCache SelectorCache { get; }
|
||||||
|
|
||||||
private IServiceProvider Services { get; }
|
private IServiceProvider Services { get; }
|
||||||
|
|
||||||
|
|
@ -102,7 +104,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
public async Task ApplyAsync_NoMatch()
|
public async Task ApplyAsync_NoMatch()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var policy = new DynamicControllerEndpointMatcherPolicy(Selector, Comparer);
|
var policy = new DynamicControllerEndpointMatcherPolicy(SelectorCache, Comparer);
|
||||||
|
|
||||||
var endpoints = new[] { DynamicEndpoint, };
|
var endpoints = new[] { DynamicEndpoint, };
|
||||||
var values = new RouteValueDictionary[] { null, };
|
var values = new RouteValueDictionary[] { null, };
|
||||||
|
|
@ -132,7 +134,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
public async Task ApplyAsync_HasMatchNoEndpointFound()
|
public async Task ApplyAsync_HasMatchNoEndpointFound()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var policy = new DynamicControllerEndpointMatcherPolicy(Selector, Comparer);
|
var policy = new DynamicControllerEndpointMatcherPolicy(SelectorCache, Comparer);
|
||||||
|
|
||||||
var endpoints = new[] { DynamicEndpoint, };
|
var endpoints = new[] { DynamicEndpoint, };
|
||||||
var values = new RouteValueDictionary[] { null, };
|
var values = new RouteValueDictionary[] { null, };
|
||||||
|
|
@ -163,7 +165,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
public async Task ApplyAsync_HasMatchFindsEndpoint_WithoutRouteValues()
|
public async Task ApplyAsync_HasMatchFindsEndpoint_WithoutRouteValues()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var policy = new DynamicControllerEndpointMatcherPolicy(Selector, Comparer);
|
var policy = new DynamicControllerEndpointMatcherPolicy(SelectorCache, Comparer);
|
||||||
|
|
||||||
var endpoints = new[] { DynamicEndpoint, };
|
var endpoints = new[] { DynamicEndpoint, };
|
||||||
var values = new RouteValueDictionary[] { null, };
|
var values = new RouteValueDictionary[] { null, };
|
||||||
|
|
@ -209,7 +211,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
public async Task ApplyAsync_ThrowsForTransformerWithInvalidLifetime()
|
public async Task ApplyAsync_ThrowsForTransformerWithInvalidLifetime()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var policy = new DynamicControllerEndpointMatcherPolicy(Selector, Comparer);
|
var policy = new DynamicControllerEndpointMatcherPolicy(SelectorCache, Comparer);
|
||||||
|
|
||||||
var endpoints = new[] { DynamicEndpoint, };
|
var endpoints = new[] { DynamicEndpoint, };
|
||||||
var values = new RouteValueDictionary[] { new RouteValueDictionary(new { slug = "test", }), };
|
var values = new RouteValueDictionary[] { new RouteValueDictionary(new { slug = "test", }), };
|
||||||
|
|
@ -240,7 +242,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
public async Task ApplyAsync_HasMatchFindsEndpoint_WithRouteValues()
|
public async Task ApplyAsync_HasMatchFindsEndpoint_WithRouteValues()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var policy = new DynamicControllerEndpointMatcherPolicy(Selector, Comparer);
|
var policy = new DynamicControllerEndpointMatcherPolicy(SelectorCache, Comparer);
|
||||||
|
|
||||||
var endpoints = new[] { DynamicEndpoint, };
|
var endpoints = new[] { DynamicEndpoint, };
|
||||||
var values = new RouteValueDictionary[] { new RouteValueDictionary(new { slug = "test", }), };
|
var values = new RouteValueDictionary[] { new RouteValueDictionary(new { slug = "test", }), };
|
||||||
|
|
@ -297,7 +299,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
public async Task ApplyAsync_CanDiscardFoundEndpoints()
|
public async Task ApplyAsync_CanDiscardFoundEndpoints()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var policy = new DynamicControllerEndpointMatcherPolicy(Selector, Comparer);
|
var policy = new DynamicControllerEndpointMatcherPolicy(SelectorCache, Comparer);
|
||||||
|
|
||||||
var endpoints = new[] { DynamicEndpoint, };
|
var endpoints = new[] { DynamicEndpoint, };
|
||||||
var values = new RouteValueDictionary[] { new RouteValueDictionary(new { slug = "test", }), };
|
var values = new RouteValueDictionary[] { new RouteValueDictionary(new { slug = "test", }), };
|
||||||
|
|
@ -336,7 +338,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
public async Task ApplyAsync_CanReplaceFoundEndpoints()
|
public async Task ApplyAsync_CanReplaceFoundEndpoints()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var policy = new DynamicControllerEndpointMatcherPolicy(Selector, Comparer);
|
var policy = new DynamicControllerEndpointMatcherPolicy(SelectorCache, Comparer);
|
||||||
|
|
||||||
var endpoints = new[] { DynamicEndpoint, };
|
var endpoints = new[] { DynamicEndpoint, };
|
||||||
var values = new RouteValueDictionary[] { new RouteValueDictionary(new { slug = "test", }), };
|
var values = new RouteValueDictionary[] { new RouteValueDictionary(new { slug = "test", }), };
|
||||||
|
|
@ -398,7 +400,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
public async Task ApplyAsync_CanExpandTheListOfFoundEndpoints()
|
public async Task ApplyAsync_CanExpandTheListOfFoundEndpoints()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var policy = new DynamicControllerEndpointMatcherPolicy(Selector, Comparer);
|
var policy = new DynamicControllerEndpointMatcherPolicy(SelectorCache, Comparer);
|
||||||
|
|
||||||
var endpoints = new[] { DynamicEndpoint, };
|
var endpoints = new[] { DynamicEndpoint, };
|
||||||
var values = new RouteValueDictionary[] { new RouteValueDictionary(new { slug = "test", }), };
|
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);
|
Assert.Same(ControllerEndpoints[2], candidates[1].Endpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
private class TestDynamicControllerEndpointSelector : DynamicControllerEndpointSelector
|
private class TestDynamicControllerEndpointSelectorCache : DynamicControllerEndpointSelectorCache
|
||||||
{
|
{
|
||||||
public TestDynamicControllerEndpointSelector(EndpointDataSource dataSource)
|
public TestDynamicControllerEndpointSelectorCache(EndpointDataSource dataSource, int key)
|
||||||
: base(dataSource)
|
|
||||||
{
|
{
|
||||||
|
AddDataSource(dataSource, key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||||
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
|
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
|
||||||
using Microsoft.AspNetCore.Mvc.Routing;
|
using Microsoft.AspNetCore.Mvc.Routing;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
|
@ -75,7 +76,9 @@ namespace Microsoft.AspNetCore.Builder
|
||||||
EnsureRazorPagesServices(endpoints);
|
EnsureRazorPagesServices(endpoints);
|
||||||
|
|
||||||
// Called for side-effect to make sure that the data source is registered.
|
// 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
|
// Maps a fallback endpoint with an empty delegate. This is OK because
|
||||||
// we don't expect the delegate to run.
|
// 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.
|
// MVC registers a policy that looks for this metadata.
|
||||||
b.Metadata.Add(CreateDynamicPageMetadata(page, area: null));
|
b.Metadata.Add(CreateDynamicPageMetadata(page, area: null));
|
||||||
|
b.Metadata.Add(new PageEndpointDataSourceIdMetadata(pageDataSource.DataSourceId));
|
||||||
});
|
});
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
@ -141,7 +145,9 @@ namespace Microsoft.AspNetCore.Builder
|
||||||
EnsureRazorPagesServices(endpoints);
|
EnsureRazorPagesServices(endpoints);
|
||||||
|
|
||||||
// Called for side-effect to make sure that the data source is registered.
|
// 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
|
// Maps a fallback endpoint with an empty delegate. This is OK because
|
||||||
// we don't expect the delegate to run.
|
// 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.
|
// MVC registers a policy that looks for this metadata.
|
||||||
b.Metadata.Add(CreateDynamicPageMetadata(page, area: null));
|
b.Metadata.Add(CreateDynamicPageMetadata(page, area: null));
|
||||||
|
b.Metadata.Add(new PageEndpointDataSourceIdMetadata(pageDataSource.DataSourceId));
|
||||||
});
|
});
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
@ -199,7 +206,9 @@ namespace Microsoft.AspNetCore.Builder
|
||||||
EnsureRazorPagesServices(endpoints);
|
EnsureRazorPagesServices(endpoints);
|
||||||
|
|
||||||
// Called for side-effect to make sure that the data source is registered.
|
// 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
|
// Maps a fallback endpoint with an empty delegate. This is OK because
|
||||||
// we don't expect the delegate to run.
|
// 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.
|
// MVC registers a policy that looks for this metadata.
|
||||||
b.Metadata.Add(CreateDynamicPageMetadata(page, area));
|
b.Metadata.Add(CreateDynamicPageMetadata(page, area));
|
||||||
|
b.Metadata.Add(new PageEndpointDataSourceIdMetadata(pageDataSource.DataSourceId));
|
||||||
});
|
});
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
@ -267,7 +277,9 @@ namespace Microsoft.AspNetCore.Builder
|
||||||
EnsureRazorPagesServices(endpoints);
|
EnsureRazorPagesServices(endpoints);
|
||||||
|
|
||||||
// Called for side-effect to make sure that the data source is registered.
|
// 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
|
// Maps a fallback endpoint with an empty delegate. This is OK because
|
||||||
// we don't expect the delegate to run.
|
// 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.
|
// MVC registers a policy that looks for this metadata.
|
||||||
b.Metadata.Add(CreateDynamicPageMetadata(page, area));
|
b.Metadata.Add(CreateDynamicPageMetadata(page, area));
|
||||||
|
b.Metadata.Add(new PageEndpointDataSourceIdMetadata(pageDataSource.DataSourceId));
|
||||||
});
|
});
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
@ -337,9 +350,51 @@ namespace Microsoft.AspNetCore.Builder
|
||||||
EnsureRazorPagesServices(endpoints);
|
EnsureRazorPagesServices(endpoints);
|
||||||
|
|
||||||
// Called for side-effect to make sure that the data source is registered.
|
// 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)
|
private static DynamicPageMetadata CreateDynamicPageMetadata(string page, string area)
|
||||||
|
|
@ -353,7 +408,7 @@ namespace Microsoft.AspNetCore.Builder
|
||||||
|
|
||||||
private static void EnsureRazorPagesServices(IEndpointRouteBuilder endpoints)
|
private static void EnsureRazorPagesServices(IEndpointRouteBuilder endpoints)
|
||||||
{
|
{
|
||||||
var marker = endpoints.ServiceProvider.GetService<PageActionEndpointDataSource>();
|
var marker = endpoints.ServiceProvider.GetService<PageActionEndpointDataSourceFactory>();
|
||||||
if (marker == null)
|
if (marker == null)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(Mvc.Core.Resources.FormatUnableToFindServices(
|
throw new InvalidOperationException(Mvc.Core.Resources.FormatUnableToFindServices(
|
||||||
|
|
@ -368,11 +423,19 @@ namespace Microsoft.AspNetCore.Builder
|
||||||
var dataSource = endpoints.DataSources.OfType<PageActionEndpointDataSource>().FirstOrDefault();
|
var dataSource = endpoints.DataSources.OfType<PageActionEndpointDataSource>().FirstOrDefault();
|
||||||
if (dataSource == null)
|
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);
|
endpoints.DataSources.Add(dataSource);
|
||||||
}
|
}
|
||||||
|
|
||||||
return 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
|
// Routing
|
||||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, PageLoaderMatcherPolicy>());
|
services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, PageLoaderMatcherPolicy>());
|
||||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, DynamicPageEndpointMatcherPolicy>());
|
services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, DynamicPageEndpointMatcherPolicy>());
|
||||||
services.TryAddSingleton<DynamicPageEndpointSelector>();
|
services.TryAddSingleton<DynamicPageEndpointSelectorCache>();
|
||||||
|
services.TryAddSingleton<PageActionEndpointDataSourceIdProvider>();
|
||||||
|
|
||||||
// Action description and invocation
|
// Action description and invocation
|
||||||
services.TryAddEnumerable(
|
services.TryAddEnumerable(
|
||||||
ServiceDescriptor.Singleton<IActionDescriptorProvider, PageActionDescriptorProvider>());
|
ServiceDescriptor.Singleton<IActionDescriptorProvider, PageActionDescriptorProvider>());
|
||||||
services.TryAddEnumerable(
|
services.TryAddEnumerable(
|
||||||
ServiceDescriptor.Singleton<IPageRouteModelProvider, CompiledPageRouteModelProvider>());
|
ServiceDescriptor.Singleton<IPageRouteModelProvider, CompiledPageRouteModelProvider>());
|
||||||
services.TryAddSingleton<PageActionEndpointDataSource>();
|
services.TryAddSingleton<PageActionEndpointDataSourceFactory>();
|
||||||
services.TryAddSingleton<DynamicPageEndpointSelector>();
|
|
||||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, DynamicPageEndpointMatcherPolicy>());
|
services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, DynamicPageEndpointMatcherPolicy>());
|
||||||
|
|
||||||
services.TryAddEnumerable(
|
services.TryAddEnumerable(
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
@ -15,15 +16,15 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
||||||
{
|
{
|
||||||
internal class DynamicPageEndpointMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy
|
internal class DynamicPageEndpointMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy
|
||||||
{
|
{
|
||||||
private readonly DynamicPageEndpointSelector _selector;
|
private readonly DynamicPageEndpointSelectorCache _selectorCache;
|
||||||
private readonly PageLoader _loader;
|
private readonly PageLoader _loader;
|
||||||
private readonly EndpointMetadataComparer _comparer;
|
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)
|
if (loader == null)
|
||||||
|
|
@ -36,7 +37,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
||||||
throw new ArgumentNullException(nameof(comparer));
|
throw new ArgumentNullException(nameof(comparer));
|
||||||
}
|
}
|
||||||
|
|
||||||
_selector = selector;
|
_selectorCache = selectorCache;
|
||||||
_loader = loader;
|
_loader = loader;
|
||||||
_comparer = comparer;
|
_comparer = comparer;
|
||||||
}
|
}
|
||||||
|
|
@ -86,6 +87,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
||||||
throw new ArgumentNullException(nameof(candidates));
|
throw new ArgumentNullException(nameof(candidates));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DynamicPageEndpointSelector selector = null;
|
||||||
|
|
||||||
// There's no real benefit here from trying to avoid the async state machine.
|
// 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
|
// We only execute on nodes that contain a dynamic policy, and thus always have
|
||||||
// to await something.
|
// to await something.
|
||||||
|
|
@ -132,7 +135,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var endpoints = _selector.SelectEndpoints(dynamicValues);
|
selector = ResolveSelector(selector, endpoint);
|
||||||
|
var endpoints = selector.SelectEndpoints(dynamicValues);
|
||||||
if (endpoints.Count == 0 && dynamicPageMetadata != null)
|
if (endpoints.Count == 0 && dynamicPageMetadata != null)
|
||||||
{
|
{
|
||||||
// Having no match for a fallback is a configuration error. We can't really check
|
// 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);
|
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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
@ -14,14 +14,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
||||||
private readonly EndpointDataSource _dataSource;
|
private readonly EndpointDataSource _dataSource;
|
||||||
private readonly DataSourceDependentCache<ActionSelectionTable<Endpoint>> _cache;
|
private readonly DataSourceDependentCache<ActionSelectionTable<Endpoint>> _cache;
|
||||||
|
|
||||||
public DynamicPageEndpointSelector(PageActionEndpointDataSource dataSource)
|
public DynamicPageEndpointSelector(EndpointDataSource 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)
|
|
||||||
{
|
{
|
||||||
if (dataSource == null)
|
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;
|
private readonly OrderedEndpointsSequenceProvider _orderSequence;
|
||||||
|
|
||||||
public PageActionEndpointDataSource(
|
public PageActionEndpointDataSource(
|
||||||
|
PageActionEndpointDataSourceIdProvider dataSourceIdProvider,
|
||||||
IActionDescriptorCollectionProvider actions,
|
IActionDescriptorCollectionProvider actions,
|
||||||
ActionEndpointFactory endpointFactory,
|
ActionEndpointFactory endpointFactory,
|
||||||
OrderedEndpointsSequenceProvider orderedEndpoints)
|
OrderedEndpointsSequenceProvider orderedEndpoints)
|
||||||
: base(actions)
|
: base(actions)
|
||||||
{
|
{
|
||||||
|
DataSourceId = dataSourceIdProvider.CreateId();
|
||||||
_endpointFactory = endpointFactory;
|
_endpointFactory = endpointFactory;
|
||||||
_orderSequence = orderedEndpoints;
|
_orderSequence = orderedEndpoints;
|
||||||
DefaultBuilder = new PageActionEndpointConventionBuilder(Lock, Conventions);
|
DefaultBuilder = new PageActionEndpointConventionBuilder(Lock, Conventions);
|
||||||
|
|
@ -32,6 +34,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
||||||
Subscribe();
|
Subscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int DataSourceId { get; }
|
||||||
|
|
||||||
public PageActionEndpointConventionBuilder DefaultBuilder { get; }
|
public PageActionEndpointConventionBuilder DefaultBuilder { get; }
|
||||||
|
|
||||||
// Used to control whether we create 'inert' (non-routable) endpoints for use in dynamic
|
// 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;
|
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;
|
CreateInertEndpoints = true;
|
||||||
lock (Lock)
|
lock (Lock)
|
||||||
{
|
{
|
||||||
var order = _orderSequence.GetNext();
|
order ??= _orderSequence.GetNext();
|
||||||
|
|
||||||
endpoints.Map(
|
endpoints.Map(
|
||||||
pattern,
|
pattern,
|
||||||
|
|
@ -68,11 +72,11 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
||||||
})
|
})
|
||||||
.Add(b =>
|
.Add(b =>
|
||||||
{
|
{
|
||||||
((RouteEndpointBuilder)b).Order = order;
|
((RouteEndpointBuilder)b).Order = order.Value;
|
||||||
b.Metadata.Add(new DynamicPageRouteValueTransformerMetadata(transformerType, state));
|
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 EndpointMetadataCollection(new object[]
|
||||||
{
|
{
|
||||||
new DynamicPageRouteValueTransformerMetadata(typeof(CustomTransformer), State),
|
new DynamicPageRouteValueTransformerMetadata(typeof(CustomTransformer), State),
|
||||||
|
new PageEndpointDataSourceIdMetadata(1),
|
||||||
}),
|
}),
|
||||||
"dynamic");
|
"dynamic");
|
||||||
|
|
||||||
DataSource = new DefaultEndpointDataSource(PageEndpoints);
|
DataSource = new DefaultEndpointDataSource(PageEndpoints);
|
||||||
|
|
||||||
Selector = new TestDynamicPageEndpointSelector(DataSource);
|
SelectorCache = new TestDynamicPageEndpointSelectorCache(DataSource);
|
||||||
|
|
||||||
var services = new ServiceCollection();
|
var services = new ServiceCollection();
|
||||||
services.AddRouting();
|
services.AddRouting();
|
||||||
|
|
@ -106,7 +107,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
||||||
|
|
||||||
private PageLoader Loader { get; }
|
private PageLoader Loader { get; }
|
||||||
|
|
||||||
private DynamicPageEndpointSelector Selector { get; }
|
private DynamicPageEndpointSelectorCache SelectorCache { get; }
|
||||||
|
|
||||||
private object State { get; }
|
private object State { get; }
|
||||||
|
|
||||||
|
|
@ -120,7 +121,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
||||||
public async Task ApplyAsync_NoMatch()
|
public async Task ApplyAsync_NoMatch()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var policy = new DynamicPageEndpointMatcherPolicy(Selector, Loader, Comparer);
|
var policy = new DynamicPageEndpointMatcherPolicy(SelectorCache, Loader, Comparer);
|
||||||
|
|
||||||
var endpoints = new[] { DynamicEndpoint, };
|
var endpoints = new[] { DynamicEndpoint, };
|
||||||
var values = new RouteValueDictionary[] { null, };
|
var values = new RouteValueDictionary[] { null, };
|
||||||
|
|
@ -150,7 +151,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
||||||
public async Task ApplyAsync_HasMatchNoEndpointFound()
|
public async Task ApplyAsync_HasMatchNoEndpointFound()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var policy = new DynamicPageEndpointMatcherPolicy(Selector, Loader, Comparer);
|
var policy = new DynamicPageEndpointMatcherPolicy(SelectorCache, Loader, Comparer);
|
||||||
|
|
||||||
var endpoints = new[] { DynamicEndpoint, };
|
var endpoints = new[] { DynamicEndpoint, };
|
||||||
var values = new RouteValueDictionary[] { null, };
|
var values = new RouteValueDictionary[] { null, };
|
||||||
|
|
@ -181,7 +182,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
||||||
public async Task ApplyAsync_HasMatchFindsEndpoint_WithoutRouteValues()
|
public async Task ApplyAsync_HasMatchFindsEndpoint_WithoutRouteValues()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var policy = new DynamicPageEndpointMatcherPolicy(Selector, Loader, Comparer);
|
var policy = new DynamicPageEndpointMatcherPolicy(SelectorCache, Loader, Comparer);
|
||||||
|
|
||||||
var endpoints = new[] { DynamicEndpoint, };
|
var endpoints = new[] { DynamicEndpoint, };
|
||||||
var values = new RouteValueDictionary[] { null, };
|
var values = new RouteValueDictionary[] { null, };
|
||||||
|
|
@ -221,7 +222,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
||||||
public async Task ApplyAsync_HasMatchFindsEndpoint_WithRouteValues()
|
public async Task ApplyAsync_HasMatchFindsEndpoint_WithRouteValues()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var policy = new DynamicPageEndpointMatcherPolicy(Selector, Loader, Comparer);
|
var policy = new DynamicPageEndpointMatcherPolicy(SelectorCache, Loader, Comparer);
|
||||||
|
|
||||||
var endpoints = new[] { DynamicEndpoint, };
|
var endpoints = new[] { DynamicEndpoint, };
|
||||||
var values = new RouteValueDictionary[] { new RouteValueDictionary(new { slug = "test", }), };
|
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()
|
public async Task ApplyAsync_Throws_ForTransformersWithInvalidLifetime()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var policy = new DynamicPageEndpointMatcherPolicy(Selector, Loader, Comparer);
|
var policy = new DynamicPageEndpointMatcherPolicy(SelectorCache, Loader, Comparer);
|
||||||
|
|
||||||
var endpoints = new[] { DynamicEndpoint, };
|
var endpoints = new[] { DynamicEndpoint, };
|
||||||
var values = new RouteValueDictionary[] { new RouteValueDictionary(new { slug = "test", }), };
|
var values = new RouteValueDictionary[] { new RouteValueDictionary(new { slug = "test", }), };
|
||||||
|
|
@ -302,7 +303,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
||||||
public async Task ApplyAsync_CanDiscardFoundEndpoints()
|
public async Task ApplyAsync_CanDiscardFoundEndpoints()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var policy = new DynamicPageEndpointMatcherPolicy(Selector, Loader, Comparer);
|
var policy = new DynamicPageEndpointMatcherPolicy(SelectorCache, Loader, Comparer);
|
||||||
|
|
||||||
var endpoints = new[] { DynamicEndpoint, };
|
var endpoints = new[] { DynamicEndpoint, };
|
||||||
var values = new RouteValueDictionary[] { new RouteValueDictionary(new { slug = "test", }), };
|
var values = new RouteValueDictionary[] { new RouteValueDictionary(new { slug = "test", }), };
|
||||||
|
|
@ -340,7 +341,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
||||||
public async Task ApplyAsync_CanReplaceFoundEndpoints()
|
public async Task ApplyAsync_CanReplaceFoundEndpoints()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var policy = new DynamicPageEndpointMatcherPolicy(Selector, Loader, Comparer);
|
var policy = new DynamicPageEndpointMatcherPolicy(SelectorCache, Loader, Comparer);
|
||||||
|
|
||||||
var endpoints = new[] { DynamicEndpoint, };
|
var endpoints = new[] { DynamicEndpoint, };
|
||||||
var values = new RouteValueDictionary[] { new RouteValueDictionary(new { slug = "test", }), };
|
var values = new RouteValueDictionary[] { new RouteValueDictionary(new { slug = "test", }), };
|
||||||
|
|
@ -400,7 +401,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
||||||
public async Task ApplyAsync_CanExpandTheListOfFoundEndpoints()
|
public async Task ApplyAsync_CanExpandTheListOfFoundEndpoints()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var policy = new DynamicPageEndpointMatcherPolicy(Selector, Loader, Comparer);
|
var policy = new DynamicPageEndpointMatcherPolicy(SelectorCache, Loader, Comparer);
|
||||||
|
|
||||||
var endpoints = new[] { DynamicEndpoint, };
|
var endpoints = new[] { DynamicEndpoint, };
|
||||||
var values = new RouteValueDictionary[] { new RouteValueDictionary(new { slug = "test", }), };
|
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);
|
Assert.Same(LoadedEndpoints[1], candidates[1].Endpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
private class TestDynamicPageEndpointSelector : DynamicPageEndpointSelector
|
private class TestDynamicPageEndpointSelectorCache : DynamicPageEndpointSelectorCache
|
||||||
{
|
{
|
||||||
public TestDynamicPageEndpointSelector(EndpointDataSource dataSource)
|
public TestDynamicPageEndpointSelectorCache(EndpointDataSource dataSource)
|
||||||
: base(dataSource)
|
|
||||||
{
|
{
|
||||||
|
AddDataSource(dataSource, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
||||||
|
|
||||||
private protected override ActionEndpointDataSourceBase CreateDataSource(IActionDescriptorCollectionProvider actions, ActionEndpointFactory endpointFactory)
|
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(
|
protected override ActionDescriptor CreateActionDescriptor(
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,7 @@ namespace Microsoft.AspNetCore.Mvc.Performance
|
||||||
private ControllerActionEndpointDataSource CreateDataSource(IActionDescriptorCollectionProvider actionDescriptorCollectionProvider)
|
private ControllerActionEndpointDataSource CreateDataSource(IActionDescriptorCollectionProvider actionDescriptorCollectionProvider)
|
||||||
{
|
{
|
||||||
var dataSource = new ControllerActionEndpointDataSource(
|
var dataSource = new ControllerActionEndpointDataSource(
|
||||||
|
new ControllerActionEndpointDataSourceIdProvider(),
|
||||||
actionDescriptorCollectionProvider,
|
actionDescriptorCollectionProvider,
|
||||||
new ActionEndpointFactory(new MockRoutePatternTransformer()),
|
new ActionEndpointFactory(new MockRoutePatternTransformer()),
|
||||||
new OrderedEndpointsSequenceProvider());
|
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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||||
using Microsoft.AspNetCore.Mvc.Routing;
|
using Microsoft.AspNetCore.Mvc.Routing;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
@ -24,6 +25,8 @@ namespace RoutingWebSite
|
||||||
.SetCompatibilityVersion(CompatibilityVersion.Latest);
|
.SetCompatibilityVersion(CompatibilityVersion.Latest);
|
||||||
|
|
||||||
services.AddTransient<Transformer>();
|
services.AddTransient<Transformer>();
|
||||||
|
services.AddScoped<TestResponseGenerator>();
|
||||||
|
services.AddSingleton<IActionContextAccessor, ActionContextAccessor>();
|
||||||
|
|
||||||
// Used by some controllers defined in this project.
|
// Used by some controllers defined in this project.
|
||||||
services.Configure<RouteOptions>(options => options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer));
|
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