diff --git a/build/dependencies.props b/build/dependencies.props
index 84f3631ef7..c0129c246e 100644
--- a/build/dependencies.props
+++ b/build/dependencies.props
@@ -7,7 +7,7 @@
is not otherwise referenced. They avoid unnecessary changes to the Universe build graph or to product
dependencies. Do not use these properties elsewhere.
-->
-
+
0.9.9
0.10.13
2.1.1
@@ -16,90 +16,90 @@
0.43.0
2.1.1.1
2.1.1
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-a-preview3-22cors-16556
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
2.0.0
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-a-preview3-address-scheme-17059
- 2.2.0-a-preview3-address-scheme-17059
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-a-rtm-allow-required-parameters-17081
+ 2.2.0-a-rtm-allow-required-parameters-17081
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
5.2.6
15.6.82
2.8.0
2.8.0
- 2.2.0-preview3-35359
+ 2.2.0-rtm-35519
1.7.0
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
2.1.0
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
2.0.9
2.1.3
- 2.2.0-preview3-26927-02
- 2.2.0-preview3-35359
- 2.2.0-preview3-35359
+ 2.2.0-preview3-27014-02
+ 2.2.0-rtm-35519
+ 2.2.0-rtm-35519
15.6.1
- 4.7.49
+ 4.10.0
2.0.3
1.0.1
11.0.2
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcEndpointDataSource.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcEndpointDataSource.cs
index d371d02f79..bf8df39064 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcEndpointDataSource.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcEndpointDataSource.cs
@@ -222,26 +222,63 @@ namespace Microsoft.AspNetCore.Mvc.Internal
bool suppressPathMatching)
{
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}/{id?} against HomeController.Index
- // can resolve to the following endpoints:
- // - /Home/Index/{id?}
- // - /Home
+ // e.g. Matching pattern {controller=Home}/{action=Index} against HomeController.Index
+ // can resolve to the following endpoints: (sorted by RouteEndpoint.Order)
// - /
- if (UseDefaultValuePlusRemainingSegmentsOptional(i, action, allDefaults, newPathSegments))
+ // - /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);
+ endpoints.Add(ep);
+
+ hasLinkGenerationEndpoint = true;
+ }
+
var subPathSegments = newPathSegments.Take(i);
var subEndpoint = CreateEndpoint(
action,
+ resolvedRouteValues,
name,
GetPattern(ref patternStringBuilder, subPathSegments),
subPathSegments,
- nonInlineDefaults,
+ nonInlineDefaultsCopy,
routeOrder++,
dataTokens,
suppressLinkGeneration,
@@ -249,15 +286,83 @@ namespace Microsoft.AspNetCore.Mvc.Internal
endpoints.Add(subEndpoint);
}
- List segmentParts = null; // Initialize only as needed
- var segment = newPathSegments[i];
- for (var j = 0; j < segment.Parts.Count; j++)
- {
- var part = segment.Parts[j];
+ UpdatePathSegments(i, action, resolvedRouteValues, routePattern, newPathSegments, ref allParameterPolicies);
+ }
- if (part.IsParameter &&
- part is RoutePatternParameterPart parameterPart &&
- action.RouteValues.ContainsKey(parameterPart.Name))
+ var finalEndpoint = CreateEndpoint(
+ action,
+ resolvedRouteValues,
+ name,
+ GetPattern(ref patternStringBuilder, newPathSegments),
+ newPathSegments,
+ nonInlineDefaultsCopy,
+ routeOrder++,
+ dataTokens,
+ suppressLinkGeneration,
+ suppressPathMatching);
+ endpoints.Add(finalEndpoint);
+
+ return routeOrder;
+
+ string GetPattern(ref StringBuilder sb, IEnumerable segments)
+ {
+ if (sb == null)
+ {
+ sb = new StringBuilder();
+ }
+
+ RoutePatternWriter.WriteString(sb, segments);
+ var rawPattern = sb.ToString();
+ sb.Length = 0;
+
+ return rawPattern;
+ }
+ }
+
+ private static IDictionary ResolveActionRouteValues(ActionDescriptor action, IReadOnlyDictionary allDefaults)
+ {
+ Dictionary 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(action.RouteValues, StringComparer.OrdinalIgnoreCase);
+ }
+
+ resolvedRequiredValues[kvp.Key] = defaultValue;
+ }
+ }
+
+ return resolvedRequiredValues ?? action.RouteValues;
+ }
+
+ private void UpdatePathSegments(
+ int i,
+ ActionDescriptor action,
+ IDictionary resolvedRequiredValues,
+ RoutePattern routePattern,
+ List newPathSegments,
+ ref IDictionary> allParameterPolicies)
+ {
+ List 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)
{
@@ -268,9 +373,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
allParameterPolicies = MvcEndpointInfo.BuildParameterPolicies(routePattern.Parameters, _parameterPolicyFactory);
}
- // Replace parameter with literal value
- var parameterRouteValue = action.RouteValues[parameterPart.Name];
-
// 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.
@@ -297,47 +399,21 @@ namespace Microsoft.AspNetCore.Mvc.Internal
}
}
}
-
- // A parameter part was replaced so replace segment with updated parts
- if (segmentParts != null)
- {
- newPathSegments[i] = RoutePatternFactory.Segment(segmentParts);
- }
}
- var endpoint = CreateEndpoint(
- action,
- name,
- GetPattern(ref patternStringBuilder, newPathSegments),
- newPathSegments,
- nonInlineDefaults,
- routeOrder++,
- dataTokens,
- suppressLinkGeneration,
- suppressPathMatching);
- endpoints.Add(endpoint);
-
- return routeOrder;
-
- string GetPattern(ref StringBuilder sb, IEnumerable segments)
+ // A parameter part was replaced so replace segment with updated parts
+ if (segmentParts != null)
{
- if (sb == null)
- {
- sb = new StringBuilder();
- }
-
- RoutePatternWriter.WriteString(sb, segments);
- var rawPattern = sb.ToString();
- sb.Length = 0;
-
- return rawPattern;
+ newPathSegments[i] = RoutePatternFactory.Segment(segmentParts);
}
}
private bool UseDefaultValuePlusRemainingSegmentsOptional(
int segmentIndex,
ActionDescriptor action,
+ IDictionary resolvedRequiredValues,
IReadOnlyDictionary allDefaults,
+ ref RouteValueDictionary nonInlineDefaults,
List pathSegments)
{
// Check whether the remaining segments are all optional and one or more of them is
@@ -352,22 +428,33 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var part = segment.Parts[j];
if (part.IsParameter && part is RoutePatternParameterPart parameterPart)
{
- if (parameterPart.IsOptional || parameterPart.IsCatchAll)
+ if (allDefaults.TryGetValue(parameterPart.Name, out var v))
{
- continue;
- }
-
- if (action.RouteValues.ContainsKey(parameterPart.Name))
- {
- if (allDefaults.TryGetValue(parameterPart.Name, out var v)
- && v is string defaultValue
- && action.RouteValues.TryGetValue(parameterPart.Name, out var routeValue)
- && string.Equals(defaultValue, routeValue, StringComparison.OrdinalIgnoreCase))
+ 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 == ".")
@@ -441,6 +528,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
private RouteEndpoint CreateEndpoint(
ActionDescriptor action,
+ IDictionary actionRouteValues,
string routeName,
string patternRawText,
IEnumerable segments,
@@ -461,12 +549,12 @@ namespace Microsoft.AspNetCore.Mvc.Internal
};
var defaults = new RouteValueDictionary(nonInlineDefaults);
- EnsureRequiredValuesInDefaults(action.RouteValues, defaults);
+ EnsureRequiredValuesInDefaults(actionRouteValues, defaults, segments);
var metadataCollection = BuildEndpointMetadata(
action,
routeName,
- new RouteValueDictionary(action.RouteValues),
+ new RouteValueDictionary(actionRouteValues),
dataTokens,
suppressLinkGeneration,
suppressPathMatching);
@@ -554,7 +642,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
return metadataCollection;
}
- // Ensure required values are a subset of defaults
+ // Ensure route values are a subset of defaults
// Examples:
//
// Template: {controller}/{action}/{category}/{id?}
@@ -568,9 +656,12 @@ namespace Microsoft.AspNetCore.Mvc.Internal
// 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 requiredValues, RouteValueDictionary defaults)
+ private void EnsureRequiredValuesInDefaults(
+ IDictionary routeValues,
+ RouteValueDictionary defaults,
+ IEnumerable segments)
{
- foreach (var kvp in requiredValues)
+ foreach (var kvp in routeValues)
{
if (kvp.Value != null)
{
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/RoutePatternWriter.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/RoutePatternWriter.cs
index 5beeb6baa6..3f0e0f6445 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/RoutePatternWriter.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/RoutePatternWriter.cs
@@ -40,6 +40,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal
if (parameterPart.IsCatchAll)
{
sb.Append("*");
+ if (!parameterPart.EncodeSlashes)
+ {
+ sb.Append("*");
+ }
}
sb.Append(parameterPart.Name);
foreach (var item in parameterPart.ParameterPolicies)
diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/MvcEndpointDataSourceTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/MvcEndpointDataSourceTests.cs
index 24622f85cd..618b281eba 100644
--- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/MvcEndpointDataSourceTests.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/MvcEndpointDataSourceTests.cs
@@ -146,22 +146,29 @@ namespace Microsoft.AspNetCore.Mvc.Internal
private static TheoryData GetSingleActionData(bool isConventionalRouting)
{
- var data = new TheoryData
+ var data = new TheoryData
{
- {"{controller}/{action}/{id?}", new[] { "TestController/TestAction/{id?}" }},
- {"{controller}/{id?}", isConventionalRouting ? new string[] { } : new[] { "TestController/{id?}" }},
- {"{action}/{id?}", isConventionalRouting ? new string[] { } : new[] { "TestAction/{id?}" }},
- {"{Controller}/{Action}/{id?}", new[] { "TestController/TestAction/{id?}" }},
- {"{CONTROLLER}/{ACTION}/{id?}", new[] { "TestController/TestAction/{id?}" }},
- {"{controller}/{action=TestAction}", new[] { "TestController", "TestController/TestAction" }},
- {"{controller}/{action=TestAction}/{id?}", new[] { "TestController", "TestController/TestAction/{id?}" }},
- {"{controller=TestController}/{action=TestAction}/{id?}", new[] { "", "TestController", "TestController/TestAction/{id?}" }},
- {"{controller}/{action}/{*catchAll}", new[] { "TestController/TestAction/{*catchAll}" }},
- {"{controller}/{action=TestAction}/{*catchAll}", new[] { "TestController", "TestController/TestAction/{*catchAll}" }},
- {"{controller}/{action=TestAction}/{id?}/{*catchAll}", new[] { "TestController", "TestController/TestAction/{id?}/{*catchAll}" }},
- {"{controller}/{action}.{ext?}", new[] { "TestController/TestAction.{ext?}" }},
- {"{controller}/{action=TestAction}.{ext?}", new[] { "TestController", "TestController/TestAction.{ext?}" }},
- {"{controller:upper-case}/{action=TestAction}.{ext?}", new[] { "TESTCONTROLLER", "TESTCONTROLLER/TestAction.{ext?}" }},
+ {"{controller}/{action}/{id?}", null, new[] { "TestController/TestAction/{id?}" }},
+ {"{controller}/{id?}", null, isConventionalRouting ? new string[] { } : new[] { "TestController/{id?}" }},
+ {"{action}/{id?}", null, isConventionalRouting ? new string[] { } : new[] { "TestAction/{id?}" }},
+ {"{Controller}/{Action}/{id?}", null, new[] { "TestController/TestAction/{id?}" }},
+ {"{Controller}/{Action}/{id?}/{more?}", null, new[] { "TestController/TestAction/{id?}/{more?}" }},
+ {"{CONTROLLER}/{ACTION}/{id?}", null, new[] { "TestController/TestAction/{id?}" }},
+ {"{controller}/{action=TestAction}", "TestController/{action=TestAction}", new[] { "TestController", "TestController/TestAction" }},
+ {"{controller}/{action=TestAction}/{id?}", "TestController/{action=TestAction}/{id?}", new[] { "TestController", "TestController/TestAction/{id?}" }},
+ {"{controller}/{action=TESTACTION}/{id?}", "TestController/{action=TESTACTION}/{id?}", new[] { "TestController", "TestController/TESTACTION/{id?}" }},
+ {"{controller}/{action=TestAction}/{id?}/{more}", null, new[] { "TestController/TestAction/{id?}/{more}" }},
+ {"{controller=TestController}/{action=TestAction}/{id?}", "{controller=TestController}/{action=TestAction}/{id?}", new[] { "", "TestController", "TestController/TestAction/{id?}" }},
+ {"{controller=TestController}/{action=TestAction}/{id?}/{more?}", "{controller=TestController}/{action=TestAction}/{id?}/{more?}", new[] { "", "TestController", "TestController/TestAction/{id?}/{more?}" }},
+ {"{controller}/{action}/{*catchAll}", null, new[] { "TestController/TestAction/{*catchAll}" }},
+ {"{controller}/{action=TestAction}/{*catchAll}", "TestController/{action=TestAction}/{*catchAll}", new[] { "TestController", "TestController/TestAction/{*catchAll}" }},
+ {"{controller}/{action=TestAction}/{id?}/{*catchAll}", "TestController/{action=TestAction}/{id?}/{*catchAll}", new[] { "TestController", "TestController/TestAction/{id?}/{*catchAll}" }},
+ {"{controller}/{action=TestAction}/{id?}/{**catchAll}", "TestController/{action=TestAction}/{id?}/{**catchAll}", new[] { "TestController", "TestController/TestAction/{id?}/{**catchAll}" }},
+ {"{controller}/{action}.{ext?}", null, new[] { "TestController/TestAction.{ext?}" }},
+ {"{controller}/{action=TestAction}.{ext?}", "TestController/{action=TestAction}.{ext?}", new[] { "TestController", "TestController/TestAction.{ext?}" }},
+ {"{controller}/{action=TestAction}.{ext?}/{more?}", "TestController/{action=TestAction}.{ext?}/{more?}", new[] { "TestController", "TestController/TestAction.{ext?}/{more?}" }},
+ {"{controller}/{action=TestAction}.{ext?}/{more}", null, new[] { "TestController/TestAction.{ext?}/{more}" }},
+ {"{controller:upper-case}/{action:upper-case=TestAction}.{ext?}", "TESTCONTROLLER/{action:upper-case=TestAction}.{ext?}", new[] { "TESTCONTROLLER", "TESTCONTROLLER/TESTACTION.{ext?}" }},
};
return data;
@@ -169,7 +176,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
[Theory]
[MemberData(nameof(GetSingleActionData_Conventional))]
- public void Endpoints_Conventional_SingleAction(string endpointInfoRoute, string[] finalEndpointPatterns)
+ public void Endpoints_Conventional_SingleAction(string endpointInfoRoute, string suppressMatchingTemplate, string[] finalEndpointPatterns)
{
// Arrange
var actionDescriptorCollection = GetActionDescriptorCollection(
@@ -178,9 +185,17 @@ namespace Microsoft.AspNetCore.Mvc.Internal
dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, endpointInfoRoute));
// Act
- var endpoints = dataSource.Endpoints;
+ var endpoints = dataSource.Endpoints.ToList();
// Assert
+
+ // Ensure there are no endpoints with duplicate Order values
+ Assert.DoesNotContain(endpoints.GroupBy(e => Assert.IsType(e).Order), g => g.Count() > 1);
+
+ endpoints = endpoints.OrderBy(e => Assert.IsType(e).Order).ToList();
+
+ AssertSuppressMatchingTemplate(suppressMatchingTemplate, endpoints);
+
var inspectors = finalEndpointPatterns
.Select(t => new Action(e => Assert.Equal(t, Assert.IsType(e).RoutePattern.RawText)))
.ToArray();
@@ -191,7 +206,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
[Theory]
[MemberData(nameof(GetSingleActionData_Attribute))]
- public void Endpoints_AttributeRouting_SingleAction(string endpointInfoRoute, string[] finalEndpointPatterns)
+ public void Endpoints_AttributeRouting_SingleAction(string endpointInfoRoute, string suppressMatchingTemplate, string[] finalEndpointPatterns)
{
// Arrange
var actionDescriptorCollection = GetActionDescriptorCollection(
@@ -200,7 +215,14 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection);
// Act
- var endpoints = dataSource.Endpoints;
+ var endpoints = dataSource.Endpoints.ToList();
+
+ // Ensure there are no endpoints with duplicate Order values
+ Assert.DoesNotContain(endpoints.GroupBy(e => Assert.IsType(e).Order), g => g.Count() > 1);
+
+ endpoints = endpoints.OrderBy(e => Assert.IsType(e).Order).ToList();
+
+ AssertSuppressMatchingTemplate(suppressMatchingTemplate, endpoints);
// Assert
var inspectors = finalEndpointPatterns
@@ -212,14 +234,14 @@ namespace Microsoft.AspNetCore.Mvc.Internal
}
[Theory]
- [InlineData("{area}/{controller}/{action}/{id?}", new[] { "TestArea/TestController/TestAction/{id?}" })]
- [InlineData("{controller}/{action}/{id?}", new string[] { })]
- [InlineData("{area=TestArea}/{controller}/{action}/{id?}", new[] { "TestArea/TestController/TestAction/{id?}" })]
- [InlineData("{area=TestArea}/{controller}/{action=TestAction}/{id?}", new[] { "TestArea/TestController", "TestArea/TestController/TestAction/{id?}" })]
- [InlineData("{area=TestArea}/{controller=TestController}/{action=TestAction}/{id?}", new[] { "", "TestArea", "TestArea/TestController", "TestArea/TestController/TestAction/{id?}" })]
- [InlineData("{area:exists}/{controller}/{action}/{id?}", new[] { "TestArea/TestController/TestAction/{id?}" })]
- [InlineData("{area:exists:upper-case}/{controller}/{action}/{id?}", new[] { "TESTAREA/TestController/TestAction/{id?}" })]
- public void Endpoints_AreaSingleAction(string endpointInfoRoute, string[] finalEndpointTemplates)
+ [InlineData("{area}/{controller}/{action}/{id?}", null, new[] { "TestArea/TestController/TestAction/{id?}" })]
+ [InlineData("{controller}/{action}/{id?}", null, new string[] { })]
+ [InlineData("{area=TestArea}/{controller}/{action}/{id?}", null, new[] { "TestArea/TestController/TestAction/{id?}" })]
+ [InlineData("{area=TestArea}/{controller}/{action=TestAction}/{id?}", "TestArea/TestController/{action=TestAction}/{id?}", new[] { "TestArea/TestController", "TestArea/TestController/TestAction/{id?}"})]
+ [InlineData("{area=TestArea}/{controller=TestController}/{action=TestAction}/{id?}", "{area=TestArea}/{controller=TestController}/{action=TestAction}/{id?}", new[] { "", "TestArea", "TestArea/TestController", "TestArea/TestController/TestAction/{id?}" })]
+ [InlineData("{area:exists}/{controller}/{action}/{id?}", null, new[] { "TestArea/TestController/TestAction/{id?}" })]
+ [InlineData("{area:exists:upper-case}/{controller}/{action}/{id?}", null, new[] { "TESTAREA/TestController/TestAction/{id?}" })]
+ public void Endpoints_AreaSingleAction(string endpointInfoRoute, string suppressMatchingTemplate, string[] finalEndpointTemplates)
{
// Arrange
var actionDescriptorCollection = GetActionDescriptorCollection(
@@ -240,9 +262,17 @@ namespace Microsoft.AspNetCore.Mvc.Internal
dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, endpointInfoRoute, serviceProvider: services.BuildServiceProvider()));
// Act
- var endpoints = dataSource.Endpoints;
+ var endpoints = dataSource.Endpoints.ToList();
// Assert
+
+ // Ensure there are no endpoints with duplicate Order values
+ Assert.DoesNotContain(endpoints.GroupBy(e => Assert.IsType(e).Order), g => g.Count() > 1);
+
+ endpoints = endpoints.OrderBy(e => Assert.IsType(e).Order).ToList();
+
+ AssertSuppressMatchingTemplate(suppressMatchingTemplate, endpoints);
+
var inspectors = finalEndpointTemplates
.Select(t => new Action(e => Assert.Equal(t, Assert.IsType(e).RoutePattern.RawText)))
.ToArray();
@@ -251,6 +281,17 @@ namespace Microsoft.AspNetCore.Mvc.Internal
Assert.Collection(endpoints, inspectors);
}
+ private static void AssertSuppressMatchingTemplate(string suppressMatchingTemplate, List endpoints)
+ {
+ if (suppressMatchingTemplate != null)
+ {
+ var suppressMatchingEndpoint = endpoints.First();
+ Assert.True(suppressMatchingEndpoint.Metadata.GetMetadata()?.SuppressMatching);
+ Assert.Equal(suppressMatchingTemplate, Assert.IsType(suppressMatchingEndpoint).RoutePattern.RawText);
+ endpoints.Remove(suppressMatchingEndpoint);
+ }
+ }
+
[Fact]
public void Endpoints_SingleAction_ConventionalRoute_ContainsParameterWithNullRequiredRouteValue()
{
@@ -304,6 +345,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
// Assert
Assert.Collection(endpoints,
+ (e) =>
+ {
+ Assert.Equal("TestController/{action=TestAction}", Assert.IsType(e).RoutePattern.RawText);
+ Assert.True(e.Metadata.GetMetadata().SuppressMatching);
+ },
(e) => Assert.Equal("TestController", Assert.IsType(e).RoutePattern.RawText),
(e) => Assert.Equal("TestController/TestAction", Assert.IsType(e).RoutePattern.RawText));
}
@@ -332,6 +378,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
// Assert
Assert.Collection(endpoints1,
+ (e) => Assert.Equal("TestController/{action=TestAction}", Assert.IsType(e).RoutePattern.RawText),
(e) => Assert.Equal("TestController", Assert.IsType(e).RoutePattern.RawText),
(e) => Assert.Equal("TestController/TestAction", Assert.IsType(e).RoutePattern.RawText));
Assert.Same(endpoints1, endpoints2);
@@ -373,6 +420,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var endpoints = dataSource.Endpoints;
Assert.Collection(endpoints,
+ (e) => Assert.Equal("TestController/{action=TestAction}", Assert.IsType(e).RoutePattern.RawText),
(e) => Assert.Equal("TestController", Assert.IsType(e).RoutePattern.RawText),
(e) => Assert.Equal("TestController/TestAction", Assert.IsType(e).RoutePattern.RawText));
@@ -720,10 +768,29 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var endpoints = dataSource.Endpoints;
// Assert
- var endpoint = Assert.Single(endpoints);
- var matcherEndpoint = Assert.IsType(endpoint);
- Assert.Equal("Foo/Bar/{subscription=general}", matcherEndpoint.RoutePattern.RawText);
- AssertIsSubset(expectedDefaults, matcherEndpoint.RoutePattern.Defaults);
+ Assert.Collection(
+ endpoints,
+ (ep) =>
+ {
+ var matcherEndpoint = Assert.IsType(ep);
+ Assert.Equal("Foo/Bar/{subscription=general}", matcherEndpoint.RoutePattern.RawText);
+ Assert.Equal(1, matcherEndpoint.Order);
+ AssertMatchingSuppressed(matcherEndpoint, true);
+ },
+ (ep) =>
+ {
+ var matcherEndpoint = Assert.IsType(ep);
+ Assert.Equal("Foo/Bar", matcherEndpoint.RoutePattern.RawText);
+ Assert.Equal(2, matcherEndpoint.Order);
+ AssertMatchingSuppressed(matcherEndpoint, false);
+ },
+ (ep) =>
+ {
+ var matcherEndpoint = Assert.IsType(ep);
+ Assert.Equal("Foo/Bar/{subscription=general}", matcherEndpoint.RoutePattern.RawText);
+ Assert.Equal(3, matcherEndpoint.Order);
+ AssertMatchingSuppressed(matcherEndpoint, false);
+ });
}
[Fact]
@@ -780,6 +847,464 @@ namespace Microsoft.AspNetCore.Mvc.Internal
AssertIsSubset(expectedDefaults, matcherEndpoint.RoutePattern.Defaults);
}
+ [Fact]
+ public void Endpoints_ConventionalRoutes_NonDefaultAndDefaultValuesEndingWithOptional_IncludeFullRouteAsHighPriority()
+ {
+ // Arrange
+ var actionDescriptorCollection = GetActionDescriptorCollection(
+ new { controller = "Home", action = "Index" });
+ var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection);
+ dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(
+ name: string.Empty,
+ template: "{controller}/{action=Index}/{id?}"));
+
+ // Act
+ var endpoints = dataSource.Endpoints;
+
+ // Assert
+ Assert.Collection(
+ endpoints,
+ (ep) =>
+ {
+ var matcherEndpoint = Assert.IsType(ep);
+ Assert.Equal("Home/{action=Index}/{id?}", matcherEndpoint.RoutePattern.RawText);
+ Assert.Equal(1, matcherEndpoint.Order);
+ AssertMatchingSuppressed(matcherEndpoint, true);
+ },
+ (ep) =>
+ {
+ var matcherEndpoint = Assert.IsType(ep);
+ Assert.Equal("Home", matcherEndpoint.RoutePattern.RawText);
+ Assert.Equal(2, matcherEndpoint.Order);
+ AssertMatchingSuppressed(matcherEndpoint, false);
+ },
+ (ep) =>
+ {
+ var matcherEndpoint = Assert.IsType(ep);
+ Assert.Equal("Home/Index/{id?}", matcherEndpoint.RoutePattern.RawText);
+ Assert.Equal(3, matcherEndpoint.Order);
+ AssertMatchingSuppressed(matcherEndpoint, false);
+ });
+ }
+
+ [Fact]
+ public void Endpoints_ConventionalRoutes_DefaultValuesEndingWithOptional_IncludeFullRouteAsHighPriority()
+ {
+ // Arrange
+ var actionDescriptorCollection = GetActionDescriptorCollection(
+ new { controller = "Home", action = "Index" });
+ var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection);
+ dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(
+ name: string.Empty,
+ template: "{controller=Home}/{action=Index}/{id?}"));
+
+ // Act
+ var endpoints = dataSource.Endpoints;
+
+ // Assert
+ Assert.Collection(
+ endpoints,
+ (ep) =>
+ {
+ var matcherEndpoint = Assert.IsType(ep);
+ Assert.Equal("{controller=Home}/{action=Index}/{id?}", matcherEndpoint.RoutePattern.RawText);
+ Assert.Equal(1, matcherEndpoint.Order);
+ AssertMatchingSuppressed(matcherEndpoint, true);
+ },
+ (ep) =>
+ {
+ var matcherEndpoint = Assert.IsType(ep);
+ Assert.Equal("", matcherEndpoint.RoutePattern.RawText);
+ Assert.Equal(2, matcherEndpoint.Order);
+ AssertMatchingSuppressed(matcherEndpoint, false);
+ },
+ (ep) =>
+ {
+ var matcherEndpoint = Assert.IsType(ep);
+ Assert.Equal("Home", matcherEndpoint.RoutePattern.RawText);
+ Assert.Equal(3, matcherEndpoint.Order);
+ AssertMatchingSuppressed(matcherEndpoint, false);
+ },
+ (ep) =>
+ {
+ var matcherEndpoint = Assert.IsType(ep);
+ Assert.Equal("Home/Index/{id?}", matcherEndpoint.RoutePattern.RawText);
+ Assert.Equal(4, matcherEndpoint.Order);
+ AssertMatchingSuppressed(matcherEndpoint, false);
+ });
+ }
+
+ [Fact]
+ public void Endpoints_ConventionalRoutes_DefaultValues_Shortened()
+ {
+ // Arrange
+ var actionDescriptorCollection = GetActionDescriptorCollection(
+ new { controller = "TestController", action = "TestAction" });
+ var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection);
+ dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(
+ name: string.Empty,
+ template: "{controller=TestController}/{action=TestAction}/{id=17}"));
+
+ // Act
+ var endpoints = dataSource.Endpoints;
+
+ // Assert
+ Assert.Collection(
+ endpoints,
+ (ep) =>
+ {
+ var matcherEndpoint = Assert.IsType(ep);
+ Assert.Equal("{controller=TestController}/{action=TestAction}/{id=17}", matcherEndpoint.RoutePattern.RawText);
+ Assert.Equal("17", matcherEndpoint.RoutePattern.Defaults["id"]);
+ Assert.Equal(1, matcherEndpoint.Order);
+ },
+ (ep) =>
+ {
+ var matcherEndpoint = Assert.IsType(ep);
+ Assert.Equal("", matcherEndpoint.RoutePattern.RawText);
+ Assert.Equal("17", matcherEndpoint.RoutePattern.Defaults["id"]);
+ Assert.Equal(2, matcherEndpoint.Order);
+ },
+ (ep) =>
+ {
+ var matcherEndpoint = Assert.IsType(ep);
+ Assert.Equal("TestController", matcherEndpoint.RoutePattern.RawText);
+ Assert.Equal("17", matcherEndpoint.RoutePattern.Defaults["id"]);
+ Assert.Equal(3, matcherEndpoint.Order);
+ },
+ (ep) =>
+ {
+ var matcherEndpoint = Assert.IsType(ep);
+ Assert.Equal("TestController/TestAction", matcherEndpoint.RoutePattern.RawText);
+ Assert.Equal("17", matcherEndpoint.RoutePattern.Defaults["id"]);
+ Assert.Equal(4, matcherEndpoint.Order);
+ },
+ (ep) =>
+ {
+ var matcherEndpoint = Assert.IsType(ep);
+ Assert.Equal("TestController/TestAction/{id=17}", matcherEndpoint.RoutePattern.RawText);
+ Assert.Equal("17", matcherEndpoint.RoutePattern.Defaults["id"]);
+ Assert.Equal(5, matcherEndpoint.Order);
+ });
+ }
+
+ [Fact]
+ public void Endpoints_ConventionalRoutes_DefaultValuesAndCatchAll_EndpointInfoDefaultsNotModified()
+ {
+ // Arrange
+ var actionDescriptorCollection = GetActionDescriptorCollection(
+ new { controller = "TestController", action = "TestAction" });
+ var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection);
+
+ var endpointInfo = CreateEndpointInfo(
+ name: string.Empty,
+ defaults: new RouteValueDictionary(),
+ template: "{controller=TestController}/{action=TestAction}/{id=17}/{**catchAll}");
+ dataSource.ConventionalEndpointInfos.Add(endpointInfo);
+
+ // Act
+ var endpoints = dataSource.Endpoints;
+
+ // Assert
+ Assert.Empty(endpointInfo.Defaults);
+ }
+
+ [Fact]
+ public void Endpoints_ConventionalRoutes_DefaultValuesAndCatchAll_Shortened()
+ {
+ // Arrange
+ var actionDescriptorCollection = GetActionDescriptorCollection(
+ new { controller = "TestController", action = "TestAction" });
+ var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection);
+ dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(
+ name: string.Empty,
+ template: "{controller=TestController}/{action=TestAction}/{id=17}/{**catchAll}"));
+
+ // Act
+ var endpoints = dataSource.Endpoints;
+
+ // Assert
+ Assert.Collection(
+ endpoints,
+ (ep) =>
+ {
+ var matcherEndpoint = Assert.IsType(ep);
+ Assert.Equal("{controller=TestController}/{action=TestAction}/{id=17}/{**catchAll}", matcherEndpoint.RoutePattern.RawText);
+ Assert.Equal("17", matcherEndpoint.RoutePattern.Defaults["id"]);
+ Assert.Equal(1, matcherEndpoint.Order);
+ AssertMatchingSuppressed(matcherEndpoint, true);
+ },
+ (ep) =>
+ {
+ var matcherEndpoint = Assert.IsType(ep);
+ Assert.Equal("", matcherEndpoint.RoutePattern.RawText);
+ Assert.Equal("17", matcherEndpoint.RoutePattern.Defaults["id"]);
+ Assert.Equal(2, matcherEndpoint.Order);
+ AssertMatchingSuppressed(matcherEndpoint, false);
+ },
+ (ep) =>
+ {
+ var matcherEndpoint = Assert.IsType(ep);
+ Assert.Equal("TestController", matcherEndpoint.RoutePattern.RawText);
+ Assert.Equal("17", matcherEndpoint.RoutePattern.Defaults["id"]);
+ Assert.Equal(3, matcherEndpoint.Order);
+ AssertMatchingSuppressed(matcherEndpoint, false);
+ },
+ (ep) =>
+ {
+ var matcherEndpoint = Assert.IsType(ep);
+ Assert.Equal("TestController/TestAction", matcherEndpoint.RoutePattern.RawText);
+ Assert.Equal("17", matcherEndpoint.RoutePattern.Defaults["id"]);
+ Assert.Equal(4, matcherEndpoint.Order);
+ AssertMatchingSuppressed(matcherEndpoint, false);
+ },
+ (ep) =>
+ {
+ var matcherEndpoint = Assert.IsType(ep);
+ Assert.Equal("TestController/TestAction/{id=17}/{**catchAll}", matcherEndpoint.RoutePattern.RawText);
+ Assert.Equal("17", matcherEndpoint.RoutePattern.Defaults["id"]);
+ Assert.Equal(5, matcherEndpoint.Order);
+ AssertMatchingSuppressed(matcherEndpoint, false);
+ });
+ }
+
+ [Fact]
+ public void Endpoints_ConventionalRoutes_DefaultValuesAndOptional_Shortened()
+ {
+ // Arrange
+ var actionDescriptorCollection = GetActionDescriptorCollection(
+ new { controller = "TestController", action = "TestAction" });
+ var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection);
+ dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(
+ name: string.Empty,
+ template: "{controller=TestController}/{action=TestAction}/{id=17}/{more?}"));
+
+ // Act
+ var endpoints = dataSource.Endpoints;
+
+ // Assert
+ Assert.Collection(
+ endpoints,
+ (ep) =>
+ {
+ var matcherEndpoint = Assert.IsType(ep);
+ Assert.Equal("{controller=TestController}/{action=TestAction}/{id=17}/{more?}", matcherEndpoint.RoutePattern.RawText);
+ Assert.Equal("17", matcherEndpoint.RoutePattern.Defaults["id"]);
+ Assert.Equal(1, matcherEndpoint.Order);
+ AssertMatchingSuppressed(matcherEndpoint, true);
+ },
+ (ep) =>
+ {
+ var matcherEndpoint = Assert.IsType(ep);
+ Assert.Equal("", matcherEndpoint.RoutePattern.RawText);
+ Assert.Equal("17", matcherEndpoint.RoutePattern.Defaults["id"]);
+ Assert.Equal(2, matcherEndpoint.Order);
+ AssertMatchingSuppressed(matcherEndpoint, false);
+ },
+ (ep) =>
+ {
+ var matcherEndpoint = Assert.IsType(ep);
+ Assert.Equal("TestController", matcherEndpoint.RoutePattern.RawText);
+ Assert.Equal("17", matcherEndpoint.RoutePattern.Defaults["id"]);
+ Assert.Equal(3, matcherEndpoint.Order);
+ AssertMatchingSuppressed(matcherEndpoint, false);
+ },
+ (ep) =>
+ {
+ var matcherEndpoint = Assert.IsType(ep);
+ Assert.Equal("TestController/TestAction", matcherEndpoint.RoutePattern.RawText);
+ Assert.Equal("17", matcherEndpoint.RoutePattern.Defaults["id"]);
+ Assert.Equal(4, matcherEndpoint.Order);
+ AssertMatchingSuppressed(matcherEndpoint, false);
+ },
+ (ep) =>
+ {
+ var matcherEndpoint = Assert.IsType(ep);
+ Assert.Equal("TestController/TestAction/{id=17}/{more?}", matcherEndpoint.RoutePattern.RawText);
+ Assert.Equal("17", matcherEndpoint.RoutePattern.Defaults["id"]);
+ Assert.Equal(5, matcherEndpoint.Order);
+ AssertMatchingSuppressed(matcherEndpoint, false);
+ });
+ }
+
+ [Fact]
+ public void Endpoints_ConventionalRoutes_OptionalExtension_IncludeFullRouteAsHighPriority()
+ {
+ // Arrange
+ var actionDescriptorCollection = GetActionDescriptorCollection(
+ new { controller = "TestController", action = "TestAction" });
+ var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection);
+ dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(
+ name: string.Empty,
+ template: "{controller}/{action=TestAction}.{ext?}"));
+
+ // Act
+ var endpoints = dataSource.Endpoints;
+
+ // Assert
+ Assert.Collection(
+ endpoints,
+ (ep) =>
+ {
+ var matcherEndpoint = Assert.IsType(ep);
+ Assert.Equal("TestController/{action=TestAction}.{ext?}", matcherEndpoint.RoutePattern.RawText);
+ Assert.Equal(1, matcherEndpoint.Order);
+ AssertMatchingSuppressed(matcherEndpoint, true);
+ },
+ (ep) =>
+ {
+ var matcherEndpoint = Assert.IsType(ep);
+ Assert.Equal("TestController", matcherEndpoint.RoutePattern.RawText);
+ Assert.Equal(2, matcherEndpoint.Order);
+ AssertMatchingSuppressed(matcherEndpoint, false);
+ },
+ (ep) =>
+ {
+ var matcherEndpoint = Assert.IsType(ep);
+ Assert.Equal("TestController/TestAction.{ext?}", matcherEndpoint.RoutePattern.RawText);
+ Assert.Equal(3, matcherEndpoint.Order);
+ AssertMatchingSuppressed(matcherEndpoint, false);
+ });
+ }
+
+ [Fact]
+ public void Endpoints_ConventionalRoutes_MultipleOptionalAndCatchAll_IncludeFullRouteAsHighPriority()
+ {
+ // Arrange
+ var actionDescriptorCollection = GetActionDescriptorCollection(
+ new { controller = "TestController", action = "TestAction" });
+ var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection);
+ dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(
+ name: string.Empty,
+ template: "{controller=TestController}/{action=TestAction}/{id?}/{more?}/{**catchAll}"));
+
+ // Act
+ var endpoints = dataSource.Endpoints;
+
+ // Assert
+ Assert.Collection(
+ endpoints,
+ (ep) =>
+ {
+ var matcherEndpoint = Assert.IsType(ep);
+ Assert.Equal("{controller=TestController}/{action=TestAction}/{id?}/{more?}/{**catchAll}", matcherEndpoint.RoutePattern.RawText);
+ Assert.Equal(1, matcherEndpoint.Order);
+ AssertMatchingSuppressed(matcherEndpoint, true);
+ },
+ (ep) =>
+ {
+ var matcherEndpoint = Assert.IsType(ep);
+ Assert.Equal("", matcherEndpoint.RoutePattern.RawText);
+ Assert.Equal(2, matcherEndpoint.Order);
+ AssertMatchingSuppressed(matcherEndpoint, false);
+ },
+ (ep) =>
+ {
+ var matcherEndpoint = Assert.IsType(ep);
+ Assert.Equal("TestController", matcherEndpoint.RoutePattern.RawText);
+ Assert.Equal(3, matcherEndpoint.Order);
+ AssertMatchingSuppressed(matcherEndpoint, false);
+ },
+ (ep) =>
+ {
+ var matcherEndpoint = Assert.IsType(ep);
+ Assert.Equal("TestController/TestAction/{id?}/{more?}/{**catchAll}", matcherEndpoint.RoutePattern.RawText);
+ Assert.Equal(4, matcherEndpoint.Order);
+ AssertMatchingSuppressed(matcherEndpoint, false);
+ });
+ }
+
+ [Fact]
+ public void Endpoints_AttributeRoutes_CatchAllWithDefault_IncludeFullRouteAsHighPriority()
+ {
+ // Arrange
+ var actionDescriptorCollection = GetActionDescriptorCollection(
+ "/TeamName/{*Name=DefaultName}/",
+ new { controller = "TestController", action = "TestAction" });
+ var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection);
+
+ // Act
+ var endpoints = dataSource.Endpoints;
+
+ // Assert
+ Assert.Collection(
+ endpoints,
+ (ep) =>
+ {
+ var matcherEndpoint = Assert.IsType(ep);
+ Assert.Equal("TeamName/{*Name=DefaultName}", matcherEndpoint.RoutePattern.RawText);
+ Assert.Equal(0, matcherEndpoint.Order);
+ AssertMatchingSuppressed(matcherEndpoint, true);
+ },
+ (ep) =>
+ {
+ var matcherEndpoint = Assert.IsType(ep);
+ Assert.Equal("TeamName", matcherEndpoint.RoutePattern.RawText);
+ Assert.Equal("DefaultName", matcherEndpoint.RoutePattern.Defaults["Name"]);
+ Assert.Equal(1, matcherEndpoint.Order);
+ AssertMatchingSuppressed(matcherEndpoint, false);
+ },
+ (ep) =>
+ {
+ var matcherEndpoint = Assert.IsType(ep);
+ Assert.Equal("TeamName/{*Name=DefaultName}", matcherEndpoint.RoutePattern.RawText);
+ Assert.Equal("DefaultName", matcherEndpoint.RoutePattern.Defaults["Name"]);
+ Assert.Equal(2, matcherEndpoint.Order);
+ AssertMatchingSuppressed(matcherEndpoint, false);
+ });
+ }
+
+ [Fact]
+ public void Endpoints_AttributeRoutes_DefaultDifferentCaseFromRouteValue_UseDefaultCase()
+ {
+ // Arrange
+ var actionDescriptorCollection = GetActionDescriptorCollection(
+ "{controller}/{action=TESTACTION}/{id?}",
+ new { controller = "TestController", action = "TestAction" });
+ var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection);
+
+ // Act
+ var endpoints = dataSource.Endpoints;
+
+ // Assert
+ Assert.Collection(
+ endpoints,
+ (ep) =>
+ {
+ var matcherEndpoint = Assert.IsType(ep);
+ Assert.Equal("TestController/{action=TESTACTION}/{id?}", matcherEndpoint.RoutePattern.RawText);
+ Assert.Equal("TESTACTION", matcherEndpoint.RoutePattern.Defaults["action"]);
+ Assert.Equal(0, matcherEndpoint.Order);
+ AssertMatchingSuppressed(matcherEndpoint, true);
+
+ var routeValuesAddress = matcherEndpoint.Metadata.GetMetadata();
+ Assert.Equal("TESTACTION", routeValuesAddress.RequiredValues["action"]);
+
+ },
+ (ep) =>
+ {
+ var matcherEndpoint = Assert.IsType(ep);
+ Assert.Equal("TestController", matcherEndpoint.RoutePattern.RawText);
+ Assert.Equal("TESTACTION", matcherEndpoint.RoutePattern.Defaults["action"]);
+ Assert.Equal(1, matcherEndpoint.Order);
+ AssertMatchingSuppressed(matcherEndpoint, false);
+
+ var routeValuesAddress = matcherEndpoint.Metadata.GetMetadata();
+ Assert.Equal("TESTACTION", routeValuesAddress.RequiredValues["action"]);
+ },
+ (ep) =>
+ {
+ var matcherEndpoint = Assert.IsType(ep);
+ Assert.Equal("TestController/TESTACTION/{id?}", matcherEndpoint.RoutePattern.RawText);
+ Assert.Equal("TESTACTION", matcherEndpoint.RoutePattern.Defaults["action"]);
+ Assert.Equal(2, matcherEndpoint.Order);
+ AssertMatchingSuppressed(matcherEndpoint, false);
+
+ var routeValuesAddress = matcherEndpoint.Metadata.GetMetadata();
+ Assert.Equal("TESTACTION", routeValuesAddress.RequiredValues["action"]);
+ });
+ }
+
private MvcEndpointDataSource CreateMvcEndpointDataSource(
IActionDescriptorCollectionProvider actionDescriptorCollectionProvider = null,
MvcEndpointInvokerFactory mvcEndpointInvokerFactory = null)
@@ -898,5 +1423,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
Assert.Equal(subsetPair.Value, fullSetPairValue);
}
}
+
+ private void AssertMatchingSuppressed(Endpoint endpoint, bool suppressed)
+ {
+ var isEndpointSuppressed = endpoint.Metadata.GetMetadata()?.SuppressMatching ?? false;
+ Assert.Equal(suppressed, isEndpointSuppressed);
+ }
}
}
\ No newline at end of file
diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RoutingTestsBase.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RoutingTestsBase.cs
index 6ee37de064..5cd65b9bcb 100644
--- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RoutingTestsBase.cs
+++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RoutingTestsBase.cs
@@ -1041,6 +1041,136 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Equal("Departments", result.RouteName);
}
+ [Fact]
+ public async Task ConventionalRoutedAction_DefaultValues_OptionalParameter_LinkToDefaultValuePath()
+ {
+ // Arrange
+ var url = LinkFrom("http://localhost/DefaultValuesRoute/Optional")
+ .To(new { });
+
+ // Act
+ var response = await Client.GetAsync(url);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var body = await response.Content.ReadAsStringAsync();
+ var result = JsonConvert.DeserializeObject(body);
+
+ Assert.Equal("DefaultValues", result.Controller);
+ Assert.Equal("OptionalParameter", result.Action);
+
+ Assert.Equal("/DefaultValuesRoute/Optional", result.Link);
+ }
+
+ [Fact]
+ public async Task ConventionalRoutedAction_DefaultValues_OptionalParameter_LinkToFullPath()
+ {
+ // Arrange
+ var url = LinkFrom("http://localhost/DefaultValuesRoute/Optional")
+ .To(new { id = "123" });
+
+ // Act
+ var response = await Client.GetAsync(url);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var body = await response.Content.ReadAsStringAsync();
+ var result = JsonConvert.DeserializeObject(body);
+
+ Assert.Equal("DefaultValues", result.Controller);
+ Assert.Equal("OptionalParameter", result.Action);
+
+ Assert.Equal("/DefaultValuesRoute/Optional/DEFAULTVALUES/OPTIONALPARAMETER/123", result.Link);
+ }
+
+ [Fact]
+ public async Task ConventionalRoutedAction_DefaultValues_DefaultParameter_LinkToDefaultValuePath()
+ {
+ // Arrange
+ var url = LinkFrom("http://localhost/DefaultValuesRoute/Default")
+ .To(new { });
+
+ // Act
+ var response = await Client.GetAsync(url);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var body = await response.Content.ReadAsStringAsync();
+ var result = JsonConvert.DeserializeObject(body);
+
+ Assert.Equal("DefaultValues", result.Controller);
+ Assert.Equal("DefaultParameter", result.Action);
+ Assert.Equal("17", result.RouteValues["id"]);
+
+ Assert.Equal("/DefaultValuesRoute/Default", result.Link);
+ }
+
+ [Fact]
+ public async Task ConventionalRoutedAction_DefaultValues_DefaultParameterWithCatchAll_LinkToDefaultValuePath()
+ {
+ // Arrange
+ var url = LinkFrom("http://localhost/DefaultValuesRoute/Default")
+ .To(new { catchAll = "CatchAll" });
+
+ // Act
+ var response = await Client.GetAsync(url);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var body = await response.Content.ReadAsStringAsync();
+ var result = JsonConvert.DeserializeObject(body);
+
+ Assert.Equal("DefaultValues", result.Controller);
+ Assert.Equal("DefaultParameter", result.Action);
+ Assert.Equal("17", result.RouteValues["id"]);
+
+ Assert.Equal("/DefaultValuesRoute/Default/DEFAULTVALUES/DEFAULTPARAMETER/17/CatchAll", result.Link);
+ }
+
+ [Fact]
+ public async Task ConventionalRoutedAction_DefaultValues_DefaultParameter_LinkToFullPath()
+ {
+ // Arrange
+ var url = LinkFrom("http://localhost/DefaultValuesRoute/Default")
+ .To(new { id = "123" });
+
+ // Act
+ var response = await Client.GetAsync(url);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var body = await response.Content.ReadAsStringAsync();
+ var result = JsonConvert.DeserializeObject(body);
+
+ Assert.Equal("DefaultValues", result.Controller);
+ Assert.Equal("DefaultParameter", result.Action);
+ Assert.Equal("17", result.RouteValues["id"]);
+
+ Assert.Equal("/DefaultValuesRoute/Default/DEFAULTVALUES/DEFAULTPARAMETER/123", result.Link);
+ }
+
+ [Fact]
+ public async Task ConventionalRoutedAction_DefaultValues_DefaultParameterMatches_LinkToShortenedPath()
+ {
+ // Arrange
+ var url = LinkFrom("http://localhost/DefaultValuesRoute/Default/DefaultValues/DefaultParameter/123")
+ .To(new { id = "17" });
+
+ // Act
+ var response = await Client.GetAsync(url);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var body = await response.Content.ReadAsStringAsync();
+ var result = JsonConvert.DeserializeObject(body);
+
+ Assert.Equal("DefaultValues", result.Controller);
+ Assert.Equal("DefaultParameter", result.Action);
+ Assert.Equal("123", result.RouteValues["id"]);
+
+ Assert.Equal("/DefaultValuesRoute/Default", result.Link);
+ }
+
[Fact]
public virtual async Task ConventionalRoutedAction_LinkToArea()
{
diff --git a/test/WebSites/RoutingWebSite/Controllers/DefaultValuesController.cs b/test/WebSites/RoutingWebSite/Controllers/DefaultValuesController.cs
new file mode 100644
index 0000000000..f28c5c14c5
--- /dev/null
+++ b/test/WebSites/RoutingWebSite/Controllers/DefaultValuesController.cs
@@ -0,0 +1,34 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Linq;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Controllers;
+using Microsoft.AspNetCore.Mvc.Filters;
+
+namespace RoutingWebSite
+{
+ public class DefaultValuesController : Controller
+ {
+ private readonly TestResponseGenerator _generator;
+
+ public DefaultValuesController(TestResponseGenerator generator)
+ {
+ _generator = generator;
+ }
+
+ public IActionResult DefaultParameter(string id)
+ {
+ return _generator.Generate(id == null
+ ? "/DefaultValuesRoute/DefaultValues"
+ : "/DefaultValuesRoute/DefaultValues/DefaultParameter/Index/" + id);
+ }
+
+ public IActionResult OptionalParameter(string id)
+ {
+ return _generator.Generate(id == "17"
+ ? "/DefaultValuesRoute/DefaultValues"
+ : "/DefaultValuesRoute/DefaultValues/OptionalParameter/Index/" + id);
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/WebSites/RoutingWebSite/Startup.cs b/test/WebSites/RoutingWebSite/Startup.cs
index 03b1fa2ba2..44bc08a25f 100644
--- a/test/WebSites/RoutingWebSite/Startup.cs
+++ b/test/WebSites/RoutingWebSite/Startup.cs
@@ -62,6 +62,18 @@ namespace RoutingWebSite
defaults: null,
constraints: new { controller = "ConventionalTransformer" });
+ routes.MapRoute(
+ "DefaultValuesRoute_OptionalParameter",
+ "DefaultValuesRoute/Optional/{controller=DEFAULTVALUES}/{action=OPTIONALPARAMETER}/{id?}/{**catchAll}",
+ defaults: null,
+ constraints: new { controller = "DefaultValues", action = "OptionalParameter" });
+
+ routes.MapRoute(
+ "DefaultValuesRoute_DefaultParameter",
+ "DefaultValuesRoute/Default/{controller=DEFAULTVALUES}/{action=DEFAULTPARAMETER}/{id=17}/{**catchAll}",
+ defaults: null,
+ constraints: new { controller = "DefaultValues", action = "DefaultParameter" });
+
routes.MapAreaRoute(
"flightRoute",
"adminRoute",
diff --git a/test/WebSites/RoutingWebSite/StartupWith21Compat.cs b/test/WebSites/RoutingWebSite/StartupWith21Compat.cs
index 473653df24..9b78b66d32 100644
--- a/test/WebSites/RoutingWebSite/StartupWith21Compat.cs
+++ b/test/WebSites/RoutingWebSite/StartupWith21Compat.cs
@@ -1,14 +1,11 @@
// 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.Linq;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
-using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
@@ -48,7 +45,7 @@ namespace RoutingWebSite
new ControllerToRemove
{
ControllerType = typeof(PageRouteController),
- Actions = new [] { nameof(PageRouteController.AttributeRoute) }
+ Actions = new[] { nameof(PageRouteController.AttributeRoute) }
});
services.TryAddEnumerable(ServiceDescriptor.Singleton(actionDescriptorProvider));
}
@@ -64,6 +61,18 @@ namespace RoutingWebSite
constraints: new { controller = "DataTokens" },
dataTokens: new { hasDataTokens = true });
+ routes.MapRoute(
+ "DefaultValuesRoute_OptionalParameter",
+ "DefaultValuesRoute/Optional/{controller=DEFAULTVALUES}/{action=OPTIONALPARAMETER}/{id?}/{**catchAll}",
+ defaults: null,
+ constraints: new { controller = "DefaultValues", action = "OptionalParameter" });
+
+ routes.MapRoute(
+ "DefaultValuesRoute_DefaultParameter",
+ "DefaultValuesRoute/Default/{controller=DEFAULTVALUES}/{action=DEFAULTPARAMETER}/{id=17}/{**catchAll}",
+ defaults: null,
+ constraints: new { controller = "DefaultValues", action = "DefaultParameter" });
+
routes.MapAreaRoute(
"flightRoute",
"adminRoute",