Add dynamic controller/page routes
Adds infrastructure for a common IRouter-based pattern. In this pattern, an extender subclasses Route to post-process the route values before MVC action selection run. The new infrastructure duplicates this kind of experience but based on endpoint routing. The approach in this PR starts at the bottom... meaning that this is the most-focused and least-invasive way to implement a feature like this. Similar to fallback routing, this is a pattern built with matcher policies and metadata rather than a built-in feature of routing. It's valuable to point out that this approach uses IActionConstraint to disambiguate between actions. The other way we could go would be to make the *other* matcher policy implementations able to do this. This would mean that whenever you have a dynamic endpoint, you will not by using the DFA for features like HTTP methods. It also means that we need to go re-implement a bunch of infrastructure. This PR also adds the concept of an 'inert' endpoint - a non-Routable endpoint that's created when fallback/dynamic is in use. This seems like a cleaner design because we don't start *matching* RouteEndpoint instances for URLs that don't match. This resolves #8130
This commit is contained in:
parent
5ca92305c2
commit
6ce8a879ae
|
|
@ -113,8 +113,8 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
|||
{
|
||||
// We do this check first for consistency with how 405 is implemented for the graph version
|
||||
// of this code. We still want to know if any endpoints in this set require an HTTP method
|
||||
// even if those endpoints are already invalid.
|
||||
var metadata = candidates[i].Endpoint.Metadata.GetMetadata<IHttpMethodMetadata>();
|
||||
// even if those endpoints are already invalid - hence the null-check.
|
||||
var metadata = candidates[i].Endpoint?.Metadata.GetMetadata<IHttpMethodMetadata>();
|
||||
if (metadata == null || metadata.HttpMethods.Count == 0)
|
||||
{
|
||||
// Can match any method.
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ namespace Microsoft.AspNetCore.Builder
|
|||
public static Microsoft.AspNetCore.Builder.ControllerActionEndpointConventionBuilder MapControllerRoute(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string name, string pattern, object defaults = null, object constraints = null, object dataTokens = null) { throw null; }
|
||||
public static Microsoft.AspNetCore.Builder.ControllerActionEndpointConventionBuilder MapControllers(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints) { throw null; }
|
||||
public static Microsoft.AspNetCore.Builder.ControllerActionEndpointConventionBuilder MapDefaultControllerRoute(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints) { throw null; }
|
||||
public static void MapDynamicControllerRoute<TTransformer>(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string pattern) where TTransformer : Microsoft.AspNetCore.Mvc.Routing.DynamicRouteValueTransformer { }
|
||||
public static Microsoft.AspNetCore.Builder.IEndpointConventionBuilder MapFallbackToAreaController(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string action, string controller, string area) { throw null; }
|
||||
public static Microsoft.AspNetCore.Builder.IEndpointConventionBuilder MapFallbackToAreaController(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string pattern, string action, string controller, string area) { throw null; }
|
||||
public static Microsoft.AspNetCore.Builder.IEndpointConventionBuilder MapFallbackToController(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string action, string controller) { throw null; }
|
||||
|
|
@ -2909,6 +2910,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
|
|||
}
|
||||
namespace Microsoft.AspNetCore.Mvc.Routing
|
||||
{
|
||||
public abstract partial class DynamicRouteValueTransformer
|
||||
{
|
||||
protected DynamicRouteValueTransformer() { }
|
||||
public abstract System.Threading.Tasks.Task<Microsoft.AspNetCore.Routing.RouteValueDictionary> TransformAsync(Microsoft.AspNetCore.Http.HttpContext httpContext, Microsoft.AspNetCore.Routing.RouteValueDictionary values);
|
||||
}
|
||||
[System.AttributeUsageAttribute(System.AttributeTargets.Method, AllowMultiple=true, Inherited=true)]
|
||||
public abstract partial class HttpMethodAttribute : System.Attribute, Microsoft.AspNetCore.Mvc.Routing.IActionHttpMethodProvider, Microsoft.AspNetCore.Mvc.Routing.IRouteTemplateProvider
|
||||
{
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Mvc.Core;
|
|||
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
|
||||
|
|
@ -212,7 +213,7 @@ namespace Microsoft.AspNetCore.Builder
|
|||
EnsureControllerServices(endpoints);
|
||||
|
||||
// Called for side-effect to make sure that the data source is registered.
|
||||
GetOrCreateDataSource(endpoints);
|
||||
GetOrCreateDataSource(endpoints).CreateInertEndpoints = true;
|
||||
|
||||
// Maps a fallback endpoint with an empty delegate. This is OK because
|
||||
// we don't expect the delegate to run.
|
||||
|
|
@ -288,7 +289,7 @@ namespace Microsoft.AspNetCore.Builder
|
|||
EnsureControllerServices(endpoints);
|
||||
|
||||
// Called for side-effect to make sure that the data source is registered.
|
||||
GetOrCreateDataSource(endpoints);
|
||||
GetOrCreateDataSource(endpoints).CreateInertEndpoints = true;
|
||||
|
||||
// Maps a fallback endpoint with an empty delegate. This is OK because
|
||||
// we don't expect the delegate to run.
|
||||
|
|
@ -356,7 +357,7 @@ namespace Microsoft.AspNetCore.Builder
|
|||
EnsureControllerServices(endpoints);
|
||||
|
||||
// Called for side-effect to make sure that the data source is registered.
|
||||
GetOrCreateDataSource(endpoints);
|
||||
GetOrCreateDataSource(endpoints).CreateInertEndpoints = true;
|
||||
|
||||
// Maps a fallback endpoint with an empty delegate. This is OK because
|
||||
// we don't expect the delegate to run.
|
||||
|
|
@ -434,7 +435,7 @@ namespace Microsoft.AspNetCore.Builder
|
|||
EnsureControllerServices(endpoints);
|
||||
|
||||
// Called for side-effect to make sure that the data source is registered.
|
||||
GetOrCreateDataSource(endpoints);
|
||||
GetOrCreateDataSource(endpoints).CreateInertEndpoints = true;
|
||||
|
||||
// Maps a fallback endpoint with an empty delegate. This is OK because
|
||||
// we don't expect the delegate to run.
|
||||
|
|
@ -447,6 +448,48 @@ namespace Microsoft.AspNetCore.Builder
|
|||
return builder;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// <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"/> with the desired service lifetime in <c>ConfigureServices</c>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static void MapDynamicControllerRoute<TTransformer>(this IEndpointRouteBuilder endpoints, string pattern)
|
||||
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.
|
||||
GetOrCreateDataSource(endpoints).CreateInertEndpoints = true;
|
||||
|
||||
endpoints.Map(
|
||||
pattern,
|
||||
context =>
|
||||
{
|
||||
throw new InvalidOperationException("This endpoint is not expected to be executed directly.");
|
||||
})
|
||||
.Add(b =>
|
||||
{
|
||||
b.Metadata.Add(new DynamicControllerRouteValueTransformerMetadata(typeof(TTransformer)));
|
||||
});
|
||||
}
|
||||
|
||||
private static DynamicControllerMetadata CreateDynamicControllerMetadata(string action, string controller, string area)
|
||||
{
|
||||
return new DynamicControllerMetadata(new RouteValueDictionary()
|
||||
|
|
|
|||
|
|
@ -74,23 +74,23 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
});
|
||||
}
|
||||
|
||||
public static ActionSelectionTable<RouteEndpoint> Create(IEnumerable<Endpoint> endpoints)
|
||||
public static ActionSelectionTable<Endpoint> Create(IEnumerable<Endpoint> endpoints)
|
||||
{
|
||||
return CreateCore<RouteEndpoint>(
|
||||
return CreateCore<Endpoint>(
|
||||
|
||||
// we don't use version for endpoints
|
||||
version: 0,
|
||||
|
||||
// Only include RouteEndpoints and only those that aren't suppressed.
|
||||
items: endpoints.OfType<RouteEndpoint>().Where(e =>
|
||||
// Exclude RouteEndpoints - we only process inert endpoints here.
|
||||
items: endpoints.Where(e =>
|
||||
{
|
||||
return e.Metadata.GetMetadata<ISuppressMatchingMetadata>()?.SuppressMatching != true;
|
||||
return e.GetType() == typeof(Endpoint);
|
||||
}),
|
||||
|
||||
getRouteKeys: e => e.RoutePattern.RequiredValues.Keys,
|
||||
getRouteKeys: e => e.Metadata.GetMetadata<ActionDescriptor>().RouteValues.Keys,
|
||||
getRouteValue: (e, key) =>
|
||||
{
|
||||
e.RoutePattern.RequiredValues.TryGetValue(key, out var value);
|
||||
e.Metadata.GetMetadata<ActionDescriptor>().RouteValues.TryGetValue(key, out var value);
|
||||
return Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
internal class ActionEndpointFactory
|
||||
{
|
||||
private readonly RoutePatternTransformer _routePatternTransformer;
|
||||
private readonly RequestDelegate _requestDelegate;
|
||||
|
||||
public ActionEndpointFactory(RoutePatternTransformer routePatternTransformer)
|
||||
{
|
||||
|
|
@ -29,6 +30,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
}
|
||||
|
||||
_routePatternTransformer = routePatternTransformer;
|
||||
_requestDelegate = CreateRequestDelegate();
|
||||
}
|
||||
|
||||
public void AddEndpoints(
|
||||
|
|
@ -36,7 +38,8 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
HashSet<string> routeNames,
|
||||
ActionDescriptor action,
|
||||
IReadOnlyList<ConventionalRouteEntry> routes,
|
||||
IReadOnlyList<Action<EndpointBuilder>> conventions)
|
||||
IReadOnlyList<Action<EndpointBuilder>> conventions,
|
||||
bool createInertEndpoints)
|
||||
{
|
||||
if (endpoints == null)
|
||||
{
|
||||
|
|
@ -63,6 +66,26 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
throw new ArgumentNullException(nameof(conventions));
|
||||
}
|
||||
|
||||
if (createInertEndpoints)
|
||||
{
|
||||
var builder = new InertEndpointBuilder()
|
||||
{
|
||||
DisplayName = action.DisplayName,
|
||||
RequestDelegate = _requestDelegate,
|
||||
};
|
||||
AddActionDataToBuilder(
|
||||
builder,
|
||||
routeNames,
|
||||
action,
|
||||
routeName: null,
|
||||
dataTokens: null,
|
||||
suppressLinkGeneration: false,
|
||||
suppressPathMatching: false,
|
||||
conventions,
|
||||
Array.Empty<Action<EndpointBuilder>>());
|
||||
endpoints.Add(builder.Build());
|
||||
}
|
||||
|
||||
if (action.AttributeRouteInfo == null)
|
||||
{
|
||||
// Check each of the conventional patterns to see if the action would be reachable.
|
||||
|
|
@ -81,18 +104,21 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
|
||||
// We suppress link generation for each conventionally routed endpoint. We generate a single endpoint per-route
|
||||
// to handle link generation.
|
||||
var builder = CreateEndpoint(
|
||||
var builder = new RouteEndpointBuilder(_requestDelegate, updatedRoutePattern, route.Order)
|
||||
{
|
||||
DisplayName = action.DisplayName,
|
||||
};
|
||||
AddActionDataToBuilder(
|
||||
builder,
|
||||
routeNames,
|
||||
action,
|
||||
updatedRoutePattern,
|
||||
route.RouteName,
|
||||
route.Order,
|
||||
route.DataTokens,
|
||||
suppressLinkGeneration: true,
|
||||
suppressPathMatching: false,
|
||||
conventions,
|
||||
route.Conventions);
|
||||
endpoints.Add(builder);
|
||||
endpoints.Add(builder.Build());
|
||||
}
|
||||
}
|
||||
else
|
||||
|
|
@ -109,18 +135,21 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
throw new InvalidOperationException("Failed to update route pattern with required values.");
|
||||
}
|
||||
|
||||
var endpoint = CreateEndpoint(
|
||||
var builder = new RouteEndpointBuilder(_requestDelegate, updatedRoutePattern, action.AttributeRouteInfo.Order)
|
||||
{
|
||||
DisplayName = action.DisplayName,
|
||||
};
|
||||
AddActionDataToBuilder(
|
||||
builder,
|
||||
routeNames,
|
||||
action,
|
||||
updatedRoutePattern,
|
||||
action.AttributeRouteInfo.Name,
|
||||
action.AttributeRouteInfo.Order,
|
||||
dataTokens: null,
|
||||
action.AttributeRouteInfo.SuppressLinkGeneration,
|
||||
action.AttributeRouteInfo.SuppressPathMatching,
|
||||
conventions,
|
||||
perRouteConventions: Array.Empty<Action<EndpointBuilder>>());
|
||||
endpoints.Add(endpoint);
|
||||
endpoints.Add(builder.Build());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -262,49 +291,17 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
return (attributeRoutePattern, resolvedRequiredValues ?? action.RouteValues);
|
||||
}
|
||||
|
||||
private RouteEndpoint CreateEndpoint(
|
||||
private void AddActionDataToBuilder(
|
||||
EndpointBuilder builder,
|
||||
HashSet<string> routeNames,
|
||||
ActionDescriptor action,
|
||||
RoutePattern routePattern,
|
||||
string routeName,
|
||||
int order,
|
||||
RouteValueDictionary dataTokens,
|
||||
bool suppressLinkGeneration,
|
||||
bool suppressPathMatching,
|
||||
IReadOnlyList<Action<EndpointBuilder>> conventions,
|
||||
IReadOnlyList<Action<EndpointBuilder>> perRouteConventions)
|
||||
{
|
||||
|
||||
// We don't want to close over the retrieve the Invoker Factory in ActionEndpointFactory as
|
||||
// that creates cycles in DI. Since we're creating this delegate at startup time
|
||||
// we don't want to create all of the things we use at runtime until the action
|
||||
// actually matches.
|
||||
//
|
||||
// The request delegate is already a closure here because we close over
|
||||
// the action descriptor.
|
||||
IActionInvokerFactory invokerFactory = null;
|
||||
|
||||
RequestDelegate requestDelegate = (context) =>
|
||||
{
|
||||
var routeData = new RouteData();
|
||||
routeData.PushState(router: null, context.Request.RouteValues, dataTokens);
|
||||
|
||||
var actionContext = new ActionContext(context, routeData, action);
|
||||
|
||||
if (invokerFactory == null)
|
||||
{
|
||||
invokerFactory = context.RequestServices.GetRequiredService<IActionInvokerFactory>();
|
||||
}
|
||||
|
||||
var invoker = invokerFactory.CreateInvoker(actionContext);
|
||||
return invoker.InvokeAsync();
|
||||
};
|
||||
|
||||
var builder = new RouteEndpointBuilder(requestDelegate, routePattern, order)
|
||||
{
|
||||
DisplayName = action.DisplayName,
|
||||
};
|
||||
|
||||
// Add action metadata first so it has a low precedence
|
||||
if (action.EndpointMetadata != null)
|
||||
{
|
||||
|
|
@ -399,8 +396,47 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
{
|
||||
perRouteConventions[i](builder);
|
||||
}
|
||||
}
|
||||
|
||||
return (RouteEndpoint)builder.Build();
|
||||
private static RequestDelegate CreateRequestDelegate()
|
||||
{
|
||||
// We don't want to close over the Invoker Factory in ActionEndpointFactory as
|
||||
// that creates cycles in DI. Since we're creating this delegate at startup time
|
||||
// we don't want to create all of the things we use at runtime until the action
|
||||
// actually matches.
|
||||
//
|
||||
// The request delegate is already a closure here because we close over
|
||||
// the action descriptor.
|
||||
IActionInvokerFactory invokerFactory = null;
|
||||
|
||||
return (context) =>
|
||||
{
|
||||
var endpoint = context.GetEndpoint();
|
||||
var dataTokens = endpoint.Metadata.GetMetadata<IDataTokensMetadata>();
|
||||
|
||||
var routeData = new RouteData();
|
||||
routeData.PushState(router: null, context.Request.RouteValues, new RouteValueDictionary(dataTokens?.DataTokens));
|
||||
|
||||
// Don't close over the ActionDescriptor, that's not valid for pages.
|
||||
var action = endpoint.Metadata.GetMetadata<ActionDescriptor>();
|
||||
var actionContext = new ActionContext(context, routeData, action);
|
||||
|
||||
if (invokerFactory == null)
|
||||
{
|
||||
invokerFactory = context.RequestServices.GetRequiredService<IActionInvokerFactory>();
|
||||
}
|
||||
|
||||
var invoker = invokerFactory.CreateInvoker(actionContext);
|
||||
return invoker.InvokeAsync();
|
||||
};
|
||||
}
|
||||
|
||||
private class InertEndpointBuilder : EndpointBuilder
|
||||
{
|
||||
public override Endpoint Build()
|
||||
{
|
||||
return new Endpoint(RequestDelegate, new EndpointMetadataCollection(Metadata), DisplayName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,8 +73,8 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
{
|
||||
// We do this check first for consistency with how 415 is implemented for the graph version
|
||||
// of this code. We still want to know if any endpoints in this set require an a ContentType
|
||||
// even if those endpoints are already invalid.
|
||||
var metadata = candidates[i].Endpoint.Metadata.GetMetadata<IConsumesMetadata>();
|
||||
// even if those endpoints are already invalid - hence the null check.
|
||||
var metadata = candidates[i].Endpoint?.Metadata.GetMetadata<IConsumesMetadata>();
|
||||
if (metadata == null || metadata.ContentTypes.Count == 0)
|
||||
{
|
||||
// Can match any content type.
|
||||
|
|
|
|||
|
|
@ -20,12 +20,12 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
private int _order;
|
||||
|
||||
public ControllerActionEndpointDataSource(
|
||||
IActionDescriptorCollectionProvider actions,
|
||||
IActionDescriptorCollectionProvider actions,
|
||||
ActionEndpointFactory endpointFactory)
|
||||
: base(actions)
|
||||
{
|
||||
_endpointFactory = endpointFactory;
|
||||
|
||||
|
||||
_routes = new List<ConventionalRouteEntry>();
|
||||
|
||||
// In traditional conventional routing setup, the routes defined by a user have a order
|
||||
|
|
@ -38,13 +38,17 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
|
||||
DefaultBuilder = new ControllerActionEndpointConventionBuilder(Lock, Conventions);
|
||||
|
||||
// IMPORTANT: this needs to be the last thing we do in the constructor.
|
||||
// IMPORTANT: this needs to be the last thing we do in the constructor.
|
||||
// Change notifications can happen immediately!
|
||||
Subscribe();
|
||||
}
|
||||
|
||||
public ControllerActionEndpointConventionBuilder DefaultBuilder { get; }
|
||||
|
||||
// Used to control whether we create 'inert' (non-routable) endpoints for use in dynamic
|
||||
// selection. Set to true by builder methods that do dynamic/fallback selection.
|
||||
public bool CreateInertEndpoints { get; set; }
|
||||
|
||||
public ControllerActionEndpointConventionBuilder AddRoute(
|
||||
string routeName,
|
||||
string pattern,
|
||||
|
|
@ -80,7 +84,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
{
|
||||
if (actions[i] is ControllerActionDescriptor action)
|
||||
{
|
||||
_endpointFactory.AddEndpoints(endpoints, routeNames, action, _routes, conventions);
|
||||
_endpointFactory.AddEndpoints(endpoints, routeNames, action, _routes, conventions, CreateInertEndpoints);
|
||||
|
||||
if (_routes.Count > 0)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ using System.Threading.Tasks;
|
|||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.Routing.Matching;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Routing
|
||||
{
|
||||
|
|
@ -49,8 +50,13 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
|
||||
for (var i = 0; i < endpoints.Count; i++)
|
||||
{
|
||||
var metadata = endpoints[i].Metadata.GetMetadata<DynamicControllerMetadata>();
|
||||
if (metadata != null)
|
||||
if (endpoints[i].Metadata.GetMetadata<DynamicControllerMetadata>() != null)
|
||||
{
|
||||
// Found a dynamic controller endpoint
|
||||
return true;
|
||||
}
|
||||
|
||||
if (endpoints[i].Metadata.GetMetadata<DynamicControllerRouteValueTransformerMetadata>() != null)
|
||||
{
|
||||
// Found a dynamic controller endpoint
|
||||
return true;
|
||||
|
|
@ -60,7 +66,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
return false;
|
||||
}
|
||||
|
||||
public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates)
|
||||
public async Task ApplyAsync(HttpContext httpContext, CandidateSet candidates)
|
||||
{
|
||||
if (httpContext == null)
|
||||
{
|
||||
|
|
@ -83,30 +89,56 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
}
|
||||
|
||||
var endpoint = candidates[i].Endpoint;
|
||||
var originalValues = candidates[i].Values;
|
||||
|
||||
var metadata = endpoint.Metadata.GetMetadata<DynamicControllerMetadata>();
|
||||
if (metadata == null)
|
||||
RouteValueDictionary dynamicValues = null;
|
||||
|
||||
// We don't expect both of these to be provided, and they are internal so there's
|
||||
// no realistic way this could happen.
|
||||
var dynamicControllerMetadata = endpoint.Metadata.GetMetadata<DynamicControllerMetadata>();
|
||||
var transformerMetadata = endpoint.Metadata.GetMetadata<DynamicControllerRouteValueTransformerMetadata>();
|
||||
if (dynamicControllerMetadata != null)
|
||||
{
|
||||
dynamicValues = dynamicControllerMetadata.Values;
|
||||
}
|
||||
else if (transformerMetadata != null)
|
||||
{
|
||||
var transformer = (DynamicRouteValueTransformer)httpContext.RequestServices.GetRequiredService(transformerMetadata.SelectorType);
|
||||
dynamicValues = await transformer.TransformAsync(httpContext, originalValues);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Not a dynamic controller.
|
||||
continue;
|
||||
}
|
||||
|
||||
var matchedValues = candidates[i].Values;
|
||||
var endpoints = _selector.SelectEndpoints(metadata.Values);
|
||||
if (endpoints.Count == 0)
|
||||
if (dynamicValues == null)
|
||||
{
|
||||
// If there's no match this is a configuration error. We can't really check
|
||||
// during startup that the action you configured exists.
|
||||
candidates.ReplaceEndpoint(i, null, null);
|
||||
continue;
|
||||
}
|
||||
|
||||
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
|
||||
// during startup that the action you configured exists, so this is the best we can do.
|
||||
throw new InvalidOperationException(
|
||||
"Cannot find the fallback endpoint specified by route values: " +
|
||||
"{ " + string.Join(", ", metadata.Values.Select(kvp => $"{kvp.Key}: {kvp.Value}")) + " }.");
|
||||
"{ " + string.Join(", ", dynamicValues.Select(kvp => $"{kvp.Key}: {kvp.Value}")) + " }.");
|
||||
}
|
||||
else if (endpoints.Count == 0)
|
||||
{
|
||||
candidates.ReplaceEndpoint(i, null, null);
|
||||
continue;
|
||||
}
|
||||
|
||||
// We need to provide the route values associated with this endpoint, so that features
|
||||
// like URL generation work.
|
||||
var values = new RouteValueDictionary(metadata.Values);
|
||||
var values = new RouteValueDictionary(dynamicValues);
|
||||
|
||||
// Include values that were matched by the fallback route.
|
||||
foreach (var kvp in matchedValues)
|
||||
foreach (var kvp in originalValues)
|
||||
{
|
||||
values.TryAdd(kvp.Key, kvp.Value);
|
||||
}
|
||||
|
|
@ -117,8 +149,6 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
// Expand the list of endpoints
|
||||
candidates.ExpandEndpoint(i, endpoints, _comparer);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
internal class DynamicControllerEndpointSelector : IDisposable
|
||||
{
|
||||
private readonly ControllerActionEndpointDataSource _dataSource;
|
||||
private readonly DataSourceDependentCache<ActionSelectionTable<RouteEndpoint>> _cache;
|
||||
private readonly DataSourceDependentCache<ActionSelectionTable<Endpoint>> _cache;
|
||||
|
||||
public DynamicControllerEndpointSelector(ControllerActionEndpointDataSource dataSource)
|
||||
{
|
||||
|
|
@ -22,12 +22,13 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
}
|
||||
|
||||
_dataSource = dataSource;
|
||||
_cache = new DataSourceDependentCache<ActionSelectionTable<RouteEndpoint>>(dataSource, Initialize);
|
||||
|
||||
_cache = new DataSourceDependentCache<ActionSelectionTable<Endpoint>>(dataSource, Initialize);
|
||||
}
|
||||
|
||||
private ActionSelectionTable<RouteEndpoint> Table => _cache.EnsureInitialized();
|
||||
private ActionSelectionTable<Endpoint> Table => _cache.EnsureInitialized();
|
||||
|
||||
public IReadOnlyList<RouteEndpoint> SelectEndpoints(RouteValueDictionary values)
|
||||
public IReadOnlyList<Endpoint> SelectEndpoints(RouteValueDictionary values)
|
||||
{
|
||||
if (values == null)
|
||||
{
|
||||
|
|
@ -38,10 +39,9 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
var matches = table.Select(values);
|
||||
return matches;
|
||||
}
|
||||
|
||||
private static ActionSelectionTable<RouteEndpoint> Initialize(IReadOnlyList<Endpoint> endpoints)
|
||||
private static ActionSelectionTable<Endpoint> Initialize(IReadOnlyList<Endpoint> endpoints)
|
||||
{
|
||||
return ActionSelectionTable<RouteEndpoint>.Create(endpoints);
|
||||
return ActionSelectionTable<Endpoint>.Create(endpoints);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
// 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 Microsoft.AspNetCore.Routing;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Routing
|
||||
{
|
||||
internal class DynamicControllerRouteValueTransformerMetadata : IDynamicEndpointMetadata
|
||||
{
|
||||
public DynamicControllerRouteValueTransformerMetadata(Type selectorType)
|
||||
{
|
||||
if (selectorType == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(selectorType));
|
||||
}
|
||||
|
||||
if (!typeof(DynamicRouteValueTransformer).IsAssignableFrom(selectorType))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"The provided type must be a subclass of {typeof(DynamicRouteValueTransformer)}",
|
||||
nameof(selectorType));
|
||||
}
|
||||
|
||||
SelectorType = selectorType;
|
||||
}
|
||||
|
||||
public bool IsDynamic => true;
|
||||
|
||||
public Type SelectorType { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
// 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.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.Routing.Matching;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Routing
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides an abstraction for dynamically manipulating route value to select a controller action or page.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <see cref="DynamicRouteValueTransformer"/> can be used with
|
||||
/// <see cref="Microsoft.AspNetCore.Builder.ControllerEndpointRouteBuilderExtensions.MapDynamicControllerRoute{TTransformer}(IEndpointRouteBuilder, string)" />
|
||||
/// or <c>MapDynamicPageRoute</c> to implement custom logic that selects a controller action or page.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The route values returned from a <see cref="TransformAsync(HttpContext, RouteValueDictionary)"/> implementation
|
||||
/// will be used to select an action based on matching of the route values. All actions that match the route values
|
||||
/// will be considered as candidates, and may be further disambiguated by <see cref="IEndpointSelectorPolicy" />
|
||||
/// implementations such as <see cref="HttpMethodMatcherPolicy" />.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Implementations <see cref="DynamicRouteValueTransformer" /> should be registered with the service
|
||||
/// collection as type <see cref="DynamicRouteValueTransformer" />. Implementations can use any service
|
||||
/// lifetime.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public abstract class DynamicRouteValueTransformer
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a set of transformed route values that will be used to select an action.
|
||||
/// </summary>
|
||||
/// <param name="httpContext">The <see cref="HttpContext" /> associated with the current request.</param>
|
||||
/// <param name="values">The route values associated with the current match. Implementations should not modify <paramref name="values"/>.</param>
|
||||
/// <returns>A task which asynchronously returns a set of route values.</returns>
|
||||
public abstract Task<RouteValueDictionary> TransformAsync(HttpContext httpContext, RouteValueDictionary values);
|
||||
}
|
||||
}
|
||||
|
|
@ -300,36 +300,6 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
Assert.Single(matches);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Select_Endpoint_NoMatch_ExcludesMatchingSuppressedAction()
|
||||
{
|
||||
var actions = new ActionDescriptor[]
|
||||
{
|
||||
new ActionDescriptor()
|
||||
{
|
||||
DisplayName = "A1",
|
||||
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "controller", "Home" },
|
||||
{ "action", "Index" }
|
||||
},
|
||||
EndpointMetadata = new List<object>()
|
||||
{
|
||||
new SuppressMatchingMetadata(),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var table = CreateTableWithEndpoints(actions);
|
||||
var values = new RouteValueDictionary(new { controller = "Home", action = "Index", });
|
||||
|
||||
// Act
|
||||
var matches = table.Select(values);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(matches);
|
||||
}
|
||||
|
||||
// In this context `CaseSensitiveMatch` means that the input route values exactly match one of the action
|
||||
// descriptor's route values in terms of casing. This is important because we optimize for this case
|
||||
// in the implementation.
|
||||
|
|
@ -584,20 +554,16 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
return ActionSelectionTable<ActionDescriptor>.Create(new ActionDescriptorCollection(actions, 0));
|
||||
}
|
||||
|
||||
private static ActionSelectionTable<RouteEndpoint> CreateTableWithEndpoints(IReadOnlyList<ActionDescriptor> actions)
|
||||
private static ActionSelectionTable<Endpoint> CreateTableWithEndpoints(IReadOnlyList<ActionDescriptor> actions)
|
||||
{
|
||||
|
||||
|
||||
var endpoints = actions.Select(a =>
|
||||
{
|
||||
var metadata = new List<object>(a.EndpointMetadata ?? Array.Empty<object>());
|
||||
metadata.Add(a);
|
||||
return new RouteEndpoint(
|
||||
return new Endpoint(
|
||||
requestDelegate: context => Task.CompletedTask,
|
||||
routePattern: RoutePatternFactory.Parse("/", defaults: a.RouteValues, parameterPolicies: null, requiredValues: a.RouteValues),
|
||||
order: 0,
|
||||
metadata: new EndpointMetadataCollection(metadata),
|
||||
a.DisplayName);
|
||||
displayName: a.DisplayName);
|
||||
});
|
||||
|
||||
return ActionSelectionTable<ActionDescriptor>.Create(endpoints);
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
var action = CreateActionDescriptor(values);
|
||||
var route = CreateRoute(
|
||||
routeName: "Test",
|
||||
pattern: "{controller}/{action}/{page}",
|
||||
pattern: "{controller}/{action}/{page}",
|
||||
defaults: new RouteValueDictionary(new { action = "TestAction" }));
|
||||
|
||||
// Act
|
||||
|
|
@ -144,8 +144,8 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
var values = new { controller = "TestController", action = "TestAction", page = (string)null };
|
||||
var action = CreateActionDescriptor(values);
|
||||
var route = CreateRoute(
|
||||
routeName: "test",
|
||||
pattern: "{controller}/{action}/{id?}",
|
||||
routeName: "test",
|
||||
pattern: "{controller}/{action}/{id?}",
|
||||
defaults: new RouteValueDictionary(new { controller = "TestController", action = "TestAction1" }));
|
||||
|
||||
// Act
|
||||
|
|
@ -166,8 +166,8 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
var values = new { controller = "TestController", action = "TestAction", page = (string)null };
|
||||
var action = CreateActionDescriptor(values);
|
||||
var route = CreateRoute(
|
||||
routeName: "test",
|
||||
pattern: "/Blog/{*slug}",
|
||||
routeName: "test",
|
||||
pattern: "/Blog/{*slug}",
|
||||
defaults: new RouteValueDictionary(new { controller = "TestController", action = "TestAction1" }));
|
||||
|
||||
// Act
|
||||
|
|
@ -252,7 +252,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
var values = new { controller = "TestController", action = "TestAction1", page = (string)null };
|
||||
var action = CreateActionDescriptor(values);
|
||||
var route = CreateRoute(
|
||||
routeName: "test",
|
||||
routeName: "test",
|
||||
pattern: "{controller}/{action}",
|
||||
constraints: new RouteValueDictionary(new { action = "(TestAction1|TestAction2)" }));
|
||||
|
||||
|
|
@ -270,7 +270,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
var values = new { controller = "TestController", action = "TestAction", page = (string)null };
|
||||
var action = CreateActionDescriptor(values);
|
||||
var route = CreateRoute(
|
||||
routeName: "test",
|
||||
routeName: "test",
|
||||
pattern: "{controller}/{action}",
|
||||
constraints: new RouteValueDictionary(new { action = "(TestAction1|TestAction2)" }));
|
||||
|
||||
|
|
@ -316,11 +316,25 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
Assert.Equal(2, matcherEndpoint.Order);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void AddEndpoints_CreatesInertEndpoint()
|
||||
{
|
||||
// Arrange
|
||||
var values = new { controller = "TestController", action = "TestAction", page = (string)null };
|
||||
var action = CreateActionDescriptor(values);
|
||||
|
||||
// Act
|
||||
var endpoints = CreateConventionalRoutedEndpoints(action, Array.Empty<ConventionalRouteEntry>(), createInertEndpoints: true);
|
||||
|
||||
// Assert
|
||||
Assert.IsType<Endpoint>(Assert.Single(endpoints));
|
||||
}
|
||||
|
||||
private RouteEndpoint CreateAttributeRoutedEndpoint(ActionDescriptor action)
|
||||
{
|
||||
var endpoints = new List<Endpoint>();
|
||||
Factory.AddEndpoints(endpoints, new HashSet<string>(StringComparer.OrdinalIgnoreCase), action, Array.Empty<ConventionalRouteEntry>(), Array.Empty<Action<EndpointBuilder>>());
|
||||
Factory.AddEndpoints(endpoints, new HashSet<string>(), action, Array.Empty<ConventionalRouteEntry>(), Array.Empty<Action<EndpointBuilder>>(), createInertEndpoints: false);
|
||||
return Assert.IsType<RouteEndpoint>(Assert.Single(endpoints));
|
||||
}
|
||||
|
||||
|
|
@ -334,7 +348,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
Assert.NotNull(action.RouteValues);
|
||||
|
||||
var endpoints = new List<Endpoint>();
|
||||
Factory.AddEndpoints(endpoints, new HashSet<string>(StringComparer.OrdinalIgnoreCase), action, new[] { route, }, Array.Empty<Action<EndpointBuilder>>());
|
||||
Factory.AddEndpoints(endpoints, new HashSet<string>(), action, new[] { route, }, Array.Empty<Action<EndpointBuilder>>(), createInertEndpoints: false);
|
||||
var endpoint = Assert.IsType<RouteEndpoint>(Assert.Single(endpoints));
|
||||
|
||||
// This should be true for all conventional-routed actions.
|
||||
|
|
@ -343,20 +357,20 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
return endpoint;
|
||||
}
|
||||
|
||||
private IReadOnlyList<RouteEndpoint> CreateConventionalRoutedEndpoints(ActionDescriptor action, ConventionalRouteEntry route)
|
||||
private IReadOnlyList<Endpoint> CreateConventionalRoutedEndpoints(ActionDescriptor action, ConventionalRouteEntry route)
|
||||
{
|
||||
return CreateConventionalRoutedEndpoints(action, new[] { route, });
|
||||
}
|
||||
|
||||
private IReadOnlyList<RouteEndpoint> CreateConventionalRoutedEndpoints(ActionDescriptor action, IReadOnlyList<ConventionalRouteEntry> routes)
|
||||
private IReadOnlyList<Endpoint> CreateConventionalRoutedEndpoints(ActionDescriptor action, IReadOnlyList<ConventionalRouteEntry> routes, bool createInertEndpoints = false)
|
||||
{
|
||||
var endpoints = new List<Endpoint>();
|
||||
Factory.AddEndpoints(endpoints, new HashSet<string>(StringComparer.OrdinalIgnoreCase), action, routes, Array.Empty<Action<EndpointBuilder>>());
|
||||
return endpoints.Cast<RouteEndpoint>().ToList();
|
||||
Factory.AddEndpoints(endpoints, new HashSet<string>(), action, routes, Array.Empty<Action<EndpointBuilder>>(), createInertEndpoints);
|
||||
return endpoints.ToList();
|
||||
}
|
||||
|
||||
private ConventionalRouteEntry CreateRoute(
|
||||
string routeName,
|
||||
string routeName,
|
||||
string pattern,
|
||||
RouteValueDictionary defaults = null,
|
||||
IDictionary<string, object> constraints = null,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ namespace Microsoft.AspNetCore.Builder
|
|||
}
|
||||
public static partial class RazorPagesEndpointRouteBuilderExtensions
|
||||
{
|
||||
public static void MapDynamicPageRoute<TTransformer>(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string pattern) where TTransformer : Microsoft.AspNetCore.Mvc.Routing.DynamicRouteValueTransformer { }
|
||||
public static Microsoft.AspNetCore.Builder.IEndpointConventionBuilder MapFallbackToAreaPage(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string page, string area) { throw null; }
|
||||
public static Microsoft.AspNetCore.Builder.IEndpointConventionBuilder MapFallbackToAreaPage(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string pattern, string page, string area) { throw null; }
|
||||
public static Microsoft.AspNetCore.Builder.IEndpointConventionBuilder MapFallbackToPage(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string page) { throw null; }
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using System.Linq;
|
|||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
|
|
@ -74,7 +75,7 @@ namespace Microsoft.AspNetCore.Builder
|
|||
EnsureRazorPagesServices(endpoints);
|
||||
|
||||
// Called for side-effect to make sure that the data source is registered.
|
||||
GetOrCreateDataSource(endpoints);
|
||||
GetOrCreateDataSource(endpoints).CreateInertEndpoints = true;
|
||||
|
||||
// Maps a fallback endpoint with an empty delegate. This is OK because
|
||||
// we don't expect the delegate to run.
|
||||
|
|
@ -140,7 +141,7 @@ namespace Microsoft.AspNetCore.Builder
|
|||
EnsureRazorPagesServices(endpoints);
|
||||
|
||||
// Called for side-effect to make sure that the data source is registered.
|
||||
GetOrCreateDataSource(endpoints);
|
||||
GetOrCreateDataSource(endpoints).CreateInertEndpoints = true;
|
||||
|
||||
// Maps a fallback endpoint with an empty delegate. This is OK because
|
||||
// we don't expect the delegate to run.
|
||||
|
|
@ -198,7 +199,7 @@ namespace Microsoft.AspNetCore.Builder
|
|||
EnsureRazorPagesServices(endpoints);
|
||||
|
||||
// Called for side-effect to make sure that the data source is registered.
|
||||
GetOrCreateDataSource(endpoints);
|
||||
GetOrCreateDataSource(endpoints).CreateInertEndpoints = true;
|
||||
|
||||
// Maps a fallback endpoint with an empty delegate. This is OK because
|
||||
// we don't expect the delegate to run.
|
||||
|
|
@ -266,7 +267,7 @@ namespace Microsoft.AspNetCore.Builder
|
|||
EnsureRazorPagesServices(endpoints);
|
||||
|
||||
// Called for side-effect to make sure that the data source is registered.
|
||||
GetOrCreateDataSource(endpoints);
|
||||
GetOrCreateDataSource(endpoints).CreateInertEndpoints = true;
|
||||
|
||||
// Maps a fallback endpoint with an empty delegate. This is OK because
|
||||
// we don't expect the delegate to run.
|
||||
|
|
@ -279,6 +280,53 @@ namespace Microsoft.AspNetCore.Builder
|
|||
return builder;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// <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)
|
||||
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.
|
||||
GetOrCreateDataSource(endpoints).CreateInertEndpoints = true;
|
||||
|
||||
endpoints.Map(
|
||||
pattern,
|
||||
context =>
|
||||
{
|
||||
throw new InvalidOperationException("This endpoint is not expected to be executed directly.");
|
||||
})
|
||||
.Add(b =>
|
||||
{
|
||||
b.Metadata.Add(new DynamicPageRouteValueTransformerMetadata(typeof(TTransformer)));
|
||||
});
|
||||
}
|
||||
|
||||
private static DynamicPageMetadata CreateDynamicPageMetadata(string page, string area)
|
||||
{
|
||||
return new DynamicPageMetadata(new RouteValueDictionary()
|
||||
|
|
|
|||
|
|
@ -110,7 +110,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
|||
routeNames: new HashSet<string>(StringComparer.OrdinalIgnoreCase),
|
||||
action: compiled,
|
||||
routes: Array.Empty<ConventionalRouteEntry>(),
|
||||
conventions: Array.Empty<Action<EndpointBuilder>>());
|
||||
conventions: Array.Empty<Action<EndpointBuilder>>(),
|
||||
createInertEndpoints: false);
|
||||
|
||||
// In some test scenarios there's no route so the endpoint isn't created. This is fine because
|
||||
// it won't happen for real.
|
||||
|
|
|
|||
|
|
@ -3,12 +3,13 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.Routing.Matching;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
||||
{
|
||||
|
|
@ -16,8 +17,9 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
|||
{
|
||||
private readonly DynamicPageEndpointSelector _selector;
|
||||
private readonly PageLoader _loader;
|
||||
private readonly EndpointMetadataComparer _comparer;
|
||||
|
||||
public DynamicPageEndpointMatcherPolicy(DynamicPageEndpointSelector selector, PageLoader loader)
|
||||
public DynamicPageEndpointMatcherPolicy(DynamicPageEndpointSelector selector, PageLoader loader, EndpointMetadataComparer comparer)
|
||||
{
|
||||
if (selector == null)
|
||||
{
|
||||
|
|
@ -29,8 +31,14 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
|||
throw new ArgumentNullException(nameof(loader));
|
||||
}
|
||||
|
||||
if (comparer == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(comparer));
|
||||
}
|
||||
|
||||
_selector = selector;
|
||||
_loader = loader;
|
||||
_comparer = comparer;
|
||||
}
|
||||
|
||||
public override int Order => int.MinValue + 100;
|
||||
|
|
@ -50,8 +58,13 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
|||
|
||||
for (var i = 0; i < endpoints.Count; i++)
|
||||
{
|
||||
var metadata = endpoints[i].Metadata.GetMetadata<DynamicPageMetadata>();
|
||||
if (metadata != null)
|
||||
if (endpoints[i].Metadata.GetMetadata<DynamicPageMetadata>() != null)
|
||||
{
|
||||
// Found a dynamic page endpoint
|
||||
return true;
|
||||
}
|
||||
|
||||
if (endpoints[i].Metadata.GetMetadata<DynamicPageRouteValueTransformerMetadata>() != null)
|
||||
{
|
||||
// Found a dynamic page endpoint
|
||||
return true;
|
||||
|
|
@ -84,40 +97,72 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
|||
}
|
||||
|
||||
var endpoint = candidates[i].Endpoint;
|
||||
var originalValues = candidates[i].Values;
|
||||
|
||||
var metadata = endpoint.Metadata.GetMetadata<DynamicPageMetadata>();
|
||||
if (metadata == null)
|
||||
RouteValueDictionary dynamicValues = null;
|
||||
|
||||
// We don't expect both of these to be provided, and they are internal so there's
|
||||
// no realistic way this could happen.
|
||||
var dynamicPageMetadata = endpoint.Metadata.GetMetadata<DynamicPageMetadata>();
|
||||
var transformerMetadata = endpoint.Metadata.GetMetadata<DynamicPageRouteValueTransformerMetadata>();
|
||||
if (dynamicPageMetadata != null)
|
||||
{
|
||||
dynamicValues = dynamicPageMetadata.Values;
|
||||
}
|
||||
else if (transformerMetadata != null)
|
||||
{
|
||||
var transformer = (DynamicRouteValueTransformer)httpContext.RequestServices.GetRequiredService(transformerMetadata.SelectorType);
|
||||
dynamicValues = await transformer.TransformAsync(httpContext, originalValues);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Not a dynamic page
|
||||
continue;
|
||||
}
|
||||
|
||||
var matchedValues = candidates[i].Values;
|
||||
var endpoints = _selector.SelectEndpoints(metadata.Values);
|
||||
if (endpoints.Count == 0)
|
||||
if (dynamicValues == null)
|
||||
{
|
||||
// If there's no match this is a configuration error. We can't really check
|
||||
// during startup that the action you configured exists.
|
||||
throw new InvalidOperationException(
|
||||
"Cannot find the fallback endpoint specified by route values: " +
|
||||
"{ " + string.Join(", ", metadata.Values.Select(kvp => $"{kvp.Key}: {kvp.Value}")) + " }.");
|
||||
candidates.ReplaceEndpoint(i, null, null);
|
||||
continue;
|
||||
}
|
||||
|
||||
// It is possible to have more than one result for pages but they are equivalent.
|
||||
|
||||
var compiled = await _loader.LoadAsync(endpoints[0].Metadata.GetMetadata<PageActionDescriptor>());
|
||||
var replacement = compiled.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
|
||||
// during startup that the action you configured exists, so this is the best we can do.
|
||||
throw new InvalidOperationException(
|
||||
"Cannot find the fallback endpoint specified by route values: " +
|
||||
"{ " + string.Join(", ", dynamicValues.Select(kvp => $"{kvp.Key}: {kvp.Value}")) + " }.");
|
||||
}
|
||||
else if (endpoints.Count == 0)
|
||||
{
|
||||
candidates.ReplaceEndpoint(i, null, null);
|
||||
continue;
|
||||
}
|
||||
|
||||
// We need to provide the route values associated with this endpoint, so that features
|
||||
// like URL generation work.
|
||||
var values = new RouteValueDictionary(metadata.Values);
|
||||
var values = new RouteValueDictionary(dynamicValues);
|
||||
|
||||
// Include values that were matched by the fallback route.
|
||||
foreach (var kvp in matchedValues)
|
||||
foreach (var kvp in originalValues)
|
||||
{
|
||||
values.TryAdd(kvp.Key, kvp.Value);
|
||||
}
|
||||
|
||||
candidates.ReplaceEndpoint(i, replacement, values);
|
||||
// Update the route values
|
||||
candidates.ReplaceEndpoint(i, endpoint, values);
|
||||
|
||||
var loadedEndpoints = new List<Endpoint>(endpoints);
|
||||
for (var j = 0; j < loadedEndpoints.Count; j++)
|
||||
{
|
||||
var compiled = await _loader.LoadAsync(loadedEndpoints[j].Metadata.GetMetadata<PageActionDescriptor>());
|
||||
loadedEndpoints[j] = compiled.Endpoint;
|
||||
}
|
||||
|
||||
// Expand the list of endpoints
|
||||
candidates.ExpandEndpoint(i, loadedEndpoints, _comparer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
|||
internal class DynamicPageEndpointSelector : IDisposable
|
||||
{
|
||||
private readonly PageActionEndpointDataSource _dataSource;
|
||||
private readonly DataSourceDependentCache<ActionSelectionTable<RouteEndpoint>> _cache;
|
||||
private readonly DataSourceDependentCache<ActionSelectionTable<Endpoint>> _cache;
|
||||
|
||||
public DynamicPageEndpointSelector(PageActionEndpointDataSource dataSource)
|
||||
{
|
||||
|
|
@ -22,12 +22,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
|||
}
|
||||
|
||||
_dataSource = dataSource;
|
||||
_cache = new DataSourceDependentCache<ActionSelectionTable<RouteEndpoint>>(dataSource, Initialize);
|
||||
_cache = new DataSourceDependentCache<ActionSelectionTable<Endpoint>>(dataSource, Initialize);
|
||||
}
|
||||
|
||||
private ActionSelectionTable<RouteEndpoint> Table => _cache.EnsureInitialized();
|
||||
private ActionSelectionTable<Endpoint> Table => _cache.EnsureInitialized();
|
||||
|
||||
public IReadOnlyList<RouteEndpoint> SelectEndpoints(RouteValueDictionary values)
|
||||
public IReadOnlyList<Endpoint> SelectEndpoints(RouteValueDictionary values)
|
||||
{
|
||||
if (values == null)
|
||||
{
|
||||
|
|
@ -39,9 +39,9 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
|||
return matches;
|
||||
}
|
||||
|
||||
private static ActionSelectionTable<RouteEndpoint> Initialize(IReadOnlyList<Endpoint> endpoints)
|
||||
private static ActionSelectionTable<Endpoint> Initialize(IReadOnlyList<Endpoint> endpoints)
|
||||
{
|
||||
return ActionSelectionTable<RouteEndpoint>.Create(endpoints);
|
||||
return ActionSelectionTable<Endpoint>.Create(endpoints);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
// 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 Microsoft.AspNetCore.Mvc.Routing;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
||||
{
|
||||
internal class DynamicPageRouteValueTransformerMetadata : IDynamicEndpointMetadata
|
||||
{
|
||||
public DynamicPageRouteValueTransformerMetadata(Type selectorType)
|
||||
{
|
||||
if (selectorType == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(selectorType));
|
||||
}
|
||||
|
||||
if (!typeof(DynamicRouteValueTransformer).IsAssignableFrom(selectorType))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"The provided type must be a subclass of {typeof(DynamicRouteValueTransformer)}",
|
||||
nameof(selectorType));
|
||||
}
|
||||
|
||||
SelectorType = selectorType;
|
||||
}
|
||||
|
||||
public bool IsDynamic => true;
|
||||
|
||||
public Type SelectorType { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -22,13 +22,17 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
|||
|
||||
DefaultBuilder = new PageActionEndpointConventionBuilder(Lock, Conventions);
|
||||
|
||||
// IMPORTANT: this needs to be the last thing we do in the constructor.
|
||||
// IMPORTANT: this needs to be the last thing we do in the constructor.
|
||||
// Change notifications can happen immediately!
|
||||
Subscribe();
|
||||
}
|
||||
|
||||
public PageActionEndpointConventionBuilder DefaultBuilder { get; }
|
||||
|
||||
// Used to control whether we create 'inert' (non-routable) endpoints for use in dynamic
|
||||
// selection. Set to true by builder methods that do dynamic/fallback selection.
|
||||
public bool CreateInertEndpoints { get; set; }
|
||||
|
||||
protected override List<Endpoint> CreateEndpoints(IReadOnlyList<ActionDescriptor> actions, IReadOnlyList<Action<EndpointBuilder>> conventions)
|
||||
{
|
||||
var endpoints = new List<Endpoint>();
|
||||
|
|
@ -37,7 +41,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
|||
{
|
||||
if (actions[i] is PageActionDescriptor action)
|
||||
{
|
||||
_endpointFactory.AddEndpoints(endpoints, routeNames, action, Array.Empty<ConventionalRouteEntry>(), conventions);
|
||||
_endpointFactory.AddEndpoints(endpoints, routeNames, action, Array.Empty<ConventionalRouteEntry>(), conventions, CreateInertEndpoints);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,103 @@
|
|||
// 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.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||
{
|
||||
public class RoutingDynamicTest : IClassFixture<MvcTestFixture<RoutingWebSite.StartupForDynamic>>
|
||||
{
|
||||
public RoutingDynamicTest(MvcTestFixture<RoutingWebSite.StartupForDynamic> fixture)
|
||||
{
|
||||
var factory = fixture.Factories.FirstOrDefault() ?? fixture.WithWebHostBuilder(ConfigureWebHostBuilder);
|
||||
Client = factory.CreateDefaultClient();
|
||||
}
|
||||
|
||||
private static void ConfigureWebHostBuilder(IWebHostBuilder builder) => builder.UseStartup<RoutingWebSite.StartupForDynamic>();
|
||||
|
||||
public HttpClient Client { get; }
|
||||
|
||||
[Fact]
|
||||
public async Task DynamicController_CanGet404ForMissingAction()
|
||||
{
|
||||
// Arrange
|
||||
var url = "http://localhost/dynamic/controller%3DFake,action%3DIndex";
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||
|
||||
// Act
|
||||
var response = await Client.SendAsync(request);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DynamicPage_CanGet404ForMissingAction()
|
||||
{
|
||||
// Arrange
|
||||
var url = "http://localhost/dynamicpage/page%3D%2FFake";
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||
|
||||
// Act
|
||||
var response = await Client.SendAsync(request);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DynamicController_CanSelectControllerInArea()
|
||||
{
|
||||
// Arrange
|
||||
var url = "http://localhost/dynamic/area%3Dadmin,controller%3Ddynamic,action%3Dindex";
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||
|
||||
// Act
|
||||
var response = await Client.SendAsync(request);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal("Hello from dynamic controller: /link_generation/dynamic/index", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DynamicController_CanSelectControllerInArea_WithActionConstraints()
|
||||
{
|
||||
// Arrange
|
||||
var url = "http://localhost/dynamic/area%3Dadmin,controller%3Ddynamic,action%3Dindex";
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, url);
|
||||
|
||||
// Act
|
||||
var response = await Client.SendAsync(request);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal("Hello from dynamic controller POST: /link_generation/dynamic/index", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DynamicPage_CanSelectPage()
|
||||
{
|
||||
// Arrange
|
||||
var url = "http://localhost/dynamicpage/page%3D%2FDynamicPage";
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||
|
||||
// Act
|
||||
var response = await Client.SendAsync(request);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal("Hello from dynamic page: /DynamicPage", content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -66,7 +66,23 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal("Hello from fallback controller: /Admin/Fallback", content);
|
||||
Assert.Equal("Hello from fallback controller: /link_generation/Admin/Fallback/Index", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Fallback_CanFallbackToControllerInArea_WithActionConstraints()
|
||||
{
|
||||
// Arrange
|
||||
var url = "http://localhost/Admin/Foo";
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, url);
|
||||
|
||||
// Act
|
||||
var response = await Client.SendAsync(request);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal("Hello from fallback controller POST: /link_generation/Admin/Fallback/Index", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -82,7 +98,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal("Hello from fallback controller POST: /Admin/Fallback", content);
|
||||
Assert.Equal("Hello from fallback controller POST: /link_generation/Admin/Fallback/Index", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
// 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;
|
||||
|
||||
namespace RoutingWebSite.Areas.Admin
|
||||
{
|
||||
[Area("Admin")]
|
||||
public class DynamicController : Controller
|
||||
{
|
||||
public ActionResult Index()
|
||||
{
|
||||
return Content("Hello from dynamic controller: " + Url.Action());
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public ActionResult Index(int x = 0)
|
||||
{
|
||||
return Content("Hello from dynamic controller POST: " + Url.Action());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@ namespace RoutingWebSite.Areas.Admin
|
|||
}
|
||||
|
||||
[HttpPost]
|
||||
public ActionResult Index(int x)
|
||||
public ActionResult Index(int x = 0)
|
||||
{
|
||||
return Content("Hello from fallback controller POST: " + Url.Action());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
@page
|
||||
@model RoutingWebSite.Pages.DynamicPageModel
|
||||
Hello from dynamic page: @Url.Page("")
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace RoutingWebSite.Pages
|
||||
{
|
||||
public class DynamicPageModel : PageModel
|
||||
{
|
||||
public void OnGet()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
// 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.Routing;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace RoutingWebSite
|
||||
{
|
||||
// For by tests for dynamic routing to pages/controllers
|
||||
public class StartupForDynamic
|
||||
{
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services
|
||||
.AddMvc()
|
||||
.AddNewtonsoftJson()
|
||||
.SetCompatibilityVersion(CompatibilityVersion.Latest);
|
||||
|
||||
services.AddSingleton<Transformer>();
|
||||
|
||||
// Used by some controllers defined in this project.
|
||||
services.Configure<RouteOptions>(options => options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer));
|
||||
}
|
||||
|
||||
public void Configure(IApplicationBuilder app)
|
||||
{
|
||||
app.UseRouting();
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapDynamicControllerRoute<Transformer>("dynamic/{**slug}");
|
||||
endpoints.MapDynamicPageRoute<Transformer>("dynamicpage/{**slug}");
|
||||
|
||||
endpoints.MapControllerRoute("link", "link_generation/{controller}/{action}/{id?}");
|
||||
|
||||
});
|
||||
|
||||
app.Map("/afterrouting", b => b.Run(c =>
|
||||
{
|
||||
return c.Response.WriteAsync("Hello from middleware after routing");
|
||||
}));
|
||||
}
|
||||
|
||||
private class Transformer : DynamicRouteValueTransformer
|
||||
{
|
||||
// Turns a format like `controller=Home,action=Index` into an RVD
|
||||
public override Task<RouteValueDictionary> TransformAsync(HttpContext httpContext, RouteValueDictionary values)
|
||||
{
|
||||
var kvps = ((string)values["slug"]).Split(",");
|
||||
|
||||
var results = new RouteValueDictionary();
|
||||
foreach (var kvp in kvps)
|
||||
{
|
||||
var split = kvp.Split("=");
|
||||
results[split[0]] = split[1];
|
||||
}
|
||||
|
||||
return Task.FromResult(results);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -28,13 +28,10 @@ namespace RoutingWebSite
|
|||
app.UseRouting();
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
// Workaround for #8130
|
||||
//
|
||||
// You can't fallback to this unless it already has another route.
|
||||
endpoints.MapAreaControllerRoute("admin", "Admin", "Admin/{controller=Home}/{action=Index}/{id?}");
|
||||
|
||||
endpoints.MapFallbackToAreaController("admin/{*path:nonfile}", "Index", "Fallback", "Admin");
|
||||
endpoints.MapFallbackToPage("/FallbackPage");
|
||||
|
||||
endpoints.MapControllerRoute("admin", "link_generation/{area}/{controller}/{action}/{id?}");
|
||||
});
|
||||
|
||||
app.Map("/afterrouting", b => b.Run(c =>
|
||||
|
|
|
|||
Loading…
Reference in New Issue