|
| 1 | +// Licensed to the .NET Foundation under one or more agreements. |
| 2 | +// The .NET Foundation licenses this file to you under the MIT license. |
| 3 | + |
| 4 | +#if !COMPONENTS |
| 5 | +using Microsoft.AspNetCore.Http; |
| 6 | +using Microsoft.AspNetCore.Routing.Matching; |
| 7 | +using Microsoft.Extensions.DependencyInjection; |
| 8 | +using Microsoft.Extensions.Options; |
| 9 | +#else |
| 10 | +using Microsoft.AspNetCore.Components.Routing; |
| 11 | +#endif |
| 12 | + |
| 13 | +namespace Microsoft.AspNetCore.Routing.Constraints; |
| 14 | + |
| 15 | +/// <summary> |
| 16 | +/// A route constraint that negates one or more inner constraints. The constraint matches |
| 17 | +/// when none of the inner constraints match the route value. |
| 18 | +/// </summary> |
| 19 | +/// <remarks> |
| 20 | +/// <para> |
| 21 | +/// The <see cref="NotRouteConstraint"/> implements logical negation for route constraints. |
| 22 | +/// It takes a semicolon-separated list of constraint names and returns <c>true</c> only |
| 23 | +/// when none of the specified constraints match the route value. |
| 24 | +/// </para> |
| 25 | +/// <para> |
| 26 | +/// <strong>Supported Features:</strong> |
| 27 | +/// </para> |
| 28 | +/// <list type="bullet"> |
| 29 | +/// <item> |
| 30 | +/// <description>Basic type constraints: <c>int</c>, <c>bool</c>, <c>guid</c>, <c>datetime</c>, <c>decimal</c>, <c>double</c>, <c>float</c>, <c>long</c></description> |
| 31 | +/// </item> |
| 32 | +/// <item> |
| 33 | +/// <description>String constraints: <c>alpha</c>, <c>length(n)</c>, <c>minlength(n)</c>, <c>maxlength(n)</c></description> |
| 34 | +/// </item> |
| 35 | +/// <item> |
| 36 | +/// <description>Numeric constraints: <c>min(n)</c>, <c>max(n)</c>, <c>range(min,max)</c></description> |
| 37 | +/// </item> |
| 38 | +/// <item> |
| 39 | +/// <description>File constraints: <c>file</c>, <c>nonfile</c></description> |
| 40 | +/// </item> |
| 41 | +/// <item> |
| 42 | +/// <description>Special constraints: <c>required</c></description> |
| 43 | +/// </item> |
| 44 | +/// <item> |
| 45 | +/// <description>Multiple constraints with semicolon separation (logical AND of negations)</description> |
| 46 | +/// </item> |
| 47 | +/// <item> |
| 48 | +/// <description>Nested negation patterns (e.g., <c>not(not(int))</c>) - fully supported as recursive constraint evaluation</description> |
| 49 | +/// </item> |
| 50 | +/// </list> |
| 51 | +/// <para> |
| 52 | +/// <strong>Examples:</strong> |
| 53 | +/// </para> |
| 54 | +/// <list type="bullet"> |
| 55 | +/// <item> |
| 56 | +/// <term><c>not(int)</c></term> |
| 57 | +/// <description>Matches any value that is NOT an integer (e.g., "abc", "12.5", "true")</description> |
| 58 | +/// </item> |
| 59 | +/// <item> |
| 60 | +/// <term><c>not(int;bool)</c></term> |
| 61 | +/// <description>Matches values that are neither integers nor booleans (e.g., "abc", "12.5")</description> |
| 62 | +/// </item> |
| 63 | +/// <item> |
| 64 | +/// <term><c>not(not(int))</c></term> |
| 65 | +/// <description>Double negation - matches integers (equivalent to just using <c>int</c> constraint)</description> |
| 66 | +/// </item> |
| 67 | +/// <item> |
| 68 | +/// <term><c>not(min(18))</c></term> |
| 69 | +/// <description>Matches integer values less than 18 or non-integer values</description> |
| 70 | +/// </item> |
| 71 | +/// <item> |
| 72 | +/// <term><c>not(alpha)</c></term> |
| 73 | +/// <description>Matches non-alphabetic values (e.g., "123", "test123")</description> |
| 74 | +/// </item> |
| 75 | +/// <item> |
| 76 | +/// <term><c>not(file)</c></term> |
| 77 | +/// <description>Matches values that don't contain file extensions</description> |
| 78 | +/// </item> |
| 79 | +/// </list> |
| 80 | +/// <para> |
| 81 | +/// <strong>Important Notes:</strong> |
| 82 | +/// </para> |
| 83 | +/// <list type="bullet"> |
| 84 | +/// <item> |
| 85 | +/// <description>Unknown constraint names are ignored and always treated as non-matching, resulting in negation returning <c>true</c></description> |
| 86 | +/// </item> |
| 87 | +/// <item> |
| 88 | +/// <description>Nested negation patterns are fully supported and work recursively (e.g., <c>not(not(int))</c> = double negation)</description> |
| 89 | +/// </item> |
| 90 | +/// <item> |
| 91 | +/// <description>Multiple constraints are combined with logical AND - ALL inner constraints must fail for the negation to succeed</description> |
| 92 | +/// </item> |
| 93 | +/// <item> |
| 94 | +/// <description>Works with both route matching and literal parameter matching scenarios</description> |
| 95 | +/// </item> |
| 96 | +/// </list> |
| 97 | +/// </remarks> |
| 98 | +#if !COMPONENTS |
| 99 | +public class NotRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy |
| 100 | +#else |
| 101 | +internal class NotRouteConstraint : IRouteConstraint |
| 102 | +#endif |
| 103 | +{ |
| 104 | + /// <summary> |
| 105 | + /// Gets the array of inner constraint names to be negated. |
| 106 | + /// </summary> |
| 107 | + private string[] _inner { get; } |
| 108 | + |
| 109 | + /// <summary> |
| 110 | + /// Cached constraint map to avoid repeated reflection-based lookups. |
| 111 | + /// </summary> |
| 112 | + private static IDictionary<string, Type>? _cachedConstraintMap; |
| 113 | + |
| 114 | + /// <summary> |
| 115 | + /// Initializes a new instance of the <see cref="NotRouteConstraint"/> class |
| 116 | + /// with the specified inner constraints. |
| 117 | + /// </summary> |
| 118 | + /// <param name="constraints"> |
| 119 | + /// A semicolon-separated string containing the names of constraints to negate. |
| 120 | + /// Can be a single constraint name (e.g., "int") or multiple constraints (e.g., "int;bool;guid"). |
| 121 | + /// Parameterized constraints are supported (e.g., "min(18);length(5)"). |
| 122 | + /// Unknown constraint names are treated as non-matching constraints. |
| 123 | + /// </param> |
| 124 | + /// <remarks> |
| 125 | + /// <para>The constraints string is split by semicolons to create individual constraint checks.</para> |
| 126 | + /// <para>Examples of valid constraint strings:</para> |
| 127 | + /// <list type="bullet"> |
| 128 | + /// <item><description><c>"int"</c> - Single type constraint</description></item> |
| 129 | + /// <item><description><c>"int;bool"</c> - Multiple type constraints</description></item> |
| 130 | + /// <item><description><c>"min(18)"</c> - Parameterized constraint</description></item> |
| 131 | + /// <item><description><c>"length(5);alpha"</c> - Mixed constraint types</description></item> |
| 132 | + /// <item><description><c>""</c> - Empty string (always returns true)</description></item> |
| 133 | + /// </list> |
| 134 | + /// </remarks> |
| 135 | + public NotRouteConstraint(string constraints) |
| 136 | + { |
| 137 | + _inner = constraints.Split(";"); |
| 138 | + } |
| 139 | + |
| 140 | + private static IDictionary<string, Type> GetConstraintMap() |
| 141 | + { |
| 142 | + // Use cached map or fall back to default constraint map |
| 143 | + return _cachedConstraintMap ??= GetDefaultConstraintMap(); |
| 144 | + } |
| 145 | + |
| 146 | + private static Dictionary<string, Type> GetDefaultConstraintMap() |
| 147 | + { |
| 148 | + // FIXME: I'm not sure if this is a good thing to do because |
| 149 | + // it requires weak spreading between the ConstraintMap and |
| 150 | + // RouteOptions. It doesn't seem appropriate to create two |
| 151 | + // identical variables for this... |
| 152 | + |
| 153 | + var defaults = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase) |
| 154 | + { |
| 155 | + // Type-specific constraints |
| 156 | + ["int"] = typeof(IntRouteConstraint), |
| 157 | + ["bool"] = typeof(BoolRouteConstraint), |
| 158 | + ["datetime"] = typeof(DateTimeRouteConstraint), |
| 159 | + ["decimal"] = typeof(DecimalRouteConstraint), |
| 160 | + ["double"] = typeof(DoubleRouteConstraint), |
| 161 | + ["float"] = typeof(FloatRouteConstraint), |
| 162 | + ["guid"] = typeof(GuidRouteConstraint), |
| 163 | + ["long"] = typeof(LongRouteConstraint), |
| 164 | + |
| 165 | + // Length constraints |
| 166 | + ["minlength"] = typeof(MinLengthRouteConstraint), |
| 167 | + ["maxlength"] = typeof(MaxLengthRouteConstraint), |
| 168 | + ["length"] = typeof(LengthRouteConstraint), |
| 169 | + |
| 170 | + // Min/Max value constraints |
| 171 | + ["min"] = typeof(MinRouteConstraint), |
| 172 | + ["max"] = typeof(MaxRouteConstraint), |
| 173 | + ["range"] = typeof(RangeRouteConstraint), |
| 174 | + |
| 175 | + // Alpha constraint |
| 176 | + ["alpha"] = typeof(AlphaRouteConstraint), |
| 177 | + |
| 178 | +#if !COMPONENTS |
| 179 | + ["required"] = typeof(RequiredRouteConstraint), |
| 180 | +#endif |
| 181 | + |
| 182 | + // File constraints |
| 183 | + ["file"] = typeof(FileNameRouteConstraint), |
| 184 | + ["nonfile"] = typeof(NonFileNameRouteConstraint), |
| 185 | + |
| 186 | + // Not constraint |
| 187 | + ["not"] = typeof(NotRouteConstraint) |
| 188 | + }; |
| 189 | + |
| 190 | + return defaults; |
| 191 | + } |
| 192 | + |
| 193 | + /// <inheritdoc /> |
| 194 | + /// <remarks> |
| 195 | + /// <para> |
| 196 | + /// This method implements the core negation logic by: |
| 197 | + /// </para> |
| 198 | + /// <list type="number"> |
| 199 | + /// <item>Resolving each inner constraint name to its corresponding <see cref="IRouteConstraint"/> implementation</item> |
| 200 | + /// <item>Testing each resolved constraint against the route value</item> |
| 201 | + /// <item>Returning <c>false</c> immediately if any constraint matches (short-circuit evaluation)</item> |
| 202 | + /// <item>Returning <c>true</c> only if no constraints match</item> |
| 203 | + /// </list> |
| 204 | + /// <para> |
| 205 | + /// The method attempts to use the constraint map from <see cref="RouteOptions"/> if available via |
| 206 | + /// the HTTP context's service provider, falling back to the default constraint map if needed. |
| 207 | + /// </para> |
| 208 | + /// <para> |
| 209 | + /// Unknown constraint names are ignored (treated as non-matching), which means they don't affect |
| 210 | + /// the negation result. |
| 211 | + /// </para> |
| 212 | + /// </remarks> |
| 213 | + public bool Match( |
| 214 | +#if !COMPONENTS |
| 215 | + HttpContext? httpContext, |
| 216 | + IRouter? route, |
| 217 | + string routeKey, |
| 218 | + RouteValueDictionary values, |
| 219 | + RouteDirection routeDirection) |
| 220 | +#else |
| 221 | + string routeKey, |
| 222 | + RouteValueDictionary values) |
| 223 | +#endif |
| 224 | + { |
| 225 | + ArgumentNullException.ThrowIfNull(routeKey); |
| 226 | + ArgumentNullException.ThrowIfNull(values); |
| 227 | + |
| 228 | + // Try to get constraint map from HttpContext first, fallback to default map |
| 229 | + IDictionary<string, Type> constraintMap; |
| 230 | + IServiceProvider? serviceProvider = null; |
| 231 | + |
| 232 | +#if !COMPONENTS |
| 233 | + if (httpContext?.RequestServices != null) |
| 234 | + { |
| 235 | + try |
| 236 | + { |
| 237 | + var routeOptions = httpContext.RequestServices.GetService<IOptions<RouteOptions>>(); |
| 238 | + if (routeOptions != null) |
| 239 | + { |
| 240 | + constraintMap = routeOptions.Value.TrimmerSafeConstraintMap; |
| 241 | + serviceProvider = httpContext.RequestServices; |
| 242 | + } |
| 243 | + else |
| 244 | + { |
| 245 | + constraintMap = GetConstraintMap(); |
| 246 | + } |
| 247 | + } |
| 248 | + catch |
| 249 | + { |
| 250 | + constraintMap = GetConstraintMap(); |
| 251 | + } |
| 252 | + } |
| 253 | + else |
| 254 | + { |
| 255 | + constraintMap = GetConstraintMap(); |
| 256 | + } |
| 257 | +#else |
| 258 | + constraintMap = GetConstraintMap(); |
| 259 | +#endif |
| 260 | + |
| 261 | + foreach (var constraintText in _inner) |
| 262 | + { |
| 263 | + var resolvedConstraint = ParameterPolicyActivator.ResolveParameterPolicy<IRouteConstraint>( |
| 264 | + constraintMap, |
| 265 | + serviceProvider, |
| 266 | + constraintText, |
| 267 | + out _); |
| 268 | + |
| 269 | + if (resolvedConstraint != null) |
| 270 | + { |
| 271 | + // If any inner constraint matches, return false (negation logic) |
| 272 | +#if !COMPONENTS |
| 273 | + if (resolvedConstraint.Match(httpContext, route, routeKey, values, routeDirection)) |
| 274 | +#else |
| 275 | + if (resolvedConstraint.Match(routeKey, values)) |
| 276 | +#endif |
| 277 | + { |
| 278 | + return false; |
| 279 | + } |
| 280 | + } |
| 281 | + } |
| 282 | + |
| 283 | + // If no inner constraints matched, return true (all constraints were negated) |
| 284 | + return true; |
| 285 | + } |
| 286 | + |
| 287 | +#if !COMPONENTS |
| 288 | + bool IParameterLiteralNodeMatchingPolicy.MatchesLiteral(string parameterName, string literal) |
| 289 | + { |
| 290 | + var constraintMap = GetConstraintMap(); |
| 291 | + |
| 292 | + foreach (var constraintText in _inner) |
| 293 | + { |
| 294 | + var resolvedConstraint = ParameterPolicyActivator.ResolveParameterPolicy<IRouteConstraint>( |
| 295 | + constraintMap, |
| 296 | + null, |
| 297 | + constraintText, |
| 298 | + out _); |
| 299 | + |
| 300 | + if (resolvedConstraint is IParameterLiteralNodeMatchingPolicy literalPolicy) |
| 301 | + { |
| 302 | + // If any inner constraint matches the literal, return false (negation logic) |
| 303 | + if (literalPolicy.MatchesLiteral(parameterName, literal)) |
| 304 | + { |
| 305 | + return false; |
| 306 | + } |
| 307 | + } |
| 308 | + } |
| 309 | + |
| 310 | + // If no inner constraints matched the literal, return true |
| 311 | + return true; |
| 312 | + } |
| 313 | +#endif |
| 314 | +} |
0 commit comments