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+ /// <item>
97+ /// <description>Supports both <see cref="RouteDirection.IncomingRequest"/> and <see cref="RouteDirection.UrlGeneration"/></description>
98+ /// </item>
99+ /// </list>
100+ /// </remarks>
101+ public class NotRouteConstraint : IRouteConstraint , IParameterLiteralNodeMatchingPolicy
102+ {
103+ /// <summary>
104+ /// Gets the array of inner constraint names to be negated.
105+ /// </summary>
106+ private string [ ] _inner { get ; }
107+
108+ /// <summary>
109+ /// Cached constraint map to avoid repeated reflection-based lookups.
110+ /// </summary>
111+ private static IDictionary < string , Type > ? _cachedConstraintMap ;
112+
113+ /// <summary>
114+ /// Initializes a new instance of the <see cref="NotRouteConstraint"/> class
115+ /// with the specified inner constraints.
116+ /// </summary>
117+ /// <param name="constraints">
118+ /// A semicolon-separated string containing the names of constraints to negate.
119+ /// Can be a single constraint name (e.g., "int") or multiple constraints (e.g., "int;bool;guid").
120+ /// Parameterized constraints are supported (e.g., "min(18);length(5)").
121+ /// Unknown constraint names are treated as non-matching constraints.
122+ /// </param>
123+ /// <remarks>
124+ /// <para>The constraints string is split by semicolons to create individual constraint checks.</para>
125+ /// <para>Examples of valid constraint strings:</para>
126+ /// <list type="bullet">
127+ /// <item><description><c>"int"</c> - Single type constraint</description></item>
128+ /// <item><description><c>"int;bool"</c> - Multiple type constraints</description></item>
129+ /// <item><description><c>"min(18)"</c> - Parameterized constraint</description></item>
130+ /// <item><description><c>"length(5);alpha"</c> - Mixed constraint types</description></item>
131+ /// <item><description><c>""</c> - Empty string (always returns true)</description></item>
132+ /// </list>
133+ /// </remarks>
134+ public NotRouteConstraint ( string constraints )
135+ {
136+ _inner = constraints . Split ( ";" ) ;
137+ }
138+
139+ private static IDictionary < string , Type > GetConstraintMap ( )
140+ {
141+ // Use cached map or fall back to default constraint map
142+ return _cachedConstraintMap ??= GetDefaultConstraintMap ( ) ;
143+ }
144+
145+ private static Dictionary < string , Type > GetDefaultConstraintMap ( )
146+ {
147+ // FIXME: I'm not sure if this is a good thing to do because
148+ // it requires weak spreading between the ConstraintMap and
149+ // RouteOptions. It doesn't seem appropriate to create two
150+ // identical variables for this...
151+
152+ var defaults = new Dictionary < string , Type > ( StringComparer . OrdinalIgnoreCase )
153+ {
154+ // Type-specific constraints
155+ [ "int" ] = typeof ( IntRouteConstraint ) ,
156+ [ "bool" ] = typeof ( BoolRouteConstraint ) ,
157+ [ "datetime" ] = typeof ( DateTimeRouteConstraint ) ,
158+ [ "decimal" ] = typeof ( DecimalRouteConstraint ) ,
159+ [ "double" ] = typeof ( DoubleRouteConstraint ) ,
160+ [ "float" ] = typeof ( FloatRouteConstraint ) ,
161+ [ "guid" ] = typeof ( GuidRouteConstraint ) ,
162+ [ "long" ] = typeof ( LongRouteConstraint ) ,
163+
164+ // Length constraints
165+ [ "minlength" ] = typeof ( MinLengthRouteConstraint ) ,
166+ [ "maxlength" ] = typeof ( MaxLengthRouteConstraint ) ,
167+ [ "length" ] = typeof ( LengthRouteConstraint ) ,
168+
169+ // Min/Max value constraints
170+ [ "min" ] = typeof ( MinRouteConstraint ) ,
171+ [ "max" ] = typeof ( MaxRouteConstraint ) ,
172+ [ "range" ] = typeof ( RangeRouteConstraint ) ,
173+
174+ // Alpha constraint
175+ [ "alpha" ] = typeof ( AlphaRouteConstraint ) ,
176+
177+ // Required constraint
178+ [ "required" ] = typeof ( RequiredRouteConstraint ) ,
179+
180+ // File constraints
181+ [ "file" ] = typeof ( FileNameRouteConstraint ) ,
182+ [ "nonfile" ] = typeof ( NonFileNameRouteConstraint ) ,
183+
184+ // Not constraint
185+ [ "not" ] = typeof ( NotRouteConstraint )
186+ } ;
187+
188+ return defaults ;
189+ }
190+
191+ /// <inheritdoc />
192+ /// <remarks>
193+ /// <para>
194+ /// This method implements the core negation logic by:
195+ /// </para>
196+ /// <list type="number">
197+ /// <item>Resolving each inner constraint name to its corresponding <see cref="IRouteConstraint"/> implementation</item>
198+ /// <item>Testing each resolved constraint against the route value</item>
199+ /// <item>Returning <c>false</c> immediately if any constraint matches (short-circuit evaluation)</item>
200+ /// <item>Returning <c>true</c> only if no constraints match</item>
201+ /// </list>
202+ /// <para>
203+ /// The method attempts to use the constraint map from <see cref="RouteOptions"/> if available via
204+ /// the HTTP context's service provider, falling back to the default constraint map if needed.
205+ /// </para>
206+ /// <para>
207+ /// Unknown constraint names are ignored (treated as non-matching), which means they don't affect
208+ /// the negation result.
209+ /// </para>
210+ /// </remarks>
211+ public bool Match (
212+ HttpContext ? httpContext ,
213+ IRouter ? route ,
214+ string routeKey ,
215+ RouteValueDictionary values ,
216+ RouteDirection routeDirection )
217+ {
218+ ArgumentNullException . ThrowIfNull ( routeKey ) ;
219+ ArgumentNullException . ThrowIfNull ( values ) ;
220+
221+ // Try to get constraint map from HttpContext first, fallback to default map
222+ IDictionary < string , Type > constraintMap ;
223+ IServiceProvider ? serviceProvider = null ;
224+
225+ if ( httpContext ? . RequestServices != null )
226+ {
227+ try
228+ {
229+ var routeOptions = httpContext . RequestServices . GetService < IOptions < RouteOptions > > ( ) ;
230+ if ( routeOptions != null )
231+ {
232+ constraintMap = routeOptions . Value . TrimmerSafeConstraintMap ;
233+ serviceProvider = httpContext . RequestServices ;
234+ }
235+ else
236+ {
237+ constraintMap = GetConstraintMap ( ) ;
238+ }
239+ }
240+ catch
241+ {
242+ constraintMap = GetConstraintMap ( ) ;
243+ }
244+ }
245+ else
246+ {
247+ constraintMap = GetConstraintMap ( ) ;
248+ }
249+
250+ foreach ( var constraintText in _inner )
251+ {
252+ var resolvedConstraint = ParameterPolicyActivator . ResolveParameterPolicy < IRouteConstraint > (
253+ constraintMap ,
254+ serviceProvider ,
255+ constraintText ,
256+ out _ ) ;
257+
258+ if ( resolvedConstraint != null )
259+ {
260+ // If any inner constraint matches, return false (negation logic)
261+ if ( resolvedConstraint . Match ( httpContext , route , routeKey , values , routeDirection ) )
262+ {
263+ return false ;
264+ }
265+ }
266+ }
267+
268+ // If no inner constraints matched, return true (all constraints were negated)
269+ return true ;
270+ }
271+
272+ bool IParameterLiteralNodeMatchingPolicy . MatchesLiteral ( string parameterName , string literal )
273+ {
274+ var constraintMap = GetConstraintMap ( ) ;
275+
276+ foreach ( var constraintText in _inner )
277+ {
278+ var resolvedConstraint = ParameterPolicyActivator . ResolveParameterPolicy < IRouteConstraint > (
279+ constraintMap ,
280+ null ,
281+ constraintText ,
282+ out _ ) ;
283+
284+ if ( resolvedConstraint is IParameterLiteralNodeMatchingPolicy literalPolicy )
285+ {
286+ // If any inner constraint matches the literal, return false (negation logic)
287+ if ( literalPolicy . MatchesLiteral ( parameterName , literal ) )
288+ {
289+ return false ;
290+ }
291+ }
292+ }
293+
294+ // If no inner constraints matched the literal, return true
295+ return true ;
296+ }
297+ }
0 commit comments