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