MVC reaction to RoutePattern required values

This commit is contained in:
James Newton-King 2018-11-29 10:45:57 +13:00
parent 96cd055e05
commit f157b78d93
16 changed files with 338 additions and 1237 deletions

View File

@ -58,6 +58,8 @@ namespace Microsoft.AspNetCore.Mvc.Performance
{
var endpointDataSource = CreateMvcEndpointDataSource(_attributeActionProvider);
var endpoints = endpointDataSource.Endpoints;
AssertHasEndpoints(endpoints);
}
[Benchmark]
@ -66,6 +68,8 @@ namespace Microsoft.AspNetCore.Mvc.Performance
var endpointDataSource = CreateMvcEndpointDataSource(_conventionalActionProvider);
endpointDataSource.ConventionalEndpointInfos.AddRange(_conventionalEndpointInfos);
var endpoints = endpointDataSource.Endpoints;
AssertHasEndpoints(endpoints);
}
private ActionDescriptor CreateAttributeRoutedAction(int id)
@ -110,11 +114,20 @@ namespace Microsoft.AspNetCore.Mvc.Performance
var dataSource = new MvcEndpointDataSource(
actionDescriptorCollectionProvider,
new MvcEndpointInvokerFactory(new ActionInvokerFactory(Array.Empty<IActionInvokerProvider>())),
new MockParameterPolicyFactory());
new MockParameterPolicyFactory(),
new MockRoutePatternTransformer());
return dataSource;
}
private class MockRoutePatternTransformer : RoutePatternTransformer
{
public override RoutePattern SubstituteRequiredValues(RoutePattern original, object requiredValues)
{
return original;
}
}
private class MockActionDescriptorCollectionProvider : IActionDescriptorCollectionProvider
{
public MockActionDescriptorCollectionProvider(List<ActionDescriptor> actionDescriptors)
@ -137,5 +150,13 @@ namespace Microsoft.AspNetCore.Mvc.Performance
throw new NotImplementedException();
}
}
private static void AssertHasEndpoints(IReadOnlyList<Http.Endpoint> endpoints)
{
if (endpoints.Count == 0)
{
throw new InvalidOperationException("Expected endpoints from data source.");
}
}
}
}

View File

@ -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();
}
}
}

View File

@ -5,11 +5,13 @@ using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Internal;
using Microsoft.Extensions.DependencyInjection;
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.
public void ConfigureServices(IServiceCollection services)
{
services.AddRouting(options =>
{
options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer);
});
services.AddMvc().SetCompatibilityVersion(Microsoft.AspNetCore.Mvc.CompatibilityVersion.Latest);
}
@ -37,6 +43,27 @@ namespace MvcSandbox
name: "default",
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.MapHealthChecks("/healthz");
@ -50,7 +77,7 @@ namespace MvcSandbox
private static Task WriteEndpoints(HttpContext httpContext)
{
var dataSource = httpContext.RequestServices.GetRequiredService<CompositeEndpointDataSource>();
var dataSource = httpContext.RequestServices.GetRequiredService<EndpointDataSource>();
var sb = new StringBuilder();
sb.AppendLine("Endpoints:");
@ -81,6 +108,7 @@ namespace MvcSandbox
factory
.AddConsole()
.AddDebug();
factory.SetMinimumLevel(LogLevel.Trace);
})
.UseIISIntegration()
.UseKestrel()

View File

@ -5,13 +5,11 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ActionConstraints;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Routing;
@ -25,6 +23,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
private readonly IActionDescriptorCollectionProvider _actions;
private readonly MvcEndpointInvokerFactory _invokerFactory;
private readonly ParameterPolicyFactory _parameterPolicyFactory;
private readonly RoutePatternTransformer _routePatternTransformer;
// The following are protected by this lock for WRITES only. This pattern is similar
// to DefaultActionDescriptorChangeProvider - see comments there for details on
@ -37,26 +36,13 @@ namespace Microsoft.AspNetCore.Mvc.Routing
public MvcEndpointDataSource(
IActionDescriptorCollectionProvider actions,
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;
_invokerFactory = invokerFactory;
_parameterPolicyFactory = parameterPolicyFactory;
_routePatternTransformer = routePatternTransformer;
ConventionalEndpointInfos = new List<MvcEndpointInfo>();
AttributeRoutingConventionResolvers = new List<Func<ActionDescriptor, DefaultEndpointConventionBuilder>>();
@ -115,7 +101,6 @@ namespace Microsoft.AspNetCore.Mvc.Routing
lock (_lock)
{
var endpoints = new List<Endpoint>();
StringBuilder patternStringBuilder = null;
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.
var conventionalRouteOrder = 1;
// 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
// area/controller/action parameter parts replaced with literals
//
// e.g. {controller}/{action} with HomeController.Index and HomeController.Login
// would result in endpoints:
// - Home/Index
// - Home/Login
// 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 action
// route values on the pattern.
foreach (var endpointInfo in ConventionalEndpointInfos)
{
// An 'endpointInfo' is applicable if:
// 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
var isApplicable = true;
foreach (var routeKey in action.RouteValues.Keys)
{
if (!MatchRouteValue(action, endpointInfo, routeKey))
{
isApplicable = false;
break;
}
}
// 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
var updatedRoutePattern = _routePatternTransformer.SubstituteRequiredValues(endpointInfo.ParsedPattern, action.RouteValues);
if (!isApplicable)
if (updatedRoutePattern == null)
{
continue;
}
conventionalRouteOrder = CreateEndpoints(
endpoints,
ref patternStringBuilder,
var endpoint = CreateEndpoint(
action,
conventionalRouteOrder,
endpointInfo.ParsedPattern,
endpointInfo.MergedDefaults,
endpointInfo.Defaults,
updatedRoutePattern,
endpointInfo.Name,
conventionalRouteOrder++,
endpointInfo.DataTokens,
endpointInfo.ParameterPolicies,
suppressLinkGeneration: false,
suppressPathMatching: false,
false,
false,
endpointInfo.Conventions);
endpoints.Add(endpoint);
}
}
else
@ -185,20 +153,27 @@ namespace Microsoft.AspNetCore.Mvc.Routing
var attributeRoutePattern = RoutePatternFactory.Parse(action.AttributeRouteInfo.Template);
CreateEndpoints(
endpoints,
ref patternStringBuilder,
// Modify the route and required values to ensure required values can be successfully subsituted.
// Subsitituting required values into an attribute route pattern should always succeed.
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.AttributeRouteInfo.Order,
attributeRoutePattern,
attributeRoutePattern.Defaults,
nonInlineDefaults: null,
updatedRoutePattern,
action.AttributeRouteInfo.Name,
action.AttributeRouteInfo.Order,
dataTokens: null,
allParameterPolicies: null,
action.AttributeRouteInfo.SuppressLinkGeneration,
action.AttributeRouteInfo.SuppressPathMatching,
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)
{
foreach (var filter in AttributeRoutingConventionResolvers)
@ -234,339 +261,10 @@ namespace Microsoft.AspNetCore.Mvc.Routing
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(
ActionDescriptor action,
IDictionary<string, string> actionRouteValues,
RoutePattern routePattern,
string routeName,
string patternRawText,
IEnumerable<RoutePatternPathSegment> segments,
object nonInlineDefaults,
int order,
RouteValueDictionary dataTokens,
bool suppressLinkGeneration,
@ -583,16 +281,12 @@ namespace Microsoft.AspNetCore.Mvc.Routing
return invoker.InvokeAsync();
};
var defaults = new RouteValueDictionary(nonInlineDefaults);
EnsureRequiredValuesInDefaults(actionRouteValues, defaults, segments);
var model = new RouteEndpointModel(requestDelegate, RoutePatternFactory.Pattern(patternRawText, defaults, parameterPolicies: null, segments), order);
var model = new RouteEndpointModel(requestDelegate, routePattern, order);
AddEndpointMetadata(
model.Metadata,
action,
routeName,
new RouteValueDictionary(actionRouteValues),
dataTokens,
suppressLinkGeneration,
suppressPathMatching);
@ -616,7 +310,6 @@ namespace Microsoft.AspNetCore.Mvc.Routing
IList<object> metadata,
ActionDescriptor action,
string routeName,
RouteValueDictionary requiredValues,
RouteValueDictionary dataTokens,
bool suppressLinkGeneration,
bool suppressPathMatching)
@ -637,7 +330,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
metadata.Add(new DataTokensMetadata(dataTokens));
}
metadata.Add(new RouteValuesAddressMetadata(routeName, requiredValues));
metadata.Add(new RouteNameMetadata(routeName));
// Add filter descriptors to endpoint metadata
if (action.FilterDescriptors != null && action.FilterDescriptors.Count > 0)
@ -685,33 +378,5 @@ namespace Microsoft.AspNetCore.Mvc.Routing
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;
}
}
}
}
}

View File

@ -25,11 +25,11 @@ namespace Microsoft.AspNetCore.Routing
var endpoint1 = CreateEndpoint(
"Home/Index/{id}",
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(
"Home/Index/{id?}",
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);
@ -55,11 +55,11 @@ namespace Microsoft.AspNetCore.Routing
var endpoint1 = CreateEndpoint(
"Home/Index/{id}",
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(
"Home/Index/{id?}",
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);
@ -83,11 +83,11 @@ namespace Microsoft.AspNetCore.Routing
var endpoint1 = CreateEndpoint(
"Home/Index/{id}",
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(
"Home/Index/{id?}",
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);
@ -114,11 +114,11 @@ namespace Microsoft.AspNetCore.Routing
var endpoint1 = CreateEndpoint(
"Home/Index/{id}",
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(
"Home/Index/{id?}",
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);
@ -144,11 +144,11 @@ namespace Microsoft.AspNetCore.Routing
var endpoint1 = CreateEndpoint(
"Home/Index/{id}",
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(
"Home/Index/{id?}",
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);
@ -177,7 +177,7 @@ namespace Microsoft.AspNetCore.Routing
{
return new RouteEndpoint(
(httpContext) => Task.CompletedTask,
RoutePatternFactory.Parse(template, defaults, parameterPolicies: null),
RoutePatternFactory.Parse(template, defaults, parameterPolicies: null, requiredValues),
order,
new EndpointMetadataCollection(metadata ?? Array.Empty<object>()),
null);

View File

@ -114,7 +114,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
"Home/Index",
defaults: 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 });
// Act
@ -273,19 +273,20 @@ namespace Microsoft.AspNetCore.Mvc.Routing
object requiredValues = null,
int order = 0,
string routeName = null,
EndpointMetadataCollection metadataCollection = null)
IList<object> metadata = null)
{
if (metadataCollection == null)
metadata = metadata ?? new List<object>();
if (routeName != null)
{
metadataCollection = new EndpointMetadataCollection(
new RouteValuesAddressMetadata(routeName, new RouteValueDictionary(requiredValues)));
metadata.Add(new RouteNameMetadata(routeName));
}
return new RouteEndpoint(
(httpContext) => Task.CompletedTask,
RoutePatternFactory.Parse(template, defaults, parameterPolicies: null),
RoutePatternFactory.Parse(template, defaults, parameterPolicies: null, requiredValues),
order,
metadataCollection,
new EndpointMetadataCollection(metadata),
null);
}

View File

@ -25,11 +25,11 @@ namespace Microsoft.AspNetCore.Routing
var endpoint1 = CreateEndpoint(
"About/{id}",
defaults: new { page = "/About", },
metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { page = "/About", })) });
requiredValues:new { page = "/About", });
var endpoint2 = CreateEndpoint(
"Admin/ManageUsers/{handler?}",
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);
@ -54,11 +54,11 @@ namespace Microsoft.AspNetCore.Routing
var endpoint1 = CreateEndpoint(
"About/{id}",
defaults: new { page = "/About", },
metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { page = "/About", })) });
requiredValues:new { page = "/About", });
var endpoint2 = CreateEndpoint(
"Admin/ManageUsers/{handler?}",
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);
@ -82,11 +82,11 @@ namespace Microsoft.AspNetCore.Routing
var endpoint1 = CreateEndpoint(
"About/{id}",
defaults: new { page = "/About", },
metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { page = "/About", })) });
requiredValues:new { page = "/About", });
var endpoint2 = CreateEndpoint(
"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);
@ -112,11 +112,11 @@ namespace Microsoft.AspNetCore.Routing
var endpoint1 = CreateEndpoint(
"About/{id}",
defaults: new { page = "/About", },
metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { page = "/About", })) });
requiredValues:new { page = "/About", });
var endpoint2 = CreateEndpoint(
"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);
@ -142,11 +142,11 @@ namespace Microsoft.AspNetCore.Routing
var endpoint1 = CreateEndpoint(
"About/{id}",
defaults: new { page = "/About", },
metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { page = "/About", })) });
requiredValues:new { page = "/About", });
var endpoint2 = CreateEndpoint(
"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);
@ -175,7 +175,7 @@ namespace Microsoft.AspNetCore.Routing
{
return new RouteEndpoint(
(httpContext) => Task.CompletedTask,
RoutePatternFactory.Parse(template, defaults, parameterPolicies: null),
RoutePatternFactory.Parse(template, defaults, parameterPolicies: null, requiredValues),
order,
new EndpointMetadataCollection(metadata ?? Array.Empty<object>()),
null);

View File

@ -323,7 +323,7 @@ Hello from page";
var expected =
@"<a href=""/Accounts/Manage/RenderPartials"">Link inside 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>";
// Act

View File

@ -1294,8 +1294,6 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Equal("/", result.Link);
}
[Fact]
public virtual async Task AttributeRoutedAction_InArea_LinkToConventionalRoutedActionInArea()
{

View File

@ -1,4 +1,4 @@

<!DOCTYPE html>
<html>
@ -12,8 +12,8 @@
<body>
<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>
| <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/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>
<div>

View File

@ -1,4 +1,4 @@

<!DOCTYPE html>
<html>
@ -12,8 +12,8 @@
<body>
<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>
| <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/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>
<div>

View File

@ -1,4 +1,4 @@

<!DOCTYPE html>
<html>
@ -19,8 +19,8 @@
<body>
<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>
| <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/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>
<div>

View File

@ -1,4 +1,4 @@

<!DOCTYPE html>
<html>
@ -12,8 +12,8 @@
<body>
<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>
| <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/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>
<div>
<div>Items: </div>

View File

@ -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());
}
}
}

View File

@ -1,12 +1,14 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Matching;
using Microsoft.Extensions.DependencyInjection;
namespace RoutingWebSite