diff --git a/src/Http/Routing/perf/Microbenchmarks/Matching/CreateMatcherRegexConstraintBenchmark.cs b/src/Http/Routing/perf/Microbenchmarks/Matching/CreateMatcherRegexConstraintBenchmark.cs
new file mode 100644
index 000000000000..79da258ebaa3
--- /dev/null
+++ b/src/Http/Routing/perf/Microbenchmarks/Matching/CreateMatcherRegexConstraintBenchmark.cs
@@ -0,0 +1,39 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using BenchmarkDotNet.Attributes;
+
+namespace Microsoft.AspNetCore.Routing.Matching;
+
+public class CreateMatcherRegexConstraintBenchmark : EndpointRoutingBenchmarkBase
+{
+ [Params(true, false)]
+ public bool RegexSame { get; set; }
+
+ private const int EndpointCount = 1_000;
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ Endpoints = new RouteEndpoint[EndpointCount];
+ for (var i = 0; i < Endpoints.Length; i++)
+ {
+ Endpoints[i] = RegexSame
+ ? CreateEndpoint("/plaintext" + i + "/{param:regex(^\\d{{7}}|(SI[[PG]]|JPA|DEM)\\d{{4}})}")
+ : CreateEndpoint("/plaintext" + i + "/{param:regex(^" + i + "\\d{{7}}|(SI[[PG]]|JPA|DEM)\\d{{4}})}");
+ }
+
+ }
+
+ [Benchmark]
+ public void Build()
+ {
+ var builder = CreateDfaMatcherBuilder();
+ for (var i = 0; i < Endpoints.Length; i++)
+ {
+ builder.AddEndpoint(Endpoints[i]);
+ }
+
+ builder.Build();
+ }
+}
diff --git a/src/Http/Routing/src/Constraints/AlphaRouteConstraint.cs b/src/Http/Routing/src/Constraints/AlphaRouteConstraint.cs
index 687481275388..01607cb624a1 100644
--- a/src/Http/Routing/src/Constraints/AlphaRouteConstraint.cs
+++ b/src/Http/Routing/src/Constraints/AlphaRouteConstraint.cs
@@ -2,13 +2,14 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Text.RegularExpressions;
+using Microsoft.AspNetCore.Routing.Matching;
namespace Microsoft.AspNetCore.Routing.Constraints;
///
/// Constrains a route parameter to contain only lowercase or uppercase letters A through Z in the English alphabet.
///
-public partial class AlphaRouteConstraint : RegexRouteConstraint
+public partial class AlphaRouteConstraint : RegexRouteConstraint, ICachableParameterPolicy
{
///
/// Initializes a new instance of the class.
diff --git a/src/Http/Routing/src/Constraints/BoolRouteConstraint.cs b/src/Http/Routing/src/Constraints/BoolRouteConstraint.cs
index e05b02a32392..95fdaf1b1b7e 100644
--- a/src/Http/Routing/src/Constraints/BoolRouteConstraint.cs
+++ b/src/Http/Routing/src/Constraints/BoolRouteConstraint.cs
@@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Routing.Constraints;
///
/// Constrains a route parameter to represent only Boolean values.
///
-public class BoolRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy
+public class BoolRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy, ICachableParameterPolicy
{
///
public bool Match(
diff --git a/src/Http/Routing/src/Constraints/DateTimeRouteConstraint.cs b/src/Http/Routing/src/Constraints/DateTimeRouteConstraint.cs
index a2cbf016cfb8..0064a6e2922d 100644
--- a/src/Http/Routing/src/Constraints/DateTimeRouteConstraint.cs
+++ b/src/Http/Routing/src/Constraints/DateTimeRouteConstraint.cs
@@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Routing.Constraints;
/// For a sample on how to list all formats which are considered, please visit
/// http://msdn.microsoft.com/en-us/library/aszyst2c(v=vs.110).aspx
///
-public class DateTimeRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy
+public class DateTimeRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy, ICachableParameterPolicy
{
///
public bool Match(
diff --git a/src/Http/Routing/src/Constraints/DecimalRouteConstraint.cs b/src/Http/Routing/src/Constraints/DecimalRouteConstraint.cs
index 0923fa65dac4..018989ebd30a 100644
--- a/src/Http/Routing/src/Constraints/DecimalRouteConstraint.cs
+++ b/src/Http/Routing/src/Constraints/DecimalRouteConstraint.cs
@@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Routing.Constraints;
///
/// Constrains a route parameter to represent only decimal values.
///
-public class DecimalRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy
+public class DecimalRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy, ICachableParameterPolicy
{
///
public bool Match(
diff --git a/src/Http/Routing/src/Constraints/DoubleRouteConstraint.cs b/src/Http/Routing/src/Constraints/DoubleRouteConstraint.cs
index 0032c051e97e..e10400ba6f73 100644
--- a/src/Http/Routing/src/Constraints/DoubleRouteConstraint.cs
+++ b/src/Http/Routing/src/Constraints/DoubleRouteConstraint.cs
@@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Routing.Constraints;
///
/// Constrains a route parameter to represent only 64-bit floating-point values.
///
-public class DoubleRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy
+public class DoubleRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy, ICachableParameterPolicy
{
///
public bool Match(
diff --git a/src/Http/Routing/src/Constraints/FileNameRouteConstraint.cs b/src/Http/Routing/src/Constraints/FileNameRouteConstraint.cs
index c3476a7bdc18..c09d67bee4d3 100644
--- a/src/Http/Routing/src/Constraints/FileNameRouteConstraint.cs
+++ b/src/Http/Routing/src/Constraints/FileNameRouteConstraint.cs
@@ -81,7 +81,7 @@ namespace Microsoft.AspNetCore.Routing.Constraints;
///
///
///
-public class FileNameRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy
+public class FileNameRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy, ICachableParameterPolicy
{
///
public bool Match(
diff --git a/src/Http/Routing/src/Constraints/FloatRouteConstraint.cs b/src/Http/Routing/src/Constraints/FloatRouteConstraint.cs
index bb50909b863f..3bf09dad148b 100644
--- a/src/Http/Routing/src/Constraints/FloatRouteConstraint.cs
+++ b/src/Http/Routing/src/Constraints/FloatRouteConstraint.cs
@@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Routing.Constraints;
///
/// Constrains a route parameter to represent only 32-bit floating-point values.
///
-public class FloatRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy
+public class FloatRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy, ICachableParameterPolicy
{
///
public bool Match(
diff --git a/src/Http/Routing/src/Constraints/GuidRouteConstraint.cs b/src/Http/Routing/src/Constraints/GuidRouteConstraint.cs
index 17463aedb40b..9dba13d55918 100644
--- a/src/Http/Routing/src/Constraints/GuidRouteConstraint.cs
+++ b/src/Http/Routing/src/Constraints/GuidRouteConstraint.cs
@@ -12,7 +12,7 @@ namespace Microsoft.AspNetCore.Routing.Constraints;
/// Matches values specified in any of the five formats "N", "D", "B", "P", or "X",
/// supported by Guid.ToString(string) and Guid.ToString(String, IFormatProvider) methods.
///
-public class GuidRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy
+public class GuidRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy, ICachableParameterPolicy
{
///
public bool Match(
diff --git a/src/Http/Routing/src/Constraints/IntRouteConstraint.cs b/src/Http/Routing/src/Constraints/IntRouteConstraint.cs
index f2ab6a06fb9b..5407158623c6 100644
--- a/src/Http/Routing/src/Constraints/IntRouteConstraint.cs
+++ b/src/Http/Routing/src/Constraints/IntRouteConstraint.cs
@@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Routing.Constraints;
///
/// Constrains a route parameter to represent only 32-bit integer values.
///
-public class IntRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy
+public class IntRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy, ICachableParameterPolicy
{
///
public bool Match(
diff --git a/src/Http/Routing/src/Constraints/LengthRouteConstraint.cs b/src/Http/Routing/src/Constraints/LengthRouteConstraint.cs
index da5d2490b817..f46f4d288e0e 100644
--- a/src/Http/Routing/src/Constraints/LengthRouteConstraint.cs
+++ b/src/Http/Routing/src/Constraints/LengthRouteConstraint.cs
@@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Routing.Constraints;
///
/// Constrains a route parameter to be a string of a given length or within a given range of lengths.
///
-public class LengthRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy
+public class LengthRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy, ICachableParameterPolicy
{
///
/// Initializes a new instance of the class that constrains
diff --git a/src/Http/Routing/src/Constraints/LongRouteConstraint.cs b/src/Http/Routing/src/Constraints/LongRouteConstraint.cs
index 9ffbd6d24574..57ea841742ce 100644
--- a/src/Http/Routing/src/Constraints/LongRouteConstraint.cs
+++ b/src/Http/Routing/src/Constraints/LongRouteConstraint.cs
@@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Routing.Constraints;
///
/// Constrains a route parameter to represent only 64-bit integer values.
///
-public class LongRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy
+public class LongRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy, ICachableParameterPolicy
{
///
public bool Match(
diff --git a/src/Http/Routing/src/Constraints/MaxLengthRouteConstraint.cs b/src/Http/Routing/src/Constraints/MaxLengthRouteConstraint.cs
index 643ea5082e4f..50cd0fa6a38a 100644
--- a/src/Http/Routing/src/Constraints/MaxLengthRouteConstraint.cs
+++ b/src/Http/Routing/src/Constraints/MaxLengthRouteConstraint.cs
@@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Routing.Constraints;
///
/// Constrains a route parameter to be a string with a maximum length.
///
-public class MaxLengthRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy
+public class MaxLengthRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy, ICachableParameterPolicy
{
///
/// Initializes a new instance of the class.
diff --git a/src/Http/Routing/src/Constraints/MaxRouteConstraint.cs b/src/Http/Routing/src/Constraints/MaxRouteConstraint.cs
index e4b3e54ee665..1f6c61aac9b5 100644
--- a/src/Http/Routing/src/Constraints/MaxRouteConstraint.cs
+++ b/src/Http/Routing/src/Constraints/MaxRouteConstraint.cs
@@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Routing.Constraints;
///
/// Constrains a route parameter to be an integer with a maximum value.
///
-public class MaxRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy
+public class MaxRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy, ICachableParameterPolicy
{
///
/// Initializes a new instance of the class.
diff --git a/src/Http/Routing/src/Constraints/MinLengthRouteConstraint.cs b/src/Http/Routing/src/Constraints/MinLengthRouteConstraint.cs
index 3d3393a04d7e..53a7c7a6eed6 100644
--- a/src/Http/Routing/src/Constraints/MinLengthRouteConstraint.cs
+++ b/src/Http/Routing/src/Constraints/MinLengthRouteConstraint.cs
@@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Routing.Constraints;
///
/// Constrains a route parameter to be a string with a minimum length.
///
-public class MinLengthRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy
+public class MinLengthRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy, ICachableParameterPolicy
{
///
/// Initializes a new instance of the class.
diff --git a/src/Http/Routing/src/Constraints/MinRouteConstraint.cs b/src/Http/Routing/src/Constraints/MinRouteConstraint.cs
index 43e2e1d2a1aa..139ac7ab1070 100644
--- a/src/Http/Routing/src/Constraints/MinRouteConstraint.cs
+++ b/src/Http/Routing/src/Constraints/MinRouteConstraint.cs
@@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Routing.Constraints;
///
/// Constrains a route parameter to be a long with a minimum value.
///
-public class MinRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy
+public class MinRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy, ICachableParameterPolicy
{
///
/// Initializes a new instance of the class.
diff --git a/src/Http/Routing/src/Constraints/NonFileNameRouteConstraint.cs b/src/Http/Routing/src/Constraints/NonFileNameRouteConstraint.cs
index 415c89fdeb81..22591ccafa47 100644
--- a/src/Http/Routing/src/Constraints/NonFileNameRouteConstraint.cs
+++ b/src/Http/Routing/src/Constraints/NonFileNameRouteConstraint.cs
@@ -77,7 +77,7 @@ namespace Microsoft.AspNetCore.Routing.Constraints;
///
///
///
-public class NonFileNameRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy
+public class NonFileNameRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy, ICachableParameterPolicy
{
///
public bool Match(
diff --git a/src/Http/Routing/src/Constraints/RangeRouteConstraint.cs b/src/Http/Routing/src/Constraints/RangeRouteConstraint.cs
index d66e9e05cd9a..d9d29e303996 100644
--- a/src/Http/Routing/src/Constraints/RangeRouteConstraint.cs
+++ b/src/Http/Routing/src/Constraints/RangeRouteConstraint.cs
@@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Routing.Constraints;
///
/// Constraints a route parameter to be an integer within a given range of values.
///
-public class RangeRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy
+public class RangeRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy, ICachableParameterPolicy
{
///
/// Initializes a new instance of the class.
diff --git a/src/Http/Routing/src/Constraints/RegexInlineRouteConstraint.cs b/src/Http/Routing/src/Constraints/RegexInlineRouteConstraint.cs
index 50963a0df25f..bf68b9208431 100644
--- a/src/Http/Routing/src/Constraints/RegexInlineRouteConstraint.cs
+++ b/src/Http/Routing/src/Constraints/RegexInlineRouteConstraint.cs
@@ -3,13 +3,14 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
+using Microsoft.AspNetCore.Routing.Matching;
namespace Microsoft.AspNetCore.Routing.Constraints;
///
/// Represents a regex constraint which can be used as an inlineConstraint.
///
-public class RegexInlineRouteConstraint : RegexRouteConstraint
+public class RegexInlineRouteConstraint : RegexRouteConstraint, ICachableParameterPolicy
{
///
/// Initializes a new instance of the class.
diff --git a/src/Http/Routing/src/Constraints/RegexRouteConstraint.cs b/src/Http/Routing/src/Constraints/RegexRouteConstraint.cs
index 0d0f5736380a..bc3ee556e974 100644
--- a/src/Http/Routing/src/Constraints/RegexRouteConstraint.cs
+++ b/src/Http/Routing/src/Constraints/RegexRouteConstraint.cs
@@ -15,6 +15,8 @@ namespace Microsoft.AspNetCore.Routing.Constraints;
public class RegexRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy
{
private static readonly TimeSpan RegexMatchTimeout = TimeSpan.FromSeconds(10);
+ private readonly string _regexPattern;
+ private Regex? _constraint;
///
/// Constructor for a given a .
@@ -24,7 +26,8 @@ public RegexRouteConstraint(Regex regex)
{
ArgumentNullException.ThrowIfNull(regex);
- Constraint = regex;
+ _constraint = regex;
+ _regexPattern = regex.ToString();
}
///
@@ -37,16 +40,26 @@ public RegexRouteConstraint(
{
ArgumentNullException.ThrowIfNull(regexPattern);
- Constraint = new Regex(
- regexPattern,
- RegexOptions.CultureInvariant | RegexOptions.Compiled | RegexOptions.IgnoreCase,
- RegexMatchTimeout);
+ _regexPattern = regexPattern;
}
///
/// Gets the regular expression used in the route constraint.
///
- public Regex Constraint { get; private set; }
+ public Regex Constraint
+ {
+ get
+ {
+ // Create regex instance lazily to avoid compiling regexes at app startup. Delay creation until constraint is first evaluated.
+ // This is not thread safe. No side effect but multiple instances of a regex instance could be created from a burst of requests.
+ _constraint ??= new Regex(
+ _regexPattern,
+ RegexOptions.CultureInvariant | RegexOptions.Compiled | RegexOptions.IgnoreCase,
+ RegexMatchTimeout);
+
+ return _constraint;
+ }
+ }
///
public bool Match(
diff --git a/src/Http/Routing/src/Constraints/StringRouteConstraint.cs b/src/Http/Routing/src/Constraints/StringRouteConstraint.cs
index 40b36510ba17..734d76ec8bb8 100644
--- a/src/Http/Routing/src/Constraints/StringRouteConstraint.cs
+++ b/src/Http/Routing/src/Constraints/StringRouteConstraint.cs
@@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Routing.Constraints;
///
/// Constrains a route parameter to contain only a specified string.
///
-public class StringRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy
+public class StringRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy, ICachableParameterPolicy
{
private readonly string _value;
diff --git a/src/Http/Routing/src/Matching/DfaMatcherBuilder.cs b/src/Http/Routing/src/Matching/DfaMatcherBuilder.cs
index f22f8c009c5a..ed9844917e3f 100644
--- a/src/Http/Routing/src/Matching/DfaMatcherBuilder.cs
+++ b/src/Http/Routing/src/Matching/DfaMatcherBuilder.cs
@@ -6,6 +6,7 @@
using System.Diagnostics;
using System.Linq;
using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing.Constraints;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.AspNetCore.Routing.Template;
using Microsoft.Extensions.Logging;
@@ -39,7 +40,8 @@ public DfaMatcherBuilder(
IEnumerable policies)
{
_loggerFactory = loggerFactory;
- _parameterPolicyFactory = parameterPolicyFactory;
+ // DfaMatcherBuilder is a transient service. Each instance has its own cache of parameter policies.
+ _parameterPolicyFactory = new CachingParameterPolicyFactory(parameterPolicyFactory);
_selector = selector;
var (nodeBuilderPolicies, endpointComparerPolicies, endpointSelectorPolicies) = ExtractPolicies(policies.OrderBy(p => p.Order));
@@ -111,6 +113,51 @@ public DfaNode BuildDfaTree(bool includeLabel = false)
return root;
}
+ private sealed class CachingParameterPolicyFactory : ParameterPolicyFactory
+ {
+ private readonly ParameterPolicyFactory _inner;
+ private readonly Dictionary _cachedParameters;
+
+ public CachingParameterPolicyFactory(ParameterPolicyFactory inner)
+ {
+ _inner = inner;
+ _cachedParameters = new Dictionary(StringComparer.Ordinal);
+ }
+
+ public override IParameterPolicy Create(RoutePatternParameterPart parameter, string inlineText)
+ {
+ // Blindly check the cache to see if it contains a match.
+ // Only cachable parameter policies are in the cache, so a match will only be available if the parameter policy key is configured to a cachable parameter policy.
+ //
+ // Note: Cache key is case sensitive. While the route prefix, e.g. "regex", is case-insensitive, the constraint could care about the case of the argument.
+ if (_cachedParameters.TryGetValue(inlineText, out var parameterPolicy))
+ {
+ return _inner.Create(parameter, parameterPolicy);
+ }
+
+ parameterPolicy = _inner.Create(parameter, inlineText);
+
+ // The created parameter policy can be wrapped in an OptionalRouteConstraint if RoutePatternParameterPart.IsOptional is true.
+ var createdParameterPolicy = (parameterPolicy is OptionalRouteConstraint optionalRouteConstraint)
+ ? optionalRouteConstraint.InnerConstraint
+ : parameterPolicy;
+
+ // Only cache policies in a known allow list. This is indicated by implementing ICachableParameterPolicy.
+ // There is a chance that a user-defined constraint has state, such as an evaluation count. That would break if the constraint is shared between routes, so don't cache.
+ if (createdParameterPolicy is ICachableParameterPolicy)
+ {
+ _cachedParameters[inlineText] = createdParameterPolicy;
+ }
+
+ return parameterPolicy;
+ }
+
+ public override IParameterPolicy Create(RoutePatternParameterPart parameter, IParameterPolicy parameterPolicy)
+ {
+ return _inner.Create(parameter, parameterPolicy);
+ }
+ }
+
private sealed class DfaBuilderWorker
{
private List _previousWork;
diff --git a/src/Http/Routing/src/Matching/ICachableParameterPolicy.cs b/src/Http/Routing/src/Matching/ICachableParameterPolicy.cs
new file mode 100644
index 000000000000..bf47a65a6205
--- /dev/null
+++ b/src/Http/Routing/src/Matching/ICachableParameterPolicy.cs
@@ -0,0 +1,11 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.AspNetCore.Routing.Matching;
+
+///
+/// A marker interface for parameter policies that can be shared.
+///
+internal interface ICachableParameterPolicy
+{
+}
diff --git a/src/Http/Routing/src/ParameterPolicyActivator.cs b/src/Http/Routing/src/ParameterPolicyActivator.cs
index ca5b77261b57..37e567d93cec 100644
--- a/src/Http/Routing/src/ParameterPolicyActivator.cs
+++ b/src/Http/Routing/src/ParameterPolicyActivator.cs
@@ -1,8 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-#nullable disable
-
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
@@ -13,35 +11,22 @@ namespace Microsoft.AspNetCore.Routing;
internal static class ParameterPolicyActivator
{
- public static T ResolveParameterPolicy(
+ public static T? ResolveParameterPolicy(
IDictionary inlineParameterPolicyMap,
- IServiceProvider serviceProvider,
+ IServiceProvider? serviceProvider,
string inlineParameterPolicy,
- out string parameterPolicyKey)
+ out string? parameterPolicyKey)
where T : IParameterPolicy
{
// IServiceProvider could be null
// DefaultInlineConstraintResolver can be created without an IServiceProvider and then call this method
- ArgumentNullException.ThrowIfNull(inlineParameterPolicyMap);
- ArgumentNullException.ThrowIfNull(inlineParameterPolicy);
-
- string argumentString;
- var indexOfFirstOpenParens = inlineParameterPolicy.IndexOf('(');
- if (indexOfFirstOpenParens >= 0 && inlineParameterPolicy.EndsWith(')'))
- {
- parameterPolicyKey = inlineParameterPolicy.Substring(0, indexOfFirstOpenParens);
- argumentString = inlineParameterPolicy.Substring(
- indexOfFirstOpenParens + 1,
- inlineParameterPolicy.Length - indexOfFirstOpenParens - 2);
- }
- else
- {
- parameterPolicyKey = inlineParameterPolicy;
- argumentString = null;
- }
-
- if (!inlineParameterPolicyMap.TryGetValue(parameterPolicyKey, out var parameterPolicyType))
+ if (!ResolveParameterPolicyTypeAndArgument(
+ inlineParameterPolicyMap,
+ inlineParameterPolicy,
+ out parameterPolicyKey,
+ out var argumentString,
+ out var parameterPolicyType))
{
return default;
}
@@ -78,12 +63,38 @@ public static T ResolveParameterPolicy(
}
}
+ private static bool ResolveParameterPolicyTypeAndArgument(
+ IDictionary inlineParameterPolicyMap,
+ string inlineParameterPolicy,
+ out string? policyKey,
+ out string? argumentString,
+ [NotNullWhen(true)] out Type? policyType)
+ {
+ ArgumentNullException.ThrowIfNull(inlineParameterPolicy);
+
+ var indexOfFirstOpenParens = inlineParameterPolicy.IndexOf('(');
+ if (indexOfFirstOpenParens >= 0 && inlineParameterPolicy.EndsWith(')'))
+ {
+ policyKey = inlineParameterPolicy.Substring(0, indexOfFirstOpenParens);
+ argumentString = inlineParameterPolicy.Substring(
+ indexOfFirstOpenParens + 1,
+ inlineParameterPolicy.Length - indexOfFirstOpenParens - 2);
+ }
+ else
+ {
+ policyKey = inlineParameterPolicy;
+ argumentString = null;
+ }
+
+ return inlineParameterPolicyMap.TryGetValue(policyKey, out policyType);
+ }
+
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2006:UnrecognizedReflectionPattern", Justification = "This type comes from the ConstraintMap.")]
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2070", Justification = "We ensure the constructor is preserved when the constraint map is added.")]
- private static IParameterPolicy CreateParameterPolicy(IServiceProvider serviceProvider, Type parameterPolicyType, string argumentString)
+ private static IParameterPolicy CreateParameterPolicy(IServiceProvider? serviceProvider, Type parameterPolicyType, string? argumentString)
{
- ConstructorInfo activationConstructor = null;
- object[] parameters = null;
+ ConstructorInfo? activationConstructor = null;
+ object?[]? parameters = null;
var constructors = parameterPolicyType.GetConstructors();
// If there is only one constructor and it has a single parameter, pass the argument string directly
@@ -91,7 +102,7 @@ private static IParameterPolicy CreateParameterPolicy(IServiceProvider servicePr
if (constructors.Length == 1 && GetNonConvertableParameterTypeCount(serviceProvider, constructors[0].GetParameters()) == 1)
{
activationConstructor = constructors[0];
- parameters = ConvertArguments(serviceProvider, activationConstructor.GetParameters(), new string[] { argumentString });
+ parameters = ConvertArguments(serviceProvider, activationConstructor.GetParameters(), new string?[] { argumentString });
}
else
{
@@ -135,7 +146,7 @@ private static IParameterPolicy CreateParameterPolicy(IServiceProvider servicePr
return (IParameterPolicy)activationConstructor.Invoke(parameters);
}
- private static int GetNonConvertableParameterTypeCount(IServiceProvider serviceProvider, ParameterInfo[] parameters)
+ private static int GetNonConvertableParameterTypeCount(IServiceProvider? serviceProvider, ParameterInfo[] parameters)
{
if (serviceProvider == null)
{
@@ -154,9 +165,9 @@ private static int GetNonConvertableParameterTypeCount(IServiceProvider serviceP
return count;
}
- private static object[] ConvertArguments(IServiceProvider serviceProvider, ParameterInfo[] parameterInfos, string[] arguments)
+ private static object?[] ConvertArguments(IServiceProvider? serviceProvider, ParameterInfo[] parameterInfos, string?[] arguments)
{
- var parameters = new object[parameterInfos.Length];
+ var parameters = new object?[parameterInfos.Length];
var argumentPosition = 0;
for (var i = 0; i < parameterInfos.Length; i++)
{
diff --git a/src/Http/Routing/test/UnitTests/Matching/DfaMatcherBuilderTest.cs b/src/Http/Routing/test/UnitTests/Matching/DfaMatcherBuilderTest.cs
index fcf535b96478..cfc4acb837b6 100644
--- a/src/Http/Routing/test/UnitTests/Matching/DfaMatcherBuilderTest.cs
+++ b/src/Http/Routing/test/UnitTests/Matching/DfaMatcherBuilderTest.cs
@@ -3373,6 +3373,75 @@ public void CreateCandidate_RouteConstraints()
Assert.Single(candidate.Constraints);
}
+ [Fact]
+ public void CreateCandidate_InlineRouteConstraints_Duplicate_SameInstance()
+ {
+ // Arrange
+ var endpoint1 = CreateEndpoint("/a/b/{c:int}");
+ var endpoint2 = CreateEndpoint("/d/e/{f:int}");
+
+ var builder = CreateDfaMatcherBuilder();
+
+ // Act
+ var candidate1 = builder.CreateCandidate(endpoint1, score: 0);
+ var candidate2 = builder.CreateCandidate(endpoint2, score: 0);
+
+ // Assert
+ Assert.Equal(Candidate.CandidateFlags.HasConstraints | Candidate.CandidateFlags.HasCaptures, candidate1.Flags);
+ var constraint1 = Assert.Single(candidate1.Constraints);
+
+ Assert.Equal(Candidate.CandidateFlags.HasConstraints | Candidate.CandidateFlags.HasCaptures, candidate2.Flags);
+ var constraint2 = Assert.Single(candidate2.Constraints);
+
+ Assert.Same(constraint1.Value, constraint2.Value);
+ }
+
+ [Fact]
+ public void CreateCandidate_InlineRouteConstraintsWithArgument_Duplicate_SameInstance()
+ {
+ // Arrange
+ var endpoint1 = CreateEndpoint("/a/b/{c:regex([A-Z])}");
+ var endpoint2 = CreateEndpoint("/d/e/{f:regex([A-Z])}");
+
+ var builder = CreateDfaMatcherBuilder();
+
+ // Act
+ var candidate1 = builder.CreateCandidate(endpoint1, score: 0);
+ var candidate2 = builder.CreateCandidate(endpoint2, score: 0);
+
+ // Assert
+ Assert.Equal(Candidate.CandidateFlags.HasConstraints | Candidate.CandidateFlags.HasCaptures, candidate1.Flags);
+ var constraint1 = Assert.Single(candidate1.Constraints);
+
+ Assert.Equal(Candidate.CandidateFlags.HasConstraints | Candidate.CandidateFlags.HasCaptures, candidate2.Flags);
+ var constraint2 = Assert.Single(candidate2.Constraints);
+
+ Assert.Same(constraint1.Value, constraint2.Value);
+ }
+
+ [Fact]
+ public void CreateCandidate_InlineRouteConstraintsWithArgument_DifferentArgument_DifferentInstance()
+ {
+ // Arrange
+ var endpoint1 = CreateEndpoint("/a/b/{c:regex([A-Z])}");
+ var endpoint2 = CreateEndpoint("/d/e/{f:regex([a-z])}");
+
+ var builder = CreateDfaMatcherBuilder();
+
+ // Act
+ var candidate1 = builder.CreateCandidate(endpoint1, score: 0);
+ var candidate2 = builder.CreateCandidate(endpoint2, score: 0);
+
+ // Assert
+ Assert.Equal(Candidate.CandidateFlags.HasConstraints | Candidate.CandidateFlags.HasCaptures, candidate1.Flags);
+ var constraint1 = Assert.Single(candidate1.Constraints);
+
+ Assert.Equal(Candidate.CandidateFlags.HasConstraints | Candidate.CandidateFlags.HasCaptures, candidate2.Flags);
+ var constraint2 = Assert.Single(candidate2.Constraints);
+
+ Assert.NotSame(constraint1.Value, constraint2.Value);
+ }
+
[Fact]
public void CreateCandidate_CustomParameterPolicy()
{