MVC reaction to RoutePattern required values
This commit is contained in:
parent
96cd055e05
commit
f157b78d93
|
|
@ -58,6 +58,8 @@ namespace Microsoft.AspNetCore.Mvc.Performance
|
||||||
{
|
{
|
||||||
var endpointDataSource = CreateMvcEndpointDataSource(_attributeActionProvider);
|
var endpointDataSource = CreateMvcEndpointDataSource(_attributeActionProvider);
|
||||||
var endpoints = endpointDataSource.Endpoints;
|
var endpoints = endpointDataSource.Endpoints;
|
||||||
|
|
||||||
|
AssertHasEndpoints(endpoints);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Benchmark]
|
[Benchmark]
|
||||||
|
|
@ -66,6 +68,8 @@ namespace Microsoft.AspNetCore.Mvc.Performance
|
||||||
var endpointDataSource = CreateMvcEndpointDataSource(_conventionalActionProvider);
|
var endpointDataSource = CreateMvcEndpointDataSource(_conventionalActionProvider);
|
||||||
endpointDataSource.ConventionalEndpointInfos.AddRange(_conventionalEndpointInfos);
|
endpointDataSource.ConventionalEndpointInfos.AddRange(_conventionalEndpointInfos);
|
||||||
var endpoints = endpointDataSource.Endpoints;
|
var endpoints = endpointDataSource.Endpoints;
|
||||||
|
|
||||||
|
AssertHasEndpoints(endpoints);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ActionDescriptor CreateAttributeRoutedAction(int id)
|
private ActionDescriptor CreateAttributeRoutedAction(int id)
|
||||||
|
|
@ -110,11 +114,20 @@ namespace Microsoft.AspNetCore.Mvc.Performance
|
||||||
var dataSource = new MvcEndpointDataSource(
|
var dataSource = new MvcEndpointDataSource(
|
||||||
actionDescriptorCollectionProvider,
|
actionDescriptorCollectionProvider,
|
||||||
new MvcEndpointInvokerFactory(new ActionInvokerFactory(Array.Empty<IActionInvokerProvider>())),
|
new MvcEndpointInvokerFactory(new ActionInvokerFactory(Array.Empty<IActionInvokerProvider>())),
|
||||||
new MockParameterPolicyFactory());
|
new MockParameterPolicyFactory(),
|
||||||
|
new MockRoutePatternTransformer());
|
||||||
|
|
||||||
return dataSource;
|
return dataSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class MockRoutePatternTransformer : RoutePatternTransformer
|
||||||
|
{
|
||||||
|
public override RoutePattern SubstituteRequiredValues(RoutePattern original, object requiredValues)
|
||||||
|
{
|
||||||
|
return original;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private class MockActionDescriptorCollectionProvider : IActionDescriptorCollectionProvider
|
private class MockActionDescriptorCollectionProvider : IActionDescriptorCollectionProvider
|
||||||
{
|
{
|
||||||
public MockActionDescriptorCollectionProvider(List<ActionDescriptor> actionDescriptors)
|
public MockActionDescriptorCollectionProvider(List<ActionDescriptor> actionDescriptors)
|
||||||
|
|
@ -137,5 +150,13 @@ namespace Microsoft.AspNetCore.Mvc.Performance
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void AssertHasEndpoints(IReadOnlyList<Http.Endpoint> endpoints)
|
||||||
|
{
|
||||||
|
if (endpoints.Count == 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Expected endpoints from data source.");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
// 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.Text.RegularExpressions;
|
||||||
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
|
||||||
|
namespace MvcSandbox
|
||||||
|
{
|
||||||
|
public class SlugifyParameterTransformer : IOutboundParameterTransformer
|
||||||
|
{
|
||||||
|
public string TransformOutbound(object value)
|
||||||
|
{
|
||||||
|
// Slugify value
|
||||||
|
return value == null ? null : Regex.Replace(value.ToString(), "([a-z])([A-Z])", "$1-$2", RegexOptions.None, TimeSpan.FromMilliseconds(100)).ToLower();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -5,11 +5,13 @@ using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
using Microsoft.AspNetCore.Routing.Internal;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
|
@ -20,6 +22,10 @@ namespace MvcSandbox
|
||||||
// This method gets called by the runtime. Use this method to add services to the container.
|
// This method gets called by the runtime. Use this method to add services to the container.
|
||||||
public void ConfigureServices(IServiceCollection services)
|
public void ConfigureServices(IServiceCollection services)
|
||||||
{
|
{
|
||||||
|
services.AddRouting(options =>
|
||||||
|
{
|
||||||
|
options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer);
|
||||||
|
});
|
||||||
services.AddMvc().SetCompatibilityVersion(Microsoft.AspNetCore.Mvc.CompatibilityVersion.Latest);
|
services.AddMvc().SetCompatibilityVersion(Microsoft.AspNetCore.Mvc.CompatibilityVersion.Latest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -37,6 +43,27 @@ namespace MvcSandbox
|
||||||
name: "default",
|
name: "default",
|
||||||
template: "{controller=Home}/{action=Index}/{id?}");
|
template: "{controller=Home}/{action=Index}/{id?}");
|
||||||
|
|
||||||
|
builder.MapControllerRoute(
|
||||||
|
name: "transform",
|
||||||
|
template: "Transform/{controller:slugify=Home}/{action:slugify=Index}/{id?}",
|
||||||
|
defaults: null,
|
||||||
|
constraints: new { controller = "Home" });
|
||||||
|
|
||||||
|
builder.MapGet(
|
||||||
|
"/graph",
|
||||||
|
"DFA Graph",
|
||||||
|
(httpContext) =>
|
||||||
|
{
|
||||||
|
using (var writer = new StreamWriter(httpContext.Response.Body, Encoding.UTF8, 1024, leaveOpen: true))
|
||||||
|
{
|
||||||
|
var graphWriter = httpContext.RequestServices.GetRequiredService<DfaGraphWriter>();
|
||||||
|
var dataSource = httpContext.RequestServices.GetRequiredService<EndpointDataSource>();
|
||||||
|
graphWriter.Write(dataSource, writer);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
});
|
||||||
|
|
||||||
builder.MapApplication();
|
builder.MapApplication();
|
||||||
|
|
||||||
builder.MapHealthChecks("/healthz");
|
builder.MapHealthChecks("/healthz");
|
||||||
|
|
@ -50,7 +77,7 @@ namespace MvcSandbox
|
||||||
|
|
||||||
private static Task WriteEndpoints(HttpContext httpContext)
|
private static Task WriteEndpoints(HttpContext httpContext)
|
||||||
{
|
{
|
||||||
var dataSource = httpContext.RequestServices.GetRequiredService<CompositeEndpointDataSource>();
|
var dataSource = httpContext.RequestServices.GetRequiredService<EndpointDataSource>();
|
||||||
|
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
sb.AppendLine("Endpoints:");
|
sb.AppendLine("Endpoints:");
|
||||||
|
|
@ -81,6 +108,7 @@ namespace MvcSandbox
|
||||||
factory
|
factory
|
||||||
.AddConsole()
|
.AddConsole()
|
||||||
.AddDebug();
|
.AddDebug();
|
||||||
|
factory.SetMinimumLevel(LogLevel.Trace);
|
||||||
})
|
})
|
||||||
.UseIISIntegration()
|
.UseIISIntegration()
|
||||||
.UseKestrel()
|
.UseKestrel()
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,11 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||||
using Microsoft.AspNetCore.Mvc.ActionConstraints;
|
using Microsoft.AspNetCore.Mvc.ActionConstraints;
|
||||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
|
||||||
using Microsoft.AspNetCore.Mvc.Filters;
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
|
@ -25,6 +23,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
private readonly IActionDescriptorCollectionProvider _actions;
|
private readonly IActionDescriptorCollectionProvider _actions;
|
||||||
private readonly MvcEndpointInvokerFactory _invokerFactory;
|
private readonly MvcEndpointInvokerFactory _invokerFactory;
|
||||||
private readonly ParameterPolicyFactory _parameterPolicyFactory;
|
private readonly ParameterPolicyFactory _parameterPolicyFactory;
|
||||||
|
private readonly RoutePatternTransformer _routePatternTransformer;
|
||||||
|
|
||||||
// The following are protected by this lock for WRITES only. This pattern is similar
|
// The following are protected by this lock for WRITES only. This pattern is similar
|
||||||
// to DefaultActionDescriptorChangeProvider - see comments there for details on
|
// to DefaultActionDescriptorChangeProvider - see comments there for details on
|
||||||
|
|
@ -37,26 +36,13 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
public MvcEndpointDataSource(
|
public MvcEndpointDataSource(
|
||||||
IActionDescriptorCollectionProvider actions,
|
IActionDescriptorCollectionProvider actions,
|
||||||
MvcEndpointInvokerFactory invokerFactory,
|
MvcEndpointInvokerFactory invokerFactory,
|
||||||
ParameterPolicyFactory parameterPolicyFactory)
|
ParameterPolicyFactory parameterPolicyFactory,
|
||||||
|
RoutePatternTransformer routePatternTransformer)
|
||||||
{
|
{
|
||||||
if (actions == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(actions));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (invokerFactory == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(invokerFactory));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parameterPolicyFactory == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(parameterPolicyFactory));
|
|
||||||
}
|
|
||||||
|
|
||||||
_actions = actions;
|
_actions = actions;
|
||||||
_invokerFactory = invokerFactory;
|
_invokerFactory = invokerFactory;
|
||||||
_parameterPolicyFactory = parameterPolicyFactory;
|
_parameterPolicyFactory = parameterPolicyFactory;
|
||||||
|
_routePatternTransformer = routePatternTransformer;
|
||||||
|
|
||||||
ConventionalEndpointInfos = new List<MvcEndpointInfo>();
|
ConventionalEndpointInfos = new List<MvcEndpointInfo>();
|
||||||
AttributeRoutingConventionResolvers = new List<Func<ActionDescriptor, DefaultEndpointConventionBuilder>>();
|
AttributeRoutingConventionResolvers = new List<Func<ActionDescriptor, DefaultEndpointConventionBuilder>>();
|
||||||
|
|
@ -115,7 +101,6 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
var endpoints = new List<Endpoint>();
|
var endpoints = new List<Endpoint>();
|
||||||
StringBuilder patternStringBuilder = null;
|
|
||||||
|
|
||||||
foreach (var action in _actions.ActionDescriptors.Items)
|
foreach (var action in _actions.ActionDescriptors.Items)
|
||||||
{
|
{
|
||||||
|
|
@ -129,48 +114,31 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
// This is for scenarios dealing with migrating existing Router based code to Endpoint Routing world.
|
// This is for scenarios dealing with migrating existing Router based code to Endpoint Routing world.
|
||||||
var conventionalRouteOrder = 1;
|
var conventionalRouteOrder = 1;
|
||||||
|
|
||||||
// Check each of the conventional patterns to see if the action would be reachable
|
// Check each of the conventional patterns to see if the action would be reachable.
|
||||||
// If the action and pattern are compatible then create an endpoint with the
|
// If the action and pattern are compatible then create an endpoint with action
|
||||||
// area/controller/action parameter parts replaced with literals
|
// route values on the pattern.
|
||||||
//
|
|
||||||
// e.g. {controller}/{action} with HomeController.Index and HomeController.Login
|
|
||||||
// would result in endpoints:
|
|
||||||
// - Home/Index
|
|
||||||
// - Home/Login
|
|
||||||
foreach (var endpointInfo in ConventionalEndpointInfos)
|
foreach (var endpointInfo in ConventionalEndpointInfos)
|
||||||
{
|
{
|
||||||
// An 'endpointInfo' is applicable if:
|
// An 'endpointInfo' is applicable if:
|
||||||
// 1. it has a parameter (or default value) for 'required' non-null route value
|
// 1. It has a parameter (or default value) for 'required' non-null route value
|
||||||
// 2. it does not have a parameter (or default value) for 'required' null route value
|
// 2. It does not have a parameter (or default value) for 'required' null route value
|
||||||
var isApplicable = true;
|
var updatedRoutePattern = _routePatternTransformer.SubstituteRequiredValues(endpointInfo.ParsedPattern, action.RouteValues);
|
||||||
foreach (var routeKey in action.RouteValues.Keys)
|
|
||||||
{
|
|
||||||
if (!MatchRouteValue(action, endpointInfo, routeKey))
|
|
||||||
{
|
|
||||||
isApplicable = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isApplicable)
|
if (updatedRoutePattern == null)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
conventionalRouteOrder = CreateEndpoints(
|
var endpoint = CreateEndpoint(
|
||||||
endpoints,
|
|
||||||
ref patternStringBuilder,
|
|
||||||
action,
|
action,
|
||||||
conventionalRouteOrder,
|
updatedRoutePattern,
|
||||||
endpointInfo.ParsedPattern,
|
|
||||||
endpointInfo.MergedDefaults,
|
|
||||||
endpointInfo.Defaults,
|
|
||||||
endpointInfo.Name,
|
endpointInfo.Name,
|
||||||
|
conventionalRouteOrder++,
|
||||||
endpointInfo.DataTokens,
|
endpointInfo.DataTokens,
|
||||||
endpointInfo.ParameterPolicies,
|
false,
|
||||||
suppressLinkGeneration: false,
|
false,
|
||||||
suppressPathMatching: false,
|
|
||||||
endpointInfo.Conventions);
|
endpointInfo.Conventions);
|
||||||
|
endpoints.Add(endpoint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
@ -185,20 +153,27 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
|
|
||||||
var attributeRoutePattern = RoutePatternFactory.Parse(action.AttributeRouteInfo.Template);
|
var attributeRoutePattern = RoutePatternFactory.Parse(action.AttributeRouteInfo.Template);
|
||||||
|
|
||||||
CreateEndpoints(
|
// Modify the route and required values to ensure required values can be successfully subsituted.
|
||||||
endpoints,
|
// Subsitituting required values into an attribute route pattern should always succeed.
|
||||||
ref patternStringBuilder,
|
var (resolvedRoutePattern, resolvedRouteValues) = ResolveDefaultsAndRequiredValues(action, attributeRoutePattern);
|
||||||
|
|
||||||
|
var updatedRoutePattern = _routePatternTransformer.SubstituteRequiredValues(resolvedRoutePattern, resolvedRouteValues);
|
||||||
|
|
||||||
|
if (updatedRoutePattern == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Failed to update route pattern with required values.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var endpoint = CreateEndpoint(
|
||||||
action,
|
action,
|
||||||
action.AttributeRouteInfo.Order,
|
updatedRoutePattern,
|
||||||
attributeRoutePattern,
|
|
||||||
attributeRoutePattern.Defaults,
|
|
||||||
nonInlineDefaults: null,
|
|
||||||
action.AttributeRouteInfo.Name,
|
action.AttributeRouteInfo.Name,
|
||||||
|
action.AttributeRouteInfo.Order,
|
||||||
dataTokens: null,
|
dataTokens: null,
|
||||||
allParameterPolicies: null,
|
|
||||||
action.AttributeRouteInfo.SuppressLinkGeneration,
|
action.AttributeRouteInfo.SuppressLinkGeneration,
|
||||||
action.AttributeRouteInfo.SuppressPathMatching,
|
action.AttributeRouteInfo.SuppressPathMatching,
|
||||||
conventionBuilder.Conventions);
|
conventionBuilder.Conventions);
|
||||||
|
endpoints.Add(endpoint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -220,6 +195,58 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static (RoutePattern resolvedRoutePattern, IDictionary<string, string> resolvedRequiredValues) ResolveDefaultsAndRequiredValues(ActionDescriptor action, RoutePattern attributeRoutePattern)
|
||||||
|
{
|
||||||
|
RouteValueDictionary updatedDefaults = null;
|
||||||
|
IDictionary<string, string> resolvedRequiredValues = null;
|
||||||
|
|
||||||
|
foreach (var routeValue in action.RouteValues)
|
||||||
|
{
|
||||||
|
var parameter = attributeRoutePattern.GetParameter(routeValue.Key);
|
||||||
|
|
||||||
|
if (!RouteValueEqualityComparer.Default.Equals(routeValue.Value, string.Empty))
|
||||||
|
{
|
||||||
|
if (parameter == null)
|
||||||
|
{
|
||||||
|
// The attribute route has a required value with no matching parameter
|
||||||
|
// Add the required values without a parameter as a default
|
||||||
|
// e.g.
|
||||||
|
// Template: "Login/{action}"
|
||||||
|
// Required values: { controller = "Login", action = "Index" }
|
||||||
|
// Updated defaults: { controller = "Login" }
|
||||||
|
|
||||||
|
if (updatedDefaults == null)
|
||||||
|
{
|
||||||
|
updatedDefaults = new RouteValueDictionary(attributeRoutePattern.Defaults);
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedDefaults[routeValue.Key] = routeValue.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (parameter != null)
|
||||||
|
{
|
||||||
|
// The attribute route has a null or empty required value with a matching parameter
|
||||||
|
// Remove the required value from the route
|
||||||
|
|
||||||
|
if (resolvedRequiredValues == null)
|
||||||
|
{
|
||||||
|
resolvedRequiredValues = new Dictionary<string, string>(action.RouteValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolvedRequiredValues.Remove(parameter.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (updatedDefaults != null)
|
||||||
|
{
|
||||||
|
attributeRoutePattern = RoutePatternFactory.Parse(action.AttributeRouteInfo.Template, updatedDefaults, parameterPolicies: null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (attributeRoutePattern, resolvedRequiredValues ?? action.RouteValues);
|
||||||
|
}
|
||||||
|
|
||||||
private DefaultEndpointConventionBuilder ResolveActionConventionBuilder(ActionDescriptor action)
|
private DefaultEndpointConventionBuilder ResolveActionConventionBuilder(ActionDescriptor action)
|
||||||
{
|
{
|
||||||
foreach (var filter in AttributeRoutingConventionResolvers)
|
foreach (var filter in AttributeRoutingConventionResolvers)
|
||||||
|
|
@ -234,339 +261,10 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateEndpoints processes the route pattern, replacing area/controller/action parameters with endpoint values
|
|
||||||
// Because of default values it is possible for a route pattern to resolve to multiple endpoints
|
|
||||||
private int CreateEndpoints(
|
|
||||||
List<Endpoint> endpoints,
|
|
||||||
ref StringBuilder patternStringBuilder,
|
|
||||||
ActionDescriptor action,
|
|
||||||
int routeOrder,
|
|
||||||
RoutePattern routePattern,
|
|
||||||
IReadOnlyDictionary<string, object> allDefaults,
|
|
||||||
IReadOnlyDictionary<string, object> nonInlineDefaults,
|
|
||||||
string name,
|
|
||||||
RouteValueDictionary dataTokens,
|
|
||||||
IDictionary<string, IList<IParameterPolicy>> allParameterPolicies,
|
|
||||||
bool suppressLinkGeneration,
|
|
||||||
bool suppressPathMatching,
|
|
||||||
List<Action<EndpointModel>> conventions)
|
|
||||||
{
|
|
||||||
var newPathSegments = routePattern.PathSegments.ToList();
|
|
||||||
var hasLinkGenerationEndpoint = false;
|
|
||||||
|
|
||||||
// Create a mutable copy
|
|
||||||
var nonInlineDefaultsCopy = nonInlineDefaults != null
|
|
||||||
? new RouteValueDictionary(nonInlineDefaults)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
var resolvedRouteValues = ResolveActionRouteValues(action, allDefaults);
|
|
||||||
|
|
||||||
for (var i = 0; i < newPathSegments.Count; i++)
|
|
||||||
{
|
|
||||||
// Check if the pattern can be shortened because the remaining parameters are optional
|
|
||||||
//
|
|
||||||
// e.g. Matching pattern {controller=Home}/{action=Index} against HomeController.Index
|
|
||||||
// can resolve to the following endpoints: (sorted by RouteEndpoint.Order)
|
|
||||||
// - /
|
|
||||||
// - /Home
|
|
||||||
// - /Home/Index
|
|
||||||
if (UseDefaultValuePlusRemainingSegmentsOptional(
|
|
||||||
i,
|
|
||||||
action,
|
|
||||||
resolvedRouteValues,
|
|
||||||
allDefaults,
|
|
||||||
ref nonInlineDefaultsCopy,
|
|
||||||
newPathSegments))
|
|
||||||
{
|
|
||||||
// The route pattern has matching default values AND an optional parameter
|
|
||||||
// For link generation we need to include an endpoint with parameters and default values
|
|
||||||
// so the link is correctly shortened
|
|
||||||
// e.g. {controller=Home}/{action=Index}/{id=17}
|
|
||||||
if (!hasLinkGenerationEndpoint)
|
|
||||||
{
|
|
||||||
var ep = CreateEndpoint(
|
|
||||||
action,
|
|
||||||
resolvedRouteValues,
|
|
||||||
name,
|
|
||||||
GetPattern(ref patternStringBuilder, newPathSegments),
|
|
||||||
newPathSegments,
|
|
||||||
nonInlineDefaultsCopy,
|
|
||||||
routeOrder++,
|
|
||||||
dataTokens,
|
|
||||||
suppressLinkGeneration,
|
|
||||||
true,
|
|
||||||
conventions);
|
|
||||||
endpoints.Add(ep);
|
|
||||||
|
|
||||||
hasLinkGenerationEndpoint = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var subPathSegments = newPathSegments.Take(i);
|
|
||||||
|
|
||||||
var subEndpoint = CreateEndpoint(
|
|
||||||
action,
|
|
||||||
resolvedRouteValues,
|
|
||||||
name,
|
|
||||||
GetPattern(ref patternStringBuilder, subPathSegments),
|
|
||||||
subPathSegments,
|
|
||||||
nonInlineDefaultsCopy,
|
|
||||||
routeOrder++,
|
|
||||||
dataTokens,
|
|
||||||
suppressLinkGeneration,
|
|
||||||
suppressPathMatching,
|
|
||||||
conventions);
|
|
||||||
endpoints.Add(subEndpoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdatePathSegments(i, action, resolvedRouteValues, routePattern, newPathSegments, ref allParameterPolicies);
|
|
||||||
}
|
|
||||||
|
|
||||||
var finalEndpoint = CreateEndpoint(
|
|
||||||
action,
|
|
||||||
resolvedRouteValues,
|
|
||||||
name,
|
|
||||||
GetPattern(ref patternStringBuilder, newPathSegments),
|
|
||||||
newPathSegments,
|
|
||||||
nonInlineDefaultsCopy,
|
|
||||||
routeOrder++,
|
|
||||||
dataTokens,
|
|
||||||
suppressLinkGeneration,
|
|
||||||
suppressPathMatching,
|
|
||||||
conventions);
|
|
||||||
endpoints.Add(finalEndpoint);
|
|
||||||
|
|
||||||
return routeOrder;
|
|
||||||
|
|
||||||
string GetPattern(ref StringBuilder sb, IEnumerable<RoutePatternPathSegment> segments)
|
|
||||||
{
|
|
||||||
if (sb == null)
|
|
||||||
{
|
|
||||||
sb = new StringBuilder();
|
|
||||||
}
|
|
||||||
|
|
||||||
RoutePatternWriter.WriteString(sb, segments);
|
|
||||||
var rawPattern = sb.ToString();
|
|
||||||
sb.Length = 0;
|
|
||||||
|
|
||||||
return rawPattern;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IDictionary<string, string> ResolveActionRouteValues(ActionDescriptor action, IReadOnlyDictionary<string, object> allDefaults)
|
|
||||||
{
|
|
||||||
Dictionary<string, string> resolvedRequiredValues = null;
|
|
||||||
|
|
||||||
foreach (var kvp in action.RouteValues)
|
|
||||||
{
|
|
||||||
// Check whether there is a matching default value with a different case
|
|
||||||
// e.g. {controller=HOME}/{action} with HomeController.Index will have route values:
|
|
||||||
// - controller = HOME
|
|
||||||
// - action = Index
|
|
||||||
if (allDefaults.TryGetValue(kvp.Key, out var value) &&
|
|
||||||
value is string defaultValue &&
|
|
||||||
!string.Equals(kvp.Value, defaultValue, StringComparison.Ordinal) &&
|
|
||||||
string.Equals(kvp.Value, defaultValue, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
if (resolvedRequiredValues == null)
|
|
||||||
{
|
|
||||||
resolvedRequiredValues = new Dictionary<string, string>(action.RouteValues, StringComparer.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
resolvedRequiredValues[kvp.Key] = defaultValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return resolvedRequiredValues ?? action.RouteValues;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdatePathSegments(
|
|
||||||
int i,
|
|
||||||
ActionDescriptor action,
|
|
||||||
IDictionary<string, string> resolvedRequiredValues,
|
|
||||||
RoutePattern routePattern,
|
|
||||||
List<RoutePatternPathSegment> newPathSegments,
|
|
||||||
ref IDictionary<string, IList<IParameterPolicy>> allParameterPolicies)
|
|
||||||
{
|
|
||||||
List<RoutePatternPart> segmentParts = null; // Initialize only as needed
|
|
||||||
var segment = newPathSegments[i];
|
|
||||||
for (var j = 0; j < segment.Parts.Count; j++)
|
|
||||||
{
|
|
||||||
var part = segment.Parts[j];
|
|
||||||
|
|
||||||
if (part is RoutePatternParameterPart parameterPart)
|
|
||||||
{
|
|
||||||
if (resolvedRequiredValues.TryGetValue(parameterPart.Name, out var parameterRouteValue))
|
|
||||||
{
|
|
||||||
if (segmentParts == null)
|
|
||||||
{
|
|
||||||
segmentParts = segment.Parts.ToList();
|
|
||||||
}
|
|
||||||
if (allParameterPolicies == null)
|
|
||||||
{
|
|
||||||
allParameterPolicies = MvcEndpointInfo.BuildParameterPolicies(routePattern.Parameters, _parameterPolicyFactory);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Route value could be null if it is a "known" route value.
|
|
||||||
// Do not use the null value to de-normalize the route pattern,
|
|
||||||
// instead leave the parameter unchanged.
|
|
||||||
// e.g.
|
|
||||||
// RouteValues will contain a null "page" value if there are Razor pages
|
|
||||||
// Skip replacing the {page} parameter
|
|
||||||
if (parameterRouteValue != null)
|
|
||||||
{
|
|
||||||
if (allParameterPolicies.TryGetValue(parameterPart.Name, out var parameterPolicies))
|
|
||||||
{
|
|
||||||
// Check if the parameter has a transformer policy
|
|
||||||
// Use the first transformer policy
|
|
||||||
for (var k = 0; k < parameterPolicies.Count; k++)
|
|
||||||
{
|
|
||||||
if (parameterPolicies[k] is IOutboundParameterTransformer parameterTransformer)
|
|
||||||
{
|
|
||||||
parameterRouteValue = parameterTransformer.TransformOutbound(parameterRouteValue);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
segmentParts[j] = RoutePatternFactory.LiteralPart(parameterRouteValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// A parameter part was replaced so replace segment with updated parts
|
|
||||||
if (segmentParts != null)
|
|
||||||
{
|
|
||||||
newPathSegments[i] = RoutePatternFactory.Segment(segmentParts);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool UseDefaultValuePlusRemainingSegmentsOptional(
|
|
||||||
int segmentIndex,
|
|
||||||
ActionDescriptor action,
|
|
||||||
IDictionary<string, string> resolvedRequiredValues,
|
|
||||||
IReadOnlyDictionary<string, object> allDefaults,
|
|
||||||
ref RouteValueDictionary nonInlineDefaults,
|
|
||||||
List<RoutePatternPathSegment> pathSegments)
|
|
||||||
{
|
|
||||||
// Check whether the remaining segments are all optional and one or more of them is
|
|
||||||
// for area/controller/action and has a default value
|
|
||||||
var usedDefaultValue = false;
|
|
||||||
|
|
||||||
for (var i = segmentIndex; i < pathSegments.Count; i++)
|
|
||||||
{
|
|
||||||
var segment = pathSegments[i];
|
|
||||||
for (var j = 0; j < segment.Parts.Count; j++)
|
|
||||||
{
|
|
||||||
var part = segment.Parts[j];
|
|
||||||
if (part.IsParameter && part is RoutePatternParameterPart parameterPart)
|
|
||||||
{
|
|
||||||
if (allDefaults.TryGetValue(parameterPart.Name, out var v))
|
|
||||||
{
|
|
||||||
if (resolvedRequiredValues.TryGetValue(parameterPart.Name, out var routeValue))
|
|
||||||
{
|
|
||||||
if (string.Equals(v as string, routeValue, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
usedDefaultValue = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (nonInlineDefaults == null)
|
|
||||||
{
|
|
||||||
nonInlineDefaults = new RouteValueDictionary();
|
|
||||||
}
|
|
||||||
nonInlineDefaults.TryAdd(parameterPart.Name, v);
|
|
||||||
|
|
||||||
usedDefaultValue = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parameterPart.IsOptional || parameterPart.IsCatchAll)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (part.IsSeparator && part is RoutePatternSeparatorPart separatorPart
|
|
||||||
&& separatorPart.Content == ".")
|
|
||||||
{
|
|
||||||
// Check if this pattern ends in an optional extension, e.g. ".{ext?}"
|
|
||||||
// Current literal must be "." and followed by a single optional parameter part
|
|
||||||
var nextPartIndex = j + 1;
|
|
||||||
|
|
||||||
if (nextPartIndex == segment.Parts.Count - 1
|
|
||||||
&& segment.Parts[nextPartIndex].IsParameter
|
|
||||||
&& segment.Parts[nextPartIndex] is RoutePatternParameterPart extensionParameterPart
|
|
||||||
&& extensionParameterPart.IsOptional)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop because there is a non-optional/non-defaulted trailing value
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return usedDefaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool MatchRouteValue(ActionDescriptor action, MvcEndpointInfo endpointInfo, string routeKey)
|
|
||||||
{
|
|
||||||
if (!action.RouteValues.TryGetValue(routeKey, out var actionValue) || string.IsNullOrWhiteSpace(actionValue))
|
|
||||||
{
|
|
||||||
// Action does not have a value for this routeKey, most likely because action is not in an area
|
|
||||||
// Check that the pattern does not have a parameter for the routeKey
|
|
||||||
var matchingParameter = endpointInfo.ParsedPattern.GetParameter(routeKey);
|
|
||||||
if (matchingParameter == null &&
|
|
||||||
(!endpointInfo.ParsedPattern.Defaults.TryGetValue(routeKey, out var value) ||
|
|
||||||
!string.IsNullOrEmpty(Convert.ToString(value))))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (endpointInfo.MergedDefaults != null && string.Equals(actionValue, endpointInfo.MergedDefaults[routeKey] as string, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var matchingParameter = endpointInfo.ParsedPattern.GetParameter(routeKey);
|
|
||||||
if (matchingParameter != null)
|
|
||||||
{
|
|
||||||
// Check that the value matches against constraints on that parameter
|
|
||||||
// e.g. For {controller:regex((Home|Login))} the controller value must match the regex
|
|
||||||
if (endpointInfo.ParameterPolicies.TryGetValue(routeKey, out var parameterPolicies))
|
|
||||||
{
|
|
||||||
foreach (var policy in parameterPolicies)
|
|
||||||
{
|
|
||||||
if (policy is IRouteConstraint constraint
|
|
||||||
&& !constraint.Match(httpContext: null, NullRouter.Instance, routeKey, new RouteValueDictionary(action.RouteValues), RouteDirection.IncomingRequest))
|
|
||||||
{
|
|
||||||
// Did not match constraint
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private RouteEndpoint CreateEndpoint(
|
private RouteEndpoint CreateEndpoint(
|
||||||
ActionDescriptor action,
|
ActionDescriptor action,
|
||||||
IDictionary<string, string> actionRouteValues,
|
RoutePattern routePattern,
|
||||||
string routeName,
|
string routeName,
|
||||||
string patternRawText,
|
|
||||||
IEnumerable<RoutePatternPathSegment> segments,
|
|
||||||
object nonInlineDefaults,
|
|
||||||
int order,
|
int order,
|
||||||
RouteValueDictionary dataTokens,
|
RouteValueDictionary dataTokens,
|
||||||
bool suppressLinkGeneration,
|
bool suppressLinkGeneration,
|
||||||
|
|
@ -583,16 +281,12 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
return invoker.InvokeAsync();
|
return invoker.InvokeAsync();
|
||||||
};
|
};
|
||||||
|
|
||||||
var defaults = new RouteValueDictionary(nonInlineDefaults);
|
var model = new RouteEndpointModel(requestDelegate, routePattern, order);
|
||||||
EnsureRequiredValuesInDefaults(actionRouteValues, defaults, segments);
|
|
||||||
|
|
||||||
var model = new RouteEndpointModel(requestDelegate, RoutePatternFactory.Pattern(patternRawText, defaults, parameterPolicies: null, segments), order);
|
|
||||||
|
|
||||||
AddEndpointMetadata(
|
AddEndpointMetadata(
|
||||||
model.Metadata,
|
model.Metadata,
|
||||||
action,
|
action,
|
||||||
routeName,
|
routeName,
|
||||||
new RouteValueDictionary(actionRouteValues),
|
|
||||||
dataTokens,
|
dataTokens,
|
||||||
suppressLinkGeneration,
|
suppressLinkGeneration,
|
||||||
suppressPathMatching);
|
suppressPathMatching);
|
||||||
|
|
@ -616,7 +310,6 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
IList<object> metadata,
|
IList<object> metadata,
|
||||||
ActionDescriptor action,
|
ActionDescriptor action,
|
||||||
string routeName,
|
string routeName,
|
||||||
RouteValueDictionary requiredValues,
|
|
||||||
RouteValueDictionary dataTokens,
|
RouteValueDictionary dataTokens,
|
||||||
bool suppressLinkGeneration,
|
bool suppressLinkGeneration,
|
||||||
bool suppressPathMatching)
|
bool suppressPathMatching)
|
||||||
|
|
@ -637,7 +330,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
metadata.Add(new DataTokensMetadata(dataTokens));
|
metadata.Add(new DataTokensMetadata(dataTokens));
|
||||||
}
|
}
|
||||||
|
|
||||||
metadata.Add(new RouteValuesAddressMetadata(routeName, requiredValues));
|
metadata.Add(new RouteNameMetadata(routeName));
|
||||||
|
|
||||||
// Add filter descriptors to endpoint metadata
|
// Add filter descriptors to endpoint metadata
|
||||||
if (action.FilterDescriptors != null && action.FilterDescriptors.Count > 0)
|
if (action.FilterDescriptors != null && action.FilterDescriptors.Count > 0)
|
||||||
|
|
@ -685,33 +378,5 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
metadata.Add(new SuppressMatchingMetadata());
|
metadata.Add(new SuppressMatchingMetadata());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure route values are a subset of defaults
|
|
||||||
// Examples:
|
|
||||||
//
|
|
||||||
// Template: {controller}/{action}/{category}/{id?}
|
|
||||||
// Defaults(in-line or non in-line): category=products
|
|
||||||
// Required values: controller=foo, action=bar
|
|
||||||
// Final constructed pattern: foo/bar/{category}/{id?}
|
|
||||||
// Final defaults: controller=foo, action=bar, category=products
|
|
||||||
//
|
|
||||||
// Template: {controller=Home}/{action=Index}/{category=products}/{id?}
|
|
||||||
// Defaults: controller=Home, action=Index, category=products
|
|
||||||
// Required values: controller=foo, action=bar
|
|
||||||
// Final constructed pattern: foo/bar/{category}/{id?}
|
|
||||||
// Final defaults: controller=foo, action=bar, category=products
|
|
||||||
private void EnsureRequiredValuesInDefaults(
|
|
||||||
IDictionary<string, string> routeValues,
|
|
||||||
RouteValueDictionary defaults,
|
|
||||||
IEnumerable<RoutePatternPathSegment> segments)
|
|
||||||
{
|
|
||||||
foreach (var kvp in routeValues)
|
|
||||||
{
|
|
||||||
if (kvp.Value != null)
|
|
||||||
{
|
|
||||||
defaults[kvp.Key] = kvp.Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,11 +25,11 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
var endpoint1 = CreateEndpoint(
|
var endpoint1 = CreateEndpoint(
|
||||||
"Home/Index/{id}",
|
"Home/Index/{id}",
|
||||||
defaults: new { controller = "Home", action = "Index", },
|
defaults: new { controller = "Home", action = "Index", },
|
||||||
metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "Index", })) });
|
requiredValues: new { controller = "Home", action = "Index" });
|
||||||
var endpoint2 = CreateEndpoint(
|
var endpoint2 = CreateEndpoint(
|
||||||
"Home/Index/{id?}",
|
"Home/Index/{id?}",
|
||||||
defaults: new { controller = "Home", action = "Index", },
|
defaults: new { controller = "Home", action = "Index", },
|
||||||
metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "Index", })) });
|
requiredValues: new { controller = "Home", action = "Index" });
|
||||||
|
|
||||||
var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
|
var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
|
||||||
|
|
||||||
|
|
@ -55,11 +55,11 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
var endpoint1 = CreateEndpoint(
|
var endpoint1 = CreateEndpoint(
|
||||||
"Home/Index/{id}",
|
"Home/Index/{id}",
|
||||||
defaults: new { controller = "Home", action = "Index", },
|
defaults: new { controller = "Home", action = "Index", },
|
||||||
metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "Index", })) });
|
requiredValues: new { controller = "Home", action = "Index" });
|
||||||
var endpoint2 = CreateEndpoint(
|
var endpoint2 = CreateEndpoint(
|
||||||
"Home/Index/{id?}",
|
"Home/Index/{id?}",
|
||||||
defaults: new { controller = "Home", action = "Index", },
|
defaults: new { controller = "Home", action = "Index", },
|
||||||
metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "Index", })) });
|
requiredValues: new { controller = "Home", action = "Index" });
|
||||||
|
|
||||||
var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
|
var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
|
||||||
|
|
||||||
|
|
@ -83,11 +83,11 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
var endpoint1 = CreateEndpoint(
|
var endpoint1 = CreateEndpoint(
|
||||||
"Home/Index/{id}",
|
"Home/Index/{id}",
|
||||||
defaults: new { controller = "Home", action = "Index", },
|
defaults: new { controller = "Home", action = "Index", },
|
||||||
metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "Index", })) });
|
requiredValues: new { controller = "Home", action = "Index" });
|
||||||
var endpoint2 = CreateEndpoint(
|
var endpoint2 = CreateEndpoint(
|
||||||
"Home/Index/{id?}",
|
"Home/Index/{id?}",
|
||||||
defaults: new { controller = "Home", action = "Index", },
|
defaults: new { controller = "Home", action = "Index", },
|
||||||
metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "Index", })) });
|
requiredValues: new { controller = "Home", action = "Index" });
|
||||||
|
|
||||||
var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
|
var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
|
||||||
|
|
||||||
|
|
@ -114,11 +114,11 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
var endpoint1 = CreateEndpoint(
|
var endpoint1 = CreateEndpoint(
|
||||||
"Home/Index/{id}",
|
"Home/Index/{id}",
|
||||||
defaults: new { controller = "Home", action = "Index", },
|
defaults: new { controller = "Home", action = "Index", },
|
||||||
metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "Index", })) });
|
requiredValues: new { controller = "Home", action = "Index" });
|
||||||
var endpoint2 = CreateEndpoint(
|
var endpoint2 = CreateEndpoint(
|
||||||
"Home/Index/{id?}",
|
"Home/Index/{id?}",
|
||||||
defaults: new { controller = "Home", action = "Index", },
|
defaults: new { controller = "Home", action = "Index", },
|
||||||
metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "Index", })) });
|
requiredValues: new { controller = "Home", action = "Index" });
|
||||||
|
|
||||||
var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
|
var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
|
||||||
|
|
||||||
|
|
@ -144,11 +144,11 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
var endpoint1 = CreateEndpoint(
|
var endpoint1 = CreateEndpoint(
|
||||||
"Home/Index/{id}",
|
"Home/Index/{id}",
|
||||||
defaults: new { controller = "Home", action = "Index", },
|
defaults: new { controller = "Home", action = "Index", },
|
||||||
metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "Index", })) });
|
requiredValues: new { controller = "Home", action = "Index" });
|
||||||
var endpoint2 = CreateEndpoint(
|
var endpoint2 = CreateEndpoint(
|
||||||
"Home/Index/{id?}",
|
"Home/Index/{id?}",
|
||||||
defaults: new { controller = "Home", action = "Index", },
|
defaults: new { controller = "Home", action = "Index", },
|
||||||
metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "Index", })) });
|
requiredValues: new { controller = "Home", action = "Index" });
|
||||||
|
|
||||||
var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
|
var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
|
||||||
|
|
||||||
|
|
@ -177,7 +177,7 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
{
|
{
|
||||||
return new RouteEndpoint(
|
return new RouteEndpoint(
|
||||||
(httpContext) => Task.CompletedTask,
|
(httpContext) => Task.CompletedTask,
|
||||||
RoutePatternFactory.Parse(template, defaults, parameterPolicies: null),
|
RoutePatternFactory.Parse(template, defaults, parameterPolicies: null, requiredValues),
|
||||||
order,
|
order,
|
||||||
new EndpointMetadataCollection(metadata ?? Array.Empty<object>()),
|
new EndpointMetadataCollection(metadata ?? Array.Empty<object>()),
|
||||||
null);
|
null);
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
"Home/Index",
|
"Home/Index",
|
||||||
defaults: new { controller = "Home", action = "Index" },
|
defaults: new { controller = "Home", action = "Index" },
|
||||||
requiredValues: new { controller = "Home", action = "Index" },
|
requiredValues: new { controller = "Home", action = "Index" },
|
||||||
metadataCollection: new EndpointMetadataCollection(new[] { new SuppressLinkGenerationMetadata() }));
|
metadata: new[] { new SuppressLinkGenerationMetadata() });
|
||||||
var urlHelper = CreateUrlHelper(new[] { endpoint });
|
var urlHelper = CreateUrlHelper(new[] { endpoint });
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
@ -273,19 +273,20 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
object requiredValues = null,
|
object requiredValues = null,
|
||||||
int order = 0,
|
int order = 0,
|
||||||
string routeName = null,
|
string routeName = null,
|
||||||
EndpointMetadataCollection metadataCollection = null)
|
IList<object> metadata = null)
|
||||||
{
|
{
|
||||||
if (metadataCollection == null)
|
metadata = metadata ?? new List<object>();
|
||||||
|
|
||||||
|
if (routeName != null)
|
||||||
{
|
{
|
||||||
metadataCollection = new EndpointMetadataCollection(
|
metadata.Add(new RouteNameMetadata(routeName));
|
||||||
new RouteValuesAddressMetadata(routeName, new RouteValueDictionary(requiredValues)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new RouteEndpoint(
|
return new RouteEndpoint(
|
||||||
(httpContext) => Task.CompletedTask,
|
(httpContext) => Task.CompletedTask,
|
||||||
RoutePatternFactory.Parse(template, defaults, parameterPolicies: null),
|
RoutePatternFactory.Parse(template, defaults, parameterPolicies: null, requiredValues),
|
||||||
order,
|
order,
|
||||||
metadataCollection,
|
new EndpointMetadataCollection(metadata),
|
||||||
null);
|
null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -25,11 +25,11 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
var endpoint1 = CreateEndpoint(
|
var endpoint1 = CreateEndpoint(
|
||||||
"About/{id}",
|
"About/{id}",
|
||||||
defaults: new { page = "/About", },
|
defaults: new { page = "/About", },
|
||||||
metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { page = "/About", })) });
|
requiredValues:new { page = "/About", });
|
||||||
var endpoint2 = CreateEndpoint(
|
var endpoint2 = CreateEndpoint(
|
||||||
"Admin/ManageUsers/{handler?}",
|
"Admin/ManageUsers/{handler?}",
|
||||||
defaults: new { page = "/Admin/ManageUsers", },
|
defaults: new { page = "/Admin/ManageUsers", },
|
||||||
metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { page = "/Admin/ManageUsers", })) });
|
requiredValues:new { page = "/Admin/ManageUsers", });
|
||||||
|
|
||||||
var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
|
var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
|
||||||
|
|
||||||
|
|
@ -54,11 +54,11 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
var endpoint1 = CreateEndpoint(
|
var endpoint1 = CreateEndpoint(
|
||||||
"About/{id}",
|
"About/{id}",
|
||||||
defaults: new { page = "/About", },
|
defaults: new { page = "/About", },
|
||||||
metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { page = "/About", })) });
|
requiredValues:new { page = "/About", });
|
||||||
var endpoint2 = CreateEndpoint(
|
var endpoint2 = CreateEndpoint(
|
||||||
"Admin/ManageUsers/{handler?}",
|
"Admin/ManageUsers/{handler?}",
|
||||||
defaults: new { page = "/Admin/ManageUsers", },
|
defaults: new { page = "/Admin/ManageUsers", },
|
||||||
metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { page = "/Admin/ManageUsers", })) });
|
requiredValues:new { page = "/Admin/ManageUsers", });
|
||||||
|
|
||||||
var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
|
var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
|
||||||
|
|
||||||
|
|
@ -82,11 +82,11 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
var endpoint1 = CreateEndpoint(
|
var endpoint1 = CreateEndpoint(
|
||||||
"About/{id}",
|
"About/{id}",
|
||||||
defaults: new { page = "/About", },
|
defaults: new { page = "/About", },
|
||||||
metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { page = "/About", })) });
|
requiredValues:new { page = "/About", });
|
||||||
var endpoint2 = CreateEndpoint(
|
var endpoint2 = CreateEndpoint(
|
||||||
"Admin/ManageUsers",
|
"Admin/ManageUsers",
|
||||||
defaults: new { page = "/Admin/ManageUsers", },
|
defaults: new { page = "/Admin/ManageUsers", },
|
||||||
metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { page = "/Admin/ManageUsers", })) });
|
requiredValues:new { page = "/Admin/ManageUsers", });
|
||||||
|
|
||||||
var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
|
var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
|
||||||
|
|
||||||
|
|
@ -112,11 +112,11 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
var endpoint1 = CreateEndpoint(
|
var endpoint1 = CreateEndpoint(
|
||||||
"About/{id}",
|
"About/{id}",
|
||||||
defaults: new { page = "/About", },
|
defaults: new { page = "/About", },
|
||||||
metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { page = "/About", })) });
|
requiredValues:new { page = "/About", });
|
||||||
var endpoint2 = CreateEndpoint(
|
var endpoint2 = CreateEndpoint(
|
||||||
"Admin/ManageUsers",
|
"Admin/ManageUsers",
|
||||||
defaults: new { page = "/Admin/ManageUsers", },
|
defaults: new { page = "/Admin/ManageUsers", },
|
||||||
metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { page = "/Admin/ManageUsers", })) });
|
requiredValues:new { page = "/Admin/ManageUsers", });
|
||||||
|
|
||||||
var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
|
var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
|
||||||
|
|
||||||
|
|
@ -142,11 +142,11 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
var endpoint1 = CreateEndpoint(
|
var endpoint1 = CreateEndpoint(
|
||||||
"About/{id}",
|
"About/{id}",
|
||||||
defaults: new { page = "/About", },
|
defaults: new { page = "/About", },
|
||||||
metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { page = "/About", })) });
|
requiredValues:new { page = "/About", });
|
||||||
var endpoint2 = CreateEndpoint(
|
var endpoint2 = CreateEndpoint(
|
||||||
"Admin/ManageUsers",
|
"Admin/ManageUsers",
|
||||||
defaults: new { page = "/Admin/ManageUsers", },
|
defaults: new { page = "/Admin/ManageUsers", },
|
||||||
metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { page = "/Admin/ManageUsers", })) });
|
requiredValues:new { page = "/Admin/ManageUsers", });
|
||||||
|
|
||||||
var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
|
var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
|
||||||
|
|
||||||
|
|
@ -175,7 +175,7 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
{
|
{
|
||||||
return new RouteEndpoint(
|
return new RouteEndpoint(
|
||||||
(httpContext) => Task.CompletedTask,
|
(httpContext) => Task.CompletedTask,
|
||||||
RoutePatternFactory.Parse(template, defaults, parameterPolicies: null),
|
RoutePatternFactory.Parse(template, defaults, parameterPolicies: null, requiredValues),
|
||||||
order,
|
order,
|
||||||
new EndpointMetadataCollection(metadata ?? Array.Empty<object>()),
|
new EndpointMetadataCollection(metadata ?? Array.Empty<object>()),
|
||||||
null);
|
null);
|
||||||
|
|
|
||||||
|
|
@ -323,7 +323,7 @@ Hello from page";
|
||||||
var expected =
|
var expected =
|
||||||
@"<a href=""/Accounts/Manage/RenderPartials"">Link inside area</a>
|
@"<a href=""/Accounts/Manage/RenderPartials"">Link inside area</a>
|
||||||
<a href=""/Products/List/old/20"">Link to external area</a>
|
<a href=""/Products/List/old/20"">Link to external area</a>
|
||||||
<a href=""/Accounts"">Link to area action</a>
|
<a href="""">Link to area action</a>
|
||||||
<a href=""/Admin"">Link to non-area page</a>";
|
<a href=""/Admin"">Link to non-area page</a>";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
|
||||||
|
|
@ -1294,8 +1294,6 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||||
Assert.Equal("/", result.Link);
|
Assert.Equal("/", result.Link);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public virtual async Task AttributeRoutedAction_InArea_LinkToConventionalRoutedActionInArea()
|
public virtual async Task AttributeRoutedAction_InArea_LinkToConventionalRoutedActionInArea()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
|
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
|
|
@ -12,8 +12,8 @@
|
||||||
<body>
|
<body>
|
||||||
<h1 style="font-family: cursive;">ASP.NET vNext - About</h1>
|
<h1 style="font-family: cursive;">ASP.NET vNext - About</h1>
|
||||||
<p>| <a href="/" style="background-color: gray;color: white;border-radius: 3px;border: 1px solid black;padding: 3px;font-family: cursive;">My Home</a>
|
<p>| <a href="/" style="background-color: gray;color: white;border-radius: 3px;border: 1px solid black;padding: 3px;font-family: cursive;">My Home</a>
|
||||||
| <a href="/Home/About" style="background-color: gray;color: white;border-radius: 3px;border: 1px solid black;padding: 3px;font-family: cursive;">My About</a>
|
| <a href="/home/about" style="background-color: gray;color: white;border-radius: 3px;border: 1px solid black;padding: 3px;font-family: cursive;">My About</a>
|
||||||
| <a href="/Home/Help" style="background-color: gray;color: white;border-radius: 3px;border: 1px solid black;padding: 3px;font-family: cursive;">My Help</a> |</p>
|
| <a href="/home/help" style="background-color: gray;color: white;border-radius: 3px;border: 1px solid black;padding: 3px;font-family: cursive;">My Help</a> |</p>
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
|
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
|
|
@ -12,8 +12,8 @@
|
||||||
<body>
|
<body>
|
||||||
<h1 style="font-family: cursive;">ASP.NET vNext - Help</h1>
|
<h1 style="font-family: cursive;">ASP.NET vNext - Help</h1>
|
||||||
<p>| <a href="/" style="background-color: gray;color: white;border-radius: 3px;border: 1px solid black;padding: 3px;font-family: cursive;">My Home</a>
|
<p>| <a href="/" style="background-color: gray;color: white;border-radius: 3px;border: 1px solid black;padding: 3px;font-family: cursive;">My Home</a>
|
||||||
| <a href="/Home/About" style="background-color: gray;color: white;border-radius: 3px;border: 1px solid black;padding: 3px;font-family: cursive;">My About</a>
|
| <a href="/home/about" style="background-color: gray;color: white;border-radius: 3px;border: 1px solid black;padding: 3px;font-family: cursive;">My About</a>
|
||||||
| <a href="/Home/Help" style="background-color: gray;color: white;border-radius: 3px;border: 1px solid black;padding: 3px;font-family: cursive;">My Help</a> |</p>
|
| <a href="/home/help" style="background-color: gray;color: white;border-radius: 3px;border: 1px solid black;padding: 3px;font-family: cursive;">My Help</a> |</p>
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
|
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
|
|
@ -19,8 +19,8 @@
|
||||||
<body>
|
<body>
|
||||||
<h1 style="font-family: cursive;">ASP.NET vNext - Home Page</h1>
|
<h1 style="font-family: cursive;">ASP.NET vNext - Home Page</h1>
|
||||||
<p>| <a href="/" style="background-color: gray;color: white;border-radius: 3px;border: 1px solid black;padding: 3px;font-family: cursive;">My Home</a>
|
<p>| <a href="/" style="background-color: gray;color: white;border-radius: 3px;border: 1px solid black;padding: 3px;font-family: cursive;">My Home</a>
|
||||||
| <a href="/Home/About" style="background-color: gray;color: white;border-radius: 3px;border: 1px solid black;padding: 3px;font-family: cursive;">My About</a>
|
| <a href="/home/about" style="background-color: gray;color: white;border-radius: 3px;border: 1px solid black;padding: 3px;font-family: cursive;">My About</a>
|
||||||
| <a href="/Home/Help" style="background-color: gray;color: white;border-radius: 3px;border: 1px solid black;padding: 3px;font-family: cursive;">My Help</a> |</p>
|
| <a href="/home/help" style="background-color: gray;color: white;border-radius: 3px;border: 1px solid black;padding: 3px;font-family: cursive;">My Help</a> |</p>
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
|
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
|
|
@ -12,8 +12,8 @@
|
||||||
<body>
|
<body>
|
||||||
<h1 style="font-family: cursive;">ASP.NET vNext - </h1>
|
<h1 style="font-family: cursive;">ASP.NET vNext - </h1>
|
||||||
<p>| <a href="/" style="background-color: gray;color: white;border-radius: 3px;border: 1px solid black;padding: 3px;font-family: cursive;">My Home</a>
|
<p>| <a href="/" style="background-color: gray;color: white;border-radius: 3px;border: 1px solid black;padding: 3px;font-family: cursive;">My Home</a>
|
||||||
| <a href="/Home/About" style="background-color: gray;color: white;border-radius: 3px;border: 1px solid black;padding: 3px;font-family: cursive;">My About</a>
|
| <a href="/home/about" style="background-color: gray;color: white;border-radius: 3px;border: 1px solid black;padding: 3px;font-family: cursive;">My About</a>
|
||||||
| <a href="/Home/Help" style="background-color: gray;color: white;border-radius: 3px;border: 1px solid black;padding: 3px;font-family: cursive;">My Help</a> |</p>
|
| <a href="/home/help" style="background-color: gray;color: white;border-radius: 3px;border: 1px solid black;padding: 3px;font-family: cursive;">My Help</a> |</p>
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
<div>Items: </div>
|
<div>Items: </div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.IO;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
using Microsoft.AspNetCore.Routing.Internal;
|
||||||
|
|
||||||
|
namespace RoutingWebSite
|
||||||
|
{
|
||||||
|
// This controller is reachable via traditional routing.
|
||||||
|
public class DebugController : Controller
|
||||||
|
{
|
||||||
|
private readonly DfaGraphWriter _graphWriter;
|
||||||
|
private readonly EndpointDataSource _endpointDataSource;
|
||||||
|
|
||||||
|
public DebugController(DfaGraphWriter graphWriter, EndpointDataSource endpointDataSource)
|
||||||
|
{
|
||||||
|
_graphWriter = graphWriter;
|
||||||
|
_endpointDataSource = endpointDataSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IActionResult Graph()
|
||||||
|
{
|
||||||
|
var sw = new StringWriter();
|
||||||
|
_graphWriter.Write(_endpointDataSource, sw);
|
||||||
|
|
||||||
|
return Content(sw.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
using Microsoft.AspNetCore.Routing.Matching;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace RoutingWebSite
|
namespace RoutingWebSite
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue