@@ -8,6 +8,7 @@ namespace Asp.Versioning.Routing;
8
8
using Microsoft . AspNetCore . Routing . Patterns ;
9
9
using Microsoft . Extensions . Logging ;
10
10
using Microsoft . Extensions . Options ;
11
+ using System . Buffers ;
11
12
using System . Runtime . CompilerServices ;
12
13
using System . Text . RegularExpressions ;
13
14
using static Asp . Versioning . ApiVersionMapping ;
@@ -261,7 +262,7 @@ private static bool DifferByRouteConstraintsOnly( CandidateSet candidates )
261
262
262
263
for ( var i = 0 ; i < candidates . Count ; i ++ )
263
264
{
264
- ref var candidate = ref candidates [ i ] ;
265
+ ref readonly var candidate = ref candidates [ i ] ;
265
266
266
267
if ( candidate . Endpoint is not RouteEndpoint endpoint )
267
268
{
@@ -291,81 +292,85 @@ private static bool DifferByRouteConstraintsOnly( CandidateSet candidates )
291
292
292
293
private static ( bool Matched , bool HasCandidates ) MatchApiVersion ( CandidateSet candidates , ApiVersion ? apiVersion )
293
294
{
294
- List < int > ? bestMatches = default ;
295
- List < int > ? implicitMatches = default ;
295
+ var total = candidates . Count ;
296
+ var count = 0 ;
297
+ var array = default ( Match [ ] ) ;
298
+ var bestMatch = default ( Match ? ) ;
296
299
var hasCandidates = false ;
300
+ Span < Match > matches =
301
+ total <= 16
302
+ ? stackalloc Match [ total ]
303
+ : ( array = ArrayPool < Match > . Shared . Rent ( total ) ) . AsSpan ( ) ;
297
304
298
- for ( var i = 0 ; i < candidates . Count ; i ++ )
305
+ for ( var i = 0 ; i < total ; i ++ )
299
306
{
300
307
if ( ! candidates . IsValidCandidate ( i ) )
301
308
{
302
309
continue ;
303
310
}
304
311
305
312
hasCandidates = true ;
306
- ref var candidate = ref candidates [ i ] ;
313
+ ref readonly var candidate = ref candidates [ i ] ;
307
314
var metadata = candidate . Endpoint . Metadata . GetMetadata < ApiVersionMetadata > ( ) ;
308
315
309
316
if ( metadata == null )
310
317
{
311
318
continue ;
312
319
}
313
320
314
- // remember whether the candidate is currently valid. a matching api version will not
315
- // make the candidate valid; however, we want to short-circuit with 400 if no candidates
316
- // match the api version at all.
321
+ var score = candidate . Score ;
322
+ bool isExplicit ;
323
+
324
+ // perf: always make the candidate invalid so we only need to loop through the
325
+ // final, best matches for any remaining candidates
326
+ candidates . SetValidity ( i , false ) ;
327
+
317
328
switch ( metadata . MappingTo ( apiVersion ) )
318
329
{
319
330
case Explicit :
320
- bestMatches ??= new ( ) ;
321
- bestMatches . Add ( i ) ;
331
+ isExplicit = true ;
322
332
break ;
323
333
case Implicit :
324
- implicitMatches ??= new ( ) ;
325
- implicitMatches . Add ( i ) ;
334
+ isExplicit = metadata . IsApiVersionNeutral ;
326
335
break ;
336
+ default :
337
+ continue ;
327
338
}
328
339
329
- // perf: always make the candidate invalid so we only need to loop through the
330
- // final, best matches for any remaining candidates
331
- candidates . SetValidity ( i , false ) ;
332
- }
340
+ var match = new Match ( i , score , isExplicit ) ;
333
341
334
- if ( bestMatches is null )
335
- {
336
- if ( implicitMatches is null )
337
- {
338
- return ( false , hasCandidates ) ;
339
- }
342
+ matches [ count ++ ] = match ;
340
343
341
- for ( var i = 0 ; i < implicitMatches . Count ; i ++ )
344
+ if ( ! bestMatch . HasValue || match . CompareTo ( bestMatch . Value ) > 0 )
342
345
{
343
- candidates . SetValidity ( implicitMatches [ i ] , true ) ;
346
+ bestMatch = match ;
344
347
}
345
-
346
- return ( true , hasCandidates ) ;
347
348
}
348
349
349
- if ( bestMatches . Count == 1 && implicitMatches is not null )
350
+ var matched = false ;
351
+
352
+ if ( bestMatch . HasValue )
350
353
{
351
- ref var candidate = ref candidates [ bestMatches [ 0 ] ] ;
352
- var metadata = candidate . Endpoint . Metadata . GetMetadata < ApiVersionMetadata > ( ) ! ;
354
+ matched = true ;
355
+ var match = bestMatch . Value ;
353
356
354
- if ( metadata . IsApiVersionNeutral )
357
+ for ( var i = 0 ; i < count ; i ++ )
355
358
{
356
- for ( var i = 0 ; i < implicitMatches . Count ; i ++ )
359
+ ref readonly var otherMatch = ref matches [ i ] ;
360
+
361
+ if ( match . CompareTo ( otherMatch ) == 0 )
357
362
{
358
- candidates . SetValidity ( implicitMatches [ i ] , true ) ;
363
+ candidates . SetValidity ( otherMatch . Index , true ) ;
359
364
}
360
365
}
361
366
}
362
367
363
- for ( var i = 0 ; i < bestMatches . Count ; i ++ )
368
+ if ( array is not null )
364
369
{
365
- candidates . SetValidity ( bestMatches [ i ] , true ) ;
370
+ ArrayPool < Match > . Shared . Return ( array ) ;
366
371
}
367
372
368
- return ( true , hasCandidates ) ;
373
+ return ( matched , hasCandidates ) ;
369
374
}
370
375
371
376
private ApiVersion TrySelectApiVersion ( HttpContext httpContext , CandidateSet candidates )
@@ -393,4 +398,24 @@ private ApiVersion TrySelectApiVersion( HttpContext httpContext, CandidateSet ca
393
398
394
399
bool INodeBuilderPolicy . AppliesToEndpoints ( IReadOnlyList < Endpoint > endpoints ) =>
395
400
! ContainsDynamicEndpoints ( endpoints ) && AppliesToEndpoints ( endpoints ) ;
401
+
402
+ private readonly struct Match
403
+ {
404
+ internal readonly int Index ;
405
+ internal readonly int Score ;
406
+ internal readonly bool IsExplicit ;
407
+
408
+ internal Match ( int index , int score , bool isExplicit )
409
+ {
410
+ Index = index ;
411
+ Score = score ;
412
+ IsExplicit = isExplicit ;
413
+ }
414
+
415
+ internal int CompareTo ( in Match other )
416
+ {
417
+ var result = - Score . CompareTo ( other . Score ) ;
418
+ return result == 0 ? IsExplicit . CompareTo ( other . IsExplicit ) : result ;
419
+ }
420
+ }
396
421
}
0 commit comments