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() {