diff --git a/src/Http/Routing/src/Patterns/RoutePatternFactory.cs b/src/Http/Routing/src/Patterns/RoutePatternFactory.cs
index 7b577e25bd..91c8ae8b38 100644
--- a/src/Http/Routing/src/Patterns/RoutePatternFactory.cs
+++ b/src/Http/Routing/src/Patterns/RoutePatternFactory.cs
@@ -1,7 +1,8 @@
-// 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.
using System;
+using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
@@ -51,6 +52,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
/// Additional parameter policies to associated with the route pattern. May be null.
/// The provided object will be converted to key-value pairs using
/// and then merged into the parsed route pattern.
+ /// Multiple policies can be specified for a key by providing a collection as the value.
///
/// The .
public static RoutePattern Parse(string pattern, object defaults, object parameterPolicies)
@@ -78,6 +80,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
/// Additional parameter policies to associated with the route pattern. May be null.
/// The provided object will be converted to key-value pairs using
/// and then merged into the parsed route pattern.
+ /// Multiple policies can be specified for a key by providing a collection as the value.
///
///
/// Route values that can be substituted for parameters in the route pattern. See remarks on .
@@ -138,6 +141,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
/// Additional parameter policies to associated with the route pattern. May be null.
/// The provided object will be converted to key-value pairs using
/// and then merged into the route pattern.
+ /// Multiple policies can be specified for a key by providing a collection as the value.
///
/// The collection of segments.
/// The .
@@ -168,6 +172,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
/// Additional parameter policies to associated with the route pattern. May be null.
/// The provided object will be converted to key-value pairs using
/// and then merged into the route pattern.
+ /// Multiple policies can be specified for a key by providing a collection as the value.
///
/// The collection of segments.
/// The .
@@ -229,6 +234,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
/// Additional parameter policies to associated with the route pattern. May be null.
/// The provided object will be converted to key-value pairs using
/// and then merged into the route pattern.
+ /// Multiple policies can be specified for a key by providing a collection as the value.
///
/// The collection of segments.
/// The .
@@ -259,6 +265,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
/// Additional parameter policies to associated with the route pattern. May be null.
/// The provided object will be converted to key-value pairs using
/// and then merged into the route pattern.
+ /// Multiple policies can be specified for a key by providing a collection as the value.
///
/// The collection of segments.
/// The .
@@ -312,12 +319,33 @@ namespace Microsoft.AspNetCore.Routing.Patterns
foreach (var kvp in parameterPolicies)
{
- updatedParameterPolicies.Add(kvp.Key, new List()
+ var policyReferences = new List();
+
+ if (kvp.Value is IParameterPolicy parameterPolicy)
{
- kvp.Value is IParameterPolicy parameterPolicy
- ? ParameterPolicy(parameterPolicy)
- : Constraint(kvp.Value), // Constraint will convert string values into regex constraints
- });
+ policyReferences.Add(ParameterPolicy(parameterPolicy));
+ }
+ else if (kvp.Value is string)
+ {
+ // Constraint will convert string values into regex constraints
+ policyReferences.Add(Constraint(kvp.Value));
+ }
+ else if (kvp.Value is IEnumerable multiplePolicies)
+ {
+ foreach (var item in multiplePolicies)
+ {
+ // Constraint will convert string values into regex constraints
+ policyReferences.Add(item is IParameterPolicy p ? ParameterPolicy(p) : Constraint(item));
+ }
+ }
+ else
+ {
+ throw new InvalidOperationException(Resources.FormatRoutePattern_InvalidConstraintReference(
+ kvp.Value ?? "null",
+ typeof(IRouteConstraint)));
+ }
+
+ updatedParameterPolicies.Add(kvp.Key, policyReferences);
}
}
diff --git a/src/Http/Routing/test/UnitTests/Patterns/RoutePatternFactoryTest.cs b/src/Http/Routing/test/UnitTests/Patterns/RoutePatternFactoryTest.cs
index ef4ac2fc82..faace7a6b0 100644
--- a/src/Http/Routing/test/UnitTests/Patterns/RoutePatternFactoryTest.cs
+++ b/src/Http/Routing/test/UnitTests/Patterns/RoutePatternFactoryTest.cs
@@ -1,4 +1,4 @@
-// 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.
using System;
@@ -218,6 +218,104 @@ namespace Microsoft.AspNetCore.Routing.Patterns
});
}
+ [Fact]
+ public void Pattern_ExtraConstraints_MultipleConstraintsForKey()
+ {
+ // Arrange
+ var template = "{a}/{b}/{c}";
+ var defaults = new { };
+ var constraints = new { d = new object[] { new RegexRouteConstraint("foo"), new RegexRouteConstraint("bar"), "baz" } };
+
+ var original = RoutePatternFactory.Parse(template);
+
+ // Act
+ var actual = RoutePatternFactory.Pattern(
+ original.RawText,
+ defaults,
+ constraints,
+ original.PathSegments);
+
+ // Assert
+ Assert.Collection(
+ actual.ParameterPolicies.OrderBy(kvp => kvp.Key),
+ kvp =>
+ {
+ Assert.Equal("d", kvp.Key);
+ Assert.Collection(
+ kvp.Value,
+ c => Assert.Equal("foo", Assert.IsType(c.ParameterPolicy).Constraint.ToString()),
+ c => Assert.Equal("bar", Assert.IsType(c.ParameterPolicy).Constraint.ToString()),
+ c => Assert.Equal("^(baz)$", Assert.IsType(c.ParameterPolicy).Constraint.ToString()));
+ });
+ }
+
+ [Fact]
+ public void Pattern_ExtraConstraints_MergeMultipleConstraintsForKey()
+ {
+ // Arrange
+ var template = "{a:int}/{b}/{c:int}";
+ var defaults = new { };
+ var constraints = new { b = "fizz", c = new object[] { new RegexRouteConstraint("foo"), new RegexRouteConstraint("bar"), "baz" } };
+
+ var original = RoutePatternFactory.Parse(template);
+
+ // Act
+ var actual = RoutePatternFactory.Pattern(
+ original.RawText,
+ defaults,
+ constraints,
+ original.PathSegments);
+
+ // Assert
+ Assert.Collection(
+ actual.ParameterPolicies.OrderBy(kvp => kvp.Key),
+ kvp =>
+ {
+ Assert.Equal("a", kvp.Key);
+ Assert.Collection(
+ kvp.Value,
+ c => Assert.Equal("int", c.Content));
+ },
+ kvp =>
+ {
+ Assert.Equal("b", kvp.Key);
+ Assert.Collection(
+ kvp.Value,
+ c => Assert.Equal("^(fizz)$", Assert.IsType(c.ParameterPolicy).Constraint.ToString()));
+ },
+ kvp =>
+ {
+ Assert.Equal("c", kvp.Key);
+ Assert.Collection(
+ kvp.Value,
+ c => Assert.Equal("foo", Assert.IsType(c.ParameterPolicy).Constraint.ToString()),
+ c => Assert.Equal("bar", Assert.IsType(c.ParameterPolicy).Constraint.ToString()),
+ c => Assert.Equal("^(baz)$", Assert.IsType(c.ParameterPolicy).Constraint.ToString()),
+ c => Assert.Equal("int", c.Content));
+ });
+ }
+
+ [Fact]
+ public void Pattern_ExtraConstraints_NestedArray_Throws()
+ {
+ // Arrange
+ var template = "{a}/{b}/{c:int}";
+ var defaults = new { };
+ var constraints = new { c = new object[] { new object[0] } };
+
+ var original = RoutePatternFactory.Parse(template);
+
+ // Act & Assert
+ Assert.Throws(() =>
+ {
+ RoutePatternFactory.Pattern(
+ original.RawText,
+ defaults,
+ constraints,
+ original.PathSegments);
+ });
+ }
+
[Fact]
public void Pattern_ExtraConstraints_RouteConstraint()
{