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