Add fallback routing for controllers and pages
This commit is contained in:
parent
bb28db6fb2
commit
a1ec03e1e6
|
|
@ -4,7 +4,6 @@
|
|||
using System;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.Routing.Patterns;
|
||||
|
||||
namespace Microsoft.AspNetCore.Builder
|
||||
{
|
||||
|
|
|
|||
|
|
@ -9,6 +9,10 @@ namespace Microsoft.AspNetCore.Builder
|
|||
public static void MapControllerRoute(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder routes, string name, string template, object defaults = null, object constraints = null, object dataTokens = null) { }
|
||||
public static Microsoft.AspNetCore.Builder.IEndpointConventionBuilder MapControllers(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder routes) { throw null; }
|
||||
public static Microsoft.AspNetCore.Builder.IEndpointConventionBuilder MapDefaultControllerRoute(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder routes) { throw null; }
|
||||
public static void MapFallbackToAreaController(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder routes, string action, string controller, string area) { }
|
||||
public static void MapFallbackToAreaController(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder routes, string pattern, string action, string controller, string area) { }
|
||||
public static void MapFallbackToController(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder routes, string action, string controller) { }
|
||||
public static void MapFallbackToController(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder routes, string pattern, string action, string controller) { }
|
||||
}
|
||||
public static partial class MvcApplicationBuilderExtensions
|
||||
{
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc.Core;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
|
|
@ -152,6 +153,296 @@ namespace Microsoft.AspNetCore.Builder
|
|||
routes.MapControllerRoute(name, template, defaultsDictionary, constraintsDictionary, dataTokens);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a specialized <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that will match
|
||||
/// requests for non-file-names with the lowest possible priority. The request will be routed to a controller endpoint that
|
||||
/// matches <paramref name="action"/>, and <paramref name="controller"/>.
|
||||
/// </summary>
|
||||
/// <param name="routes">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
|
||||
/// <param name="action">The action name.</param>
|
||||
/// <param name="controller">The controller name.</param>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <see cref="MapFallbackToController(IEndpointRouteBuilder, string, string)"/> is intended to handle cases where URL path of
|
||||
/// the request does not contain a file name, and no other endpoint has matched. This is convenient for routing
|
||||
/// requests for dynamic content to a SPA framework, while also allowing requests for non-existent files to
|
||||
/// result in an HTTP 404.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <see cref="MapFallbackToController(IEndpointRouteBuilder, string, string)"/> registers an endpoint using the pattern
|
||||
/// <c>{*path:nonfile}</c>. The order of the registered endpoint will be <c>int.MaxValue</c>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <see cref="MapFallbackToController(IEndpointRouteBuilder, string, string)"/> does not re-execute routing, and will
|
||||
/// not generate route values based on routes defined elsewhere. When using this overload, the <c>path</c> route value
|
||||
/// will be available.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <see cref="MapFallbackToController(IEndpointRouteBuilder, string, string)"/> does not attempt to disambiguate between
|
||||
/// multiple actions that match the provided <paramref name="action"/> and <paramref name="controller"/>. If multiple
|
||||
/// actions match these values, the result is implementation defined.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static void MapFallbackToController(
|
||||
this IEndpointRouteBuilder routes,
|
||||
string action,
|
||||
string controller)
|
||||
{
|
||||
if (routes == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(routes));
|
||||
}
|
||||
|
||||
if (action == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(action));
|
||||
}
|
||||
|
||||
if (controller == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(controller));
|
||||
}
|
||||
|
||||
EnsureControllerServices(routes);
|
||||
|
||||
// Called for side-effect to make sure that the data source is registered.
|
||||
GetOrCreateDataSource(routes);
|
||||
|
||||
// Maps a fallback endpoint with an empty delegate. This is OK because
|
||||
// we don't expect the delegate to run.
|
||||
routes.MapFallback(context => Task.CompletedTask).Add(b =>
|
||||
{
|
||||
// MVC registers a policy that looks for this metadata.
|
||||
b.Metadata.Add(CreateDynamicControllerMetadata(action, controller, area: null));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a specialized <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that will match
|
||||
/// requests for non-file-names with the lowest possible priority. The request will be routed to a controller endpoint that
|
||||
/// matches <paramref name="action"/>, and <paramref name="controller"/>.
|
||||
/// </summary>
|
||||
/// <param name="routes">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
|
||||
/// <param name="pattern">The route pattern.</param>
|
||||
/// <param name="action">The action name.</param>
|
||||
/// <param name="controller">The controller name.</param>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <see cref="MapFallbackToController(IEndpointRouteBuilder, string, string, string)"/> is intended to handle cases
|
||||
/// where URL path of the request does not contain a file name, and no other endpoint has matched. This is convenient
|
||||
/// for routing requests for dynamic content to a SPA framework, while also allowing requests for non-existent files to
|
||||
/// result in an HTTP 404.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The order of the registered endpoint will be <c>int.MaxValue</c>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This overload will use the provided <paramref name="pattern"/> verbatim. Use the <c>:nonfile</c> route contraint
|
||||
/// to exclude requests for static files.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <see cref="MapFallbackToController(IEndpointRouteBuilder, string, string, string)"/> does not re-execute routing, and will
|
||||
/// not generate route values based on routes defined elsewhere. When using this overload, the route values provided by matching
|
||||
/// <paramref name="pattern"/> will be available.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <see cref="MapFallbackToController(IEndpointRouteBuilder, string, string, string)"/> does not attempt to disambiguate between
|
||||
/// multiple actions that match the provided <paramref name="action"/> and <paramref name="controller"/>. If multiple
|
||||
/// actions match these values, the result is implementation defined.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static void MapFallbackToController(
|
||||
this IEndpointRouteBuilder routes,
|
||||
string pattern,
|
||||
string action,
|
||||
string controller)
|
||||
{
|
||||
if (routes == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(routes));
|
||||
}
|
||||
|
||||
if (pattern == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(pattern));
|
||||
}
|
||||
|
||||
if (action == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(action));
|
||||
}
|
||||
|
||||
if (controller == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(controller));
|
||||
}
|
||||
|
||||
EnsureControllerServices(routes);
|
||||
|
||||
// Called for side-effect to make sure that the data source is registered.
|
||||
GetOrCreateDataSource(routes);
|
||||
|
||||
// Maps a fallback endpoint with an empty delegate. This is OK because
|
||||
// we don't expect the delegate to run.
|
||||
routes.MapFallback(pattern, context => Task.CompletedTask).Add(b =>
|
||||
{
|
||||
// MVC registers a policy that looks for this metadata.
|
||||
b.Metadata.Add(CreateDynamicControllerMetadata(action, controller, area: null));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a specialized <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that will match
|
||||
/// requests for non-file-names with the lowest possible priority. The request will be routed to a controller endpoint that
|
||||
/// matches <paramref name="action"/>, <paramref name="controller"/>, and <paramref name="area"/>.
|
||||
/// </summary>
|
||||
/// <param name="routes">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
|
||||
/// <param name="action">The action name.</param>
|
||||
/// <param name="controller">The controller name.</param>
|
||||
/// <param name="area">The area name.</param>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <see cref="MapFallbackToAreaController(IEndpointRouteBuilder, string, string, string)"/> is intended to handle cases where URL path of
|
||||
/// the request does not contain a file name, and no other endpoint has matched. This is convenient for routing
|
||||
/// requests for dynamic content to a SPA framework, while also allowing requests for non-existent files to
|
||||
/// result in an HTTP 404.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <see cref="MapFallbackToAreaController(IEndpointRouteBuilder, string, string, string)"/> registers an endpoint using the pattern
|
||||
/// <c>{*path:nonfile}</c>. The order of the registered endpoint will be <c>int.MaxValue</c>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <see cref="MapFallbackToAreaController(IEndpointRouteBuilder, string, string, string)"/> does not re-execute routing, and will
|
||||
/// not generate route values based on routes defined elsewhere. When using this overload, the <c>path</c> route value
|
||||
/// will be available.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <see cref="MapFallbackToAreaController(IEndpointRouteBuilder, string, string, string)"/> does not attempt to disambiguate between
|
||||
/// multiple actions that match the provided <paramref name="action"/>, <paramref name="controller"/>, and <paramref name="area"/>. If multiple
|
||||
/// actions match these values, the result is implementation defined.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static void MapFallbackToAreaController(
|
||||
this IEndpointRouteBuilder routes,
|
||||
string action,
|
||||
string controller,
|
||||
string area)
|
||||
{
|
||||
if (routes == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(routes));
|
||||
}
|
||||
|
||||
if (action == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(action));
|
||||
}
|
||||
|
||||
if (controller == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(controller));
|
||||
}
|
||||
|
||||
EnsureControllerServices(routes);
|
||||
|
||||
// Called for side-effect to make sure that the data source is registered.
|
||||
GetOrCreateDataSource(routes);
|
||||
|
||||
// Maps a fallback endpoint with an empty delegate. This is OK because
|
||||
// we don't expect the delegate to run.
|
||||
routes.MapFallback(context => Task.CompletedTask).Add(b =>
|
||||
{
|
||||
// MVC registers a policy that looks for this metadata.
|
||||
b.Metadata.Add(CreateDynamicControllerMetadata(action, controller, area));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a specialized <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that will match
|
||||
/// requests for non-file-names with the lowest possible priority. The request will be routed to a controller endpoint that
|
||||
/// matches <paramref name="action"/>, <paramref name="controller"/>, and <paramref name="area"/>.
|
||||
/// </summary>
|
||||
/// <param name="routes">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
|
||||
/// <param name="pattern">The route pattern.</param>
|
||||
/// <param name="action">The action name.</param>
|
||||
/// <param name="controller">The controller name.</param>
|
||||
/// <param name="area">The area name.</param>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <see cref="MapFallbackToAreaController(IEndpointRouteBuilder, string, string, string, string)"/> is intended to handle
|
||||
/// cases where URL path of the request does not contain a file name, and no other endpoint has matched. This is
|
||||
/// convenient for routing requests for dynamic content to a SPA framework, while also allowing requests for non-existent files to
|
||||
/// result in an HTTP 404.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The order of the registered endpoint will be <c>int.MaxValue</c>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This overload will use the provided <paramref name="pattern"/> verbatim. Use the <c>:nonfile</c> route contraint
|
||||
/// to exclude requests for static files.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <see cref="MapFallbackToAreaController(IEndpointRouteBuilder, string, string, string, string)"/> does not re-execute routing, and will
|
||||
/// not generate route values based on routes defined elsewhere. When using this overload, the route values provided by matching
|
||||
/// <paramref name="pattern"/> will be available.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <see cref="MapFallbackToAreaController(IEndpointRouteBuilder, string, string, string, string)"/> does not attempt to disambiguate between
|
||||
/// multiple actions that match the provided <paramref name="action"/>, <paramref name="controller"/>, and <paramref name="area"/>. If multiple
|
||||
/// actions match these values, the result is implementation defined.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static void MapFallbackToAreaController(
|
||||
this IEndpointRouteBuilder routes,
|
||||
string pattern,
|
||||
string action,
|
||||
string controller,
|
||||
string area)
|
||||
{
|
||||
if (routes == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(routes));
|
||||
}
|
||||
|
||||
if (pattern == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(pattern));
|
||||
}
|
||||
|
||||
if (action == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(action));
|
||||
}
|
||||
|
||||
if (controller == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(controller));
|
||||
}
|
||||
|
||||
EnsureControllerServices(routes);
|
||||
|
||||
// Called for side-effect to make sure that the data source is registered.
|
||||
GetOrCreateDataSource(routes);
|
||||
|
||||
// Maps a fallback endpoint with an empty delegate. This is OK because
|
||||
// we don't expect the delegate to run.
|
||||
routes.MapFallback(pattern, context => Task.CompletedTask).Add(b =>
|
||||
{
|
||||
// MVC registers a policy that looks for this metadata.
|
||||
b.Metadata.Add(CreateDynamicControllerMetadata(action, controller, area));
|
||||
});
|
||||
}
|
||||
|
||||
private static DynamicControllerMetadata CreateDynamicControllerMetadata(string action, string controller, string area)
|
||||
{
|
||||
return new DynamicControllerMetadata(new RouteValueDictionary()
|
||||
{
|
||||
{ "action", action },
|
||||
{ "controller", controller },
|
||||
{ "area", area }
|
||||
});
|
||||
}
|
||||
|
||||
private static void EnsureControllerServices(IEndpointRouteBuilder routes)
|
||||
{
|
||||
var marker = routes.ServiceProvider.GetService<MvcMarkerService>();
|
||||
|
|
|
|||
|
|
@ -272,6 +272,8 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
services.TryAddSingleton<ActionEndpointDataSource>();
|
||||
services.TryAddSingleton<ControllerActionEndpointDataSource>();
|
||||
services.TryAddSingleton<ActionEndpointFactory>();
|
||||
services.TryAddSingleton<DynamicControllerEndpointSelector>();
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, DynamicControllerEndpointMatcherPolicy>());
|
||||
|
||||
//
|
||||
// Middleware pipeline filter related
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
// Only include RouteEndpoints and only those that aren't suppressed.
|
||||
items: endpoints.OfType<RouteEndpoint>().Where(e =>
|
||||
{
|
||||
return e.Metadata.GetMetadata<ISuppressMatchingMetadata>().SuppressMatching != true;
|
||||
return e.Metadata.GetMetadata<ISuppressMatchingMetadata>()?.SuppressMatching != true;
|
||||
}),
|
||||
|
||||
getRouteKeys: e => e.RoutePattern.RequiredValues.Keys,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.Internal;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||
{
|
||||
|
|
@ -60,20 +59,14 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
return 0;
|
||||
}
|
||||
|
||||
var hash = new HashCodeCombiner();
|
||||
var hash = new HashCode();
|
||||
for (var i = 0; i < obj.Length; i++)
|
||||
{
|
||||
var o = obj[i];
|
||||
|
||||
// Route values define null and "" to be equivalent.
|
||||
if (string.IsNullOrEmpty(o))
|
||||
{
|
||||
o = null;
|
||||
}
|
||||
hash.Add(o, _valueComparer);
|
||||
hash.Add(obj[i] ?? string.Empty, _valueComparer);
|
||||
}
|
||||
|
||||
return hash.CombinedHash;
|
||||
return hash.ToHashCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,120 @@
|
|||
// 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.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.Routing.Matching;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Routing
|
||||
{
|
||||
internal class DynamicControllerEndpointMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy
|
||||
{
|
||||
private readonly DynamicControllerEndpointSelector _selector;
|
||||
|
||||
public DynamicControllerEndpointMatcherPolicy(DynamicControllerEndpointSelector selector)
|
||||
{
|
||||
if (selector == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(selector));
|
||||
}
|
||||
|
||||
_selector = selector;
|
||||
}
|
||||
|
||||
public override int Order => int.MinValue + 100;
|
||||
|
||||
public bool AppliesToEndpoints(IReadOnlyList<Endpoint> endpoints)
|
||||
{
|
||||
if (endpoints == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(endpoints));
|
||||
}
|
||||
|
||||
if (!ContainsDynamicEndpoints(endpoints))
|
||||
{
|
||||
// Dynamic controller endpoints are always dynamic endpoints.
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < endpoints.Count; i++)
|
||||
{
|
||||
var metadata = endpoints[i].Metadata.GetMetadata<DynamicControllerMetadata>();
|
||||
if (metadata != null)
|
||||
{
|
||||
// Found a dynamic controller endpoint
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public Task ApplyAsync(HttpContext httpContext, EndpointSelectorContext context, CandidateSet candidates)
|
||||
{
|
||||
if (httpContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(httpContext));
|
||||
}
|
||||
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (candidates == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(candidates));
|
||||
}
|
||||
|
||||
// There's no real benefit here from trying to avoid the async state machine.
|
||||
// We only execute on nodes that contain a dynamic policy, and thus always have
|
||||
// to await something.
|
||||
for (var i = 0; i < candidates.Count; i++)
|
||||
{
|
||||
if (!candidates.IsValidCandidate(i))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var endpoint = candidates[i].Endpoint;
|
||||
|
||||
var metadata = endpoint.Metadata.GetMetadata<DynamicControllerMetadata>();
|
||||
if (metadata == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var matchedValues = candidates[i].Values;
|
||||
var endpoints = _selector.SelectEndpoints(metadata.Values);
|
||||
if (endpoints.Count == 0)
|
||||
{
|
||||
// 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}")) + " }.");
|
||||
}
|
||||
|
||||
var replacement = endpoints[0];
|
||||
|
||||
// We need to provide the route values associated with this endpoint, so that features
|
||||
// like URL generation work.
|
||||
var values = new RouteValueDictionary(metadata.Values);
|
||||
|
||||
// Include values that were matched by the fallback route.
|
||||
foreach (var kvp in matchedValues)
|
||||
{
|
||||
values.TryAdd(kvp.Key, kvp.Value);
|
||||
}
|
||||
|
||||
candidates.ReplaceEndpoint(i, replacement, values);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
// 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.Generic;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Routing
|
||||
{
|
||||
internal class DynamicControllerEndpointSelector : IDisposable
|
||||
{
|
||||
private readonly ControllerActionEndpointDataSource _dataSource;
|
||||
private readonly DataSourceDependentCache<ActionSelectionTable<RouteEndpoint>> _cache;
|
||||
|
||||
public DynamicControllerEndpointSelector(ControllerActionEndpointDataSource dataSource)
|
||||
{
|
||||
if (dataSource == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(dataSource));
|
||||
}
|
||||
|
||||
_dataSource = dataSource;
|
||||
_cache = new DataSourceDependentCache<ActionSelectionTable<RouteEndpoint>>(dataSource, Initialize);
|
||||
}
|
||||
|
||||
private ActionSelectionTable<RouteEndpoint> Table => _cache.EnsureInitialized();
|
||||
|
||||
public IReadOnlyList<RouteEndpoint> SelectEndpoints(RouteValueDictionary values)
|
||||
{
|
||||
if (values == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(values));
|
||||
}
|
||||
|
||||
var table = Table;
|
||||
var matches = table.Select(values);
|
||||
return matches;
|
||||
}
|
||||
|
||||
private static ActionSelectionTable<RouteEndpoint> Initialize(IReadOnlyList<Endpoint> endpoints)
|
||||
{
|
||||
return ActionSelectionTable<RouteEndpoint>.Create(endpoints);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cache.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
// 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 DynamicControllerMetadata : IDynamicEndpointMetadata
|
||||
{
|
||||
public DynamicControllerMetadata(RouteValueDictionary values)
|
||||
{
|
||||
if (values == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(values));
|
||||
}
|
||||
|
||||
Values = values;
|
||||
}
|
||||
|
||||
public bool IsDynamic => true;
|
||||
|
||||
public RouteValueDictionary Values { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -327,6 +327,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
{
|
||||
typeof(ConsumesMatcherPolicy),
|
||||
typeof(ActionConstraintMatcherPolicy),
|
||||
typeof(DynamicControllerEndpointMatcherPolicy),
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@ namespace Microsoft.AspNetCore.Builder
|
|||
{
|
||||
public static partial class RazorPagesEndpointRouteBuilderExtensions
|
||||
{
|
||||
public static void MapFallbackToAreaPage(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder routes, string page, string area) { }
|
||||
public static void MapFallbackToAreaPage(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder routes, string pattern, string page, string area) { }
|
||||
public static void MapFallbackToPage(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder routes, string page) { }
|
||||
public static void MapFallbackToPage(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder routes, string pattern, string page) { }
|
||||
public static Microsoft.AspNetCore.Builder.IEndpointConventionBuilder MapRazorPages(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder routes) { throw null; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -261,7 +261,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
|||
}
|
||||
|
||||
// Internal for unit testing
|
||||
internal static void EnsureValidPageName(string pageName)
|
||||
internal static void EnsureValidPageName(string pageName, string argumentName = "pageName")
|
||||
{
|
||||
if (string.IsNullOrEmpty(pageName))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
|
@ -31,6 +33,253 @@ namespace Microsoft.AspNetCore.Builder
|
|||
return GetOrCreateDataSource(routes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a specialized <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that will match
|
||||
/// requests for non-file-names with the lowest possible priority. The request will be routed to a page endpoint that
|
||||
/// matches <paramref name="page"/>.
|
||||
/// </summary>
|
||||
/// <param name="routes">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
|
||||
/// <param name="page">The page name.</param>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <see cref="MapFallbackToPage(IEndpointRouteBuilder, string)"/> is intended to handle cases where URL path of
|
||||
/// the request does not contain a file name, and no other endpoint has matched. This is convenient for routing
|
||||
/// requests for dynamic content to a SPA framework, while also allowing requests for non-existent files to
|
||||
/// result in an HTTP 404.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <see cref="MapFallbackToPage(IEndpointRouteBuilder, string)"/> registers an endpoint using the pattern
|
||||
/// <c>{*path:nonfile}</c>. The order of the registered endpoint will be <c>int.MaxValue</c>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <see cref="MapFallbackToPage(IEndpointRouteBuilder, string)"/> does not re-execute routing, and will
|
||||
/// not generate route values based on routes defined elsewhere. When using this overload, the <c>path</c> route value
|
||||
/// will be available.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static void MapFallbackToPage(this IEndpointRouteBuilder routes, string page)
|
||||
{
|
||||
if (routes == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(routes));
|
||||
}
|
||||
|
||||
if (page == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(page));
|
||||
}
|
||||
|
||||
PageConventionCollection.EnsureValidPageName(page, nameof(page));
|
||||
|
||||
EnsureRazorPagesServices(routes);
|
||||
|
||||
// Called for side-effect to make sure that the data source is registered.
|
||||
GetOrCreateDataSource(routes);
|
||||
|
||||
// Maps a fallback endpoint with an empty delegate. This is OK because
|
||||
// we don't expect the delegate to run.
|
||||
routes.MapFallback(context => Task.CompletedTask).Add(b =>
|
||||
{
|
||||
// MVC registers a policy that looks for this metadata.
|
||||
b.Metadata.Add(CreateDynamicPageMetadata(page, area: null));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a specialized <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that will match
|
||||
/// requests for non-file-names with the lowest possible priority. The request will be routed to a page endpoint that
|
||||
/// matches <paramref name="page"/>.
|
||||
/// </summary>
|
||||
/// <param name="routes">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
|
||||
/// <param name="pattern">The route pattern.</param>
|
||||
/// <param name="page">The action name.</param>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <see cref="MapFallbackToPage(IEndpointRouteBuilder, string, string)"/> is intended to handle cases where URL path of
|
||||
/// the request does not contain a file name, and no other endpoint has matched. This is convenient for routing
|
||||
/// requests for dynamic content to a SPA framework, while also allowing requests for non-existent files to
|
||||
/// result in an HTTP 404.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The order of the registered endpoint will be <c>int.MaxValue</c>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This overload will use the provided <paramref name="pattern"/> verbatim. Use the <c>:nonfile</c> route contraint
|
||||
/// to exclude requests for static files.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <see cref="MapFallbackToPage(IEndpointRouteBuilder, string, string)"/> does not re-execute routing, and will
|
||||
/// not generate route values based on routes defined elsewhere. When using this overload, the route values provided by matching
|
||||
/// <paramref name="pattern"/> will be available.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static void MapFallbackToPage(
|
||||
this IEndpointRouteBuilder routes,
|
||||
string pattern,
|
||||
string page)
|
||||
{
|
||||
if (routes == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(routes));
|
||||
}
|
||||
|
||||
if (pattern == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(pattern));
|
||||
}
|
||||
|
||||
if (page == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(page));
|
||||
}
|
||||
|
||||
PageConventionCollection.EnsureValidPageName(page, nameof(page));
|
||||
|
||||
EnsureRazorPagesServices(routes);
|
||||
|
||||
// Called for side-effect to make sure that the data source is registered.
|
||||
GetOrCreateDataSource(routes);
|
||||
|
||||
// Maps a fallback endpoint with an empty delegate. This is OK because
|
||||
// we don't expect the delegate to run.
|
||||
routes.MapFallback(pattern, context => Task.CompletedTask).Add(b =>
|
||||
{
|
||||
// MVC registers a policy that looks for this metadata.
|
||||
b.Metadata.Add(CreateDynamicPageMetadata(page, area: null));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a specialized <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that will match
|
||||
/// requests for non-file-names with the lowest possible priority. The request will be routed to a page endpoint that
|
||||
/// matches <paramref name="page"/>, and <paramref name="area"/>.
|
||||
/// </summary>
|
||||
/// <param name="routes">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
|
||||
/// <param name="page">The action name.</param>
|
||||
/// <param name="area">The area name.</param>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <see cref="MapFallbackToAreaPage(IEndpointRouteBuilder, string, string)"/> is intended to handle cases where URL path of
|
||||
/// the request does not contain a file name, and no other endpoint has matched. This is convenient for routing
|
||||
/// requests for dynamic content to a SPA framework, while also allowing requests for non-existent files to
|
||||
/// result in an HTTP 404.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <see cref="MapFallbackToAreaPage(IEndpointRouteBuilder, string, string)"/> registers an endpoint using the pattern
|
||||
/// <c>{*path:nonfile}</c>. The order of the registered endpoint will be <c>int.MaxValue</c>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <see cref="MapFallbackToAreaPage(IEndpointRouteBuilder, string, string)"/> does not re-execute routing, and will
|
||||
/// not generate route values based on routes defined elsewhere. When using this overload, the <c>path</c> route value
|
||||
/// will be available.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static void MapFallbackToAreaPage(
|
||||
this IEndpointRouteBuilder routes,
|
||||
string page,
|
||||
string area)
|
||||
{
|
||||
if (routes == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(routes));
|
||||
}
|
||||
|
||||
if (page == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(page));
|
||||
}
|
||||
|
||||
PageConventionCollection.EnsureValidPageName(page, nameof(page));
|
||||
|
||||
EnsureRazorPagesServices(routes);
|
||||
|
||||
// Called for side-effect to make sure that the data source is registered.
|
||||
GetOrCreateDataSource(routes);
|
||||
|
||||
// Maps a fallback endpoint with an empty delegate. This is OK because
|
||||
// we don't expect the delegate to run.
|
||||
routes.MapFallback(context => Task.CompletedTask).Add(b =>
|
||||
{
|
||||
// MVC registers a policy that looks for this metadata.
|
||||
b.Metadata.Add(CreateDynamicPageMetadata(page, area));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a specialized <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that will match
|
||||
/// requests for non-file-names with the lowest possible priority. The request will be routed to a page endpoint that
|
||||
/// matches <paramref name="page"/>, and <paramref name="area"/>.
|
||||
/// </summary>
|
||||
/// <param name="routes">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
|
||||
/// <param name="pattern">The route pattern.</param>
|
||||
/// <param name="page">The action name.</param>
|
||||
/// <param name="area">The area name.</param>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <see cref="MapFallbackToAreaPage(IEndpointRouteBuilder, string, string, string)"/> is intended to handle cases where URL path of
|
||||
/// the request does not contain a file name, and no other endpoint has matched. This is convenient for routing
|
||||
/// requests for dynamic content to a SPA framework, while also allowing requests for non-existent files to
|
||||
/// result in an HTTP 404.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The order of the registered endpoint will be <c>int.MaxValue</c>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This overload will use the provided <paramref name="pattern"/> verbatim. Use the <c>:nonfile</c> route contraint
|
||||
/// to exclude requests for static files.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <see cref="MapFallbackToAreaPage(IEndpointRouteBuilder, string, string, string)"/> does not re-execute routing, and will
|
||||
/// not generate route values based on routes defined elsewhere. When using this overload, the route values provided by matching
|
||||
/// <paramref name="pattern"/> will be available.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static void MapFallbackToAreaPage(
|
||||
this IEndpointRouteBuilder routes,
|
||||
string pattern,
|
||||
string page,
|
||||
string area)
|
||||
{
|
||||
if (routes == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(routes));
|
||||
}
|
||||
|
||||
if (pattern == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(pattern));
|
||||
}
|
||||
|
||||
if (page == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(page));
|
||||
}
|
||||
|
||||
PageConventionCollection.EnsureValidPageName(page, nameof(page));
|
||||
|
||||
EnsureRazorPagesServices(routes);
|
||||
|
||||
// Called for side-effect to make sure that the data source is registered.
|
||||
GetOrCreateDataSource(routes);
|
||||
|
||||
// Maps a fallback endpoint with an empty delegate. This is OK because
|
||||
// we don't expect the delegate to run.
|
||||
routes.MapFallback(pattern, context => Task.CompletedTask).Add(b =>
|
||||
{
|
||||
// MVC registers a policy that looks for this metadata.
|
||||
b.Metadata.Add(CreateDynamicPageMetadata(page, area));
|
||||
});
|
||||
}
|
||||
|
||||
private static DynamicPageMetadata CreateDynamicPageMetadata(string page, string area)
|
||||
{
|
||||
return new DynamicPageMetadata(new RouteValueDictionary()
|
||||
{
|
||||
{ "page", page },
|
||||
{ "area", area }
|
||||
});
|
||||
}
|
||||
|
||||
private static void EnsureRazorPagesServices(IEndpointRouteBuilder routes)
|
||||
{
|
||||
var marker = routes.ServiceProvider.GetService<PageActionEndpointDataSource>();
|
||||
|
|
|
|||
|
|
@ -85,6 +85,8 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
|
||||
// Routing
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, PageLoaderMatcherPolicy>());
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, DynamicPageEndpointMatcherPolicy>());
|
||||
services.TryAddSingleton<DynamicPageEndpointSelector>();
|
||||
|
||||
// Action description and invocation
|
||||
services.TryAddEnumerable(
|
||||
|
|
@ -92,6 +94,8 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
services.TryAddEnumerable(
|
||||
ServiceDescriptor.Singleton<IPageRouteModelProvider, CompiledPageRouteModelProvider>());
|
||||
services.TryAddSingleton<PageActionEndpointDataSource>();
|
||||
services.TryAddSingleton<DynamicPageEndpointSelector>();
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, DynamicPageEndpointMatcherPolicy>());
|
||||
|
||||
services.TryAddEnumerable(
|
||||
ServiceDescriptor.Singleton<IPageApplicationModelProvider, DefaultPageApplicationModelProvider>());
|
||||
|
|
|
|||
|
|
@ -0,0 +1,126 @@
|
|||
// 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.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.Routing.Matching;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
||||
{
|
||||
internal class DynamicPageEndpointMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy
|
||||
{
|
||||
private readonly DynamicPageEndpointSelector _selector;
|
||||
private readonly PageLoader _loader;
|
||||
|
||||
public DynamicPageEndpointMatcherPolicy(DynamicPageEndpointSelector selector, PageLoader loader)
|
||||
{
|
||||
if (selector == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(selector));
|
||||
}
|
||||
|
||||
if (loader == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(loader));
|
||||
}
|
||||
|
||||
_selector = selector;
|
||||
_loader = loader;
|
||||
}
|
||||
|
||||
public override int Order => int.MinValue + 100;
|
||||
|
||||
public bool AppliesToEndpoints(IReadOnlyList<Endpoint> endpoints)
|
||||
{
|
||||
if (endpoints == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(endpoints));
|
||||
}
|
||||
|
||||
if (!ContainsDynamicEndpoints(endpoints))
|
||||
{
|
||||
// Dynamic page endpoints are always dynamic endpoints.
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < endpoints.Count; i++)
|
||||
{
|
||||
var metadata = endpoints[i].Metadata.GetMetadata<DynamicPageMetadata>();
|
||||
if (metadata != null)
|
||||
{
|
||||
// Found a dynamic page endpoint
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task ApplyAsync(HttpContext httpContext, EndpointSelectorContext context, CandidateSet candidates)
|
||||
{
|
||||
if (httpContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(httpContext));
|
||||
}
|
||||
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (candidates == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(candidates));
|
||||
}
|
||||
|
||||
// There's no real benefit here from trying to avoid the async state machine.
|
||||
// We only execute on nodes that contain a dynamic policy, and thus always have
|
||||
// to await something.
|
||||
for (var i = 0; i < candidates.Count; i++)
|
||||
{
|
||||
if (!candidates.IsValidCandidate(i))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var endpoint = candidates[i].Endpoint;
|
||||
|
||||
var metadata = endpoint.Metadata.GetMetadata<DynamicPageMetadata>();
|
||||
if (metadata == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var matchedValues = candidates[i].Values;
|
||||
var endpoints = _selector.SelectEndpoints(metadata.Values);
|
||||
if (endpoints.Count == 0)
|
||||
{
|
||||
// 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}")) + " }.");
|
||||
}
|
||||
|
||||
var compiled = await _loader.LoadAsync(endpoints[0].Metadata.GetMetadata<PageActionDescriptor>());
|
||||
var replacement = compiled.Endpoint;
|
||||
|
||||
// We need to provide the route values associated with this endpoint, so that features
|
||||
// like URL generation work.
|
||||
var values = new RouteValueDictionary(metadata.Values);
|
||||
|
||||
// Include values that were matched by the fallback route.
|
||||
foreach (var kvp in matchedValues)
|
||||
{
|
||||
values.TryAdd(kvp.Key, kvp.Value);
|
||||
}
|
||||
|
||||
candidates.ReplaceEndpoint(i, replacement, values);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
// 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.Generic;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
||||
{
|
||||
internal class DynamicPageEndpointSelector : IDisposable
|
||||
{
|
||||
private readonly PageActionEndpointDataSource _dataSource;
|
||||
private readonly DataSourceDependentCache<ActionSelectionTable<RouteEndpoint>> _cache;
|
||||
|
||||
public DynamicPageEndpointSelector(PageActionEndpointDataSource dataSource)
|
||||
{
|
||||
if (dataSource == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(dataSource));
|
||||
}
|
||||
|
||||
_dataSource = dataSource;
|
||||
_cache = new DataSourceDependentCache<ActionSelectionTable<RouteEndpoint>>(dataSource, Initialize);
|
||||
}
|
||||
|
||||
private ActionSelectionTable<RouteEndpoint> Table => _cache.EnsureInitialized();
|
||||
|
||||
public IReadOnlyList<RouteEndpoint> SelectEndpoints(RouteValueDictionary values)
|
||||
{
|
||||
if (values == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(values));
|
||||
}
|
||||
|
||||
var table = Table;
|
||||
var matches = table.Select(values);
|
||||
return matches;
|
||||
}
|
||||
|
||||
private static ActionSelectionTable<RouteEndpoint> Initialize(IReadOnlyList<Endpoint> endpoints)
|
||||
{
|
||||
return ActionSelectionTable<RouteEndpoint>.Create(endpoints);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cache.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
// 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.RazorPages.Infrastructure
|
||||
{
|
||||
internal class DynamicPageMetadata : IDynamicEndpointMetadata
|
||||
{
|
||||
public DynamicPageMetadata(RouteValueDictionary values)
|
||||
{
|
||||
if (values == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(values));
|
||||
}
|
||||
|
||||
Values = values;
|
||||
}
|
||||
|
||||
public bool IsDynamic => true;
|
||||
|
||||
public RouteValueDictionary Values { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -68,6 +68,8 @@ namespace MvcSandbox
|
|||
|
||||
builder.MapControllers();
|
||||
builder.MapRazorPages();
|
||||
|
||||
builder.MapFallbackToController("Index", "Home");
|
||||
});
|
||||
|
||||
app.UseDeveloperExceptionPage();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
// 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 RoutingFallbackTest : IClassFixture<MvcTestFixture<RoutingWebSite.StartupForFallback>>
|
||||
{
|
||||
public RoutingFallbackTest(MvcTestFixture<RoutingWebSite.StartupForFallback> fixture)
|
||||
{
|
||||
var factory = fixture.Factories.FirstOrDefault() ?? fixture.WithWebHostBuilder(ConfigureWebHostBuilder);
|
||||
Client = factory.CreateDefaultClient();
|
||||
}
|
||||
|
||||
private static void ConfigureWebHostBuilder(IWebHostBuilder builder) => builder.UseStartup<RoutingWebSite.StartupForFallback>();
|
||||
|
||||
public HttpClient Client { get; }
|
||||
|
||||
[Fact]
|
||||
public async Task Fallback_CanGet404ForMissingFile()
|
||||
{
|
||||
// Arrange
|
||||
var url = "http://localhost/pranav.jpg";
|
||||
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 Fallback_CanAccessKnownEndpoint()
|
||||
{
|
||||
// Arrange
|
||||
var url = "http://localhost/Edit/17";
|
||||
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 Edit page", content.Trim());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Fallback_CanFallbackToControllerInArea()
|
||||
{
|
||||
// Arrange
|
||||
var url = "http://localhost/Admin/Foo";
|
||||
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 fallback controller: /Admin/Fallback", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Fallback_CanFallbackToPage()
|
||||
{
|
||||
// Arrange
|
||||
var url = "http://localhost/Foo";
|
||||
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 fallback page: /FallbackPage", content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
// 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 FallbackController : Controller
|
||||
{
|
||||
public ActionResult Index()
|
||||
{
|
||||
return Content("Hello from fallback controller: " + Url.Action());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
@page
|
||||
@model RoutingWebSite.Pages.FallbackPageModel
|
||||
Hello from fallback 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 FallbackPageModel : PageModel
|
||||
{
|
||||
public void OnGet()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
// 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.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace RoutingWebSite
|
||||
{
|
||||
// For by tests for fallback routing to pages/controllers
|
||||
public class StartupForFallback
|
||||
{
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services
|
||||
.AddMvc()
|
||||
.AddNewtonsoftJson()
|
||||
.SetCompatibilityVersion(CompatibilityVersion.Latest);
|
||||
|
||||
// Used by some controllers defined in this project.
|
||||
services.Configure<RouteOptions>(options => options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer));
|
||||
}
|
||||
|
||||
public void Configure(IApplicationBuilder app)
|
||||
{
|
||||
app.UseRouting(routes =>
|
||||
{
|
||||
// Workaround for #8130
|
||||
//
|
||||
// You can't fallback to this unless it already has another route.
|
||||
routes.MapAreaControllerRoute("admin", "Admin", "Admin/{controller=Home}/{action=Index}/{id?}");
|
||||
|
||||
routes.MapFallbackToAreaController("admin/{*path:nonfile}", "Index", "Fallback", "Admin");
|
||||
routes.MapFallbackToPage("/FallbackPage");
|
||||
});
|
||||
|
||||
app.Map("/afterrouting", b => b.Run(c =>
|
||||
{
|
||||
return c.Response.WriteAsync("Hello from middleware after routing");
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue