@@ -8,6 +8,7 @@ namespace Asp.Versioning.Routing;
88using Microsoft . AspNetCore . Routing . Patterns ;
99using Microsoft . Extensions . Logging ;
1010using Microsoft . Extensions . Options ;
11+ using System . Buffers ;
1112using System . Runtime . CompilerServices ;
1213using System . Text . RegularExpressions ;
1314using static Asp . Versioning . ApiVersionMapping ;
@@ -261,7 +262,7 @@ private static bool DifferByRouteConstraintsOnly( CandidateSet candidates )
261262
262263 for ( var i = 0 ; i < candidates . Count ; i ++ )
263264 {
264- ref var candidate = ref candidates [ i ] ;
265+ ref readonly var candidate = ref candidates [ i ] ;
265266
266267 if ( candidate . Endpoint is not RouteEndpoint endpoint )
267268 {
@@ -291,81 +292,85 @@ private static bool DifferByRouteConstraintsOnly( CandidateSet candidates )
291292
292293 private static ( bool Matched , bool HasCandidates ) MatchApiVersion ( CandidateSet candidates , ApiVersion ? apiVersion )
293294 {
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 ? ) ;
296299 var hasCandidates = false ;
300+ Span < Match > matches =
301+ total <= 16
302+ ? stackalloc Match [ total ]
303+ : ( array = ArrayPool < Match > . Shared . Rent ( total ) ) . AsSpan ( ) ;
297304
298- for ( var i = 0 ; i < candidates . Count ; i ++ )
305+ for ( var i = 0 ; i < total ; i ++ )
299306 {
300307 if ( ! candidates . IsValidCandidate ( i ) )
301308 {
302309 continue ;
303310 }
304311
305312 hasCandidates = true ;
306- ref var candidate = ref candidates [ i ] ;
313+ ref readonly var candidate = ref candidates [ i ] ;
307314 var metadata = candidate . Endpoint . Metadata . GetMetadata < ApiVersionMetadata > ( ) ;
308315
309316 if ( metadata == null )
310317 {
311318 continue ;
312319 }
313320
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+
317328 switch ( metadata . MappingTo ( apiVersion ) )
318329 {
319330 case Explicit :
320- bestMatches ??= new ( ) ;
321- bestMatches . Add ( i ) ;
331+ isExplicit = true ;
322332 break ;
323333 case Implicit :
324- implicitMatches ??= new ( ) ;
325- implicitMatches . Add ( i ) ;
334+ isExplicit = metadata . IsApiVersionNeutral ;
326335 break ;
336+ default :
337+ continue ;
327338 }
328339
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 ) ;
333341
334- if ( bestMatches is null )
335- {
336- if ( implicitMatches is null )
337- {
338- return ( false , hasCandidates ) ;
339- }
342+ matches [ count ++ ] = match ;
340343
341- for ( var i = 0 ; i < implicitMatches . Count ; i ++ )
344+ if ( ! bestMatch . HasValue || match . CompareTo ( bestMatch . Value ) > 0 )
342345 {
343- candidates . SetValidity ( implicitMatches [ i ] , true ) ;
346+ bestMatch = match ;
344347 }
345-
346- return ( true , hasCandidates ) ;
347348 }
348349
349- if ( bestMatches . Count == 1 && implicitMatches is not null )
350+ var matched = false ;
351+
352+ if ( bestMatch . HasValue )
350353 {
351- ref var candidate = ref candidates [ bestMatches [ 0 ] ] ;
352- var metadata = candidate . Endpoint . Metadata . GetMetadata < ApiVersionMetadata > ( ) ! ;
354+ matched = true ;
355+ var match = bestMatch . Value ;
353356
354- if ( metadata . IsApiVersionNeutral )
357+ for ( var i = 0 ; i < count ; i ++ )
355358 {
356- for ( var i = 0 ; i < implicitMatches . Count ; i ++ )
359+ ref readonly var otherMatch = ref matches [ i ] ;
360+
361+ if ( match . CompareTo ( otherMatch ) == 0 )
357362 {
358- candidates . SetValidity ( implicitMatches [ i ] , true ) ;
363+ candidates . SetValidity ( otherMatch . Index , true ) ;
359364 }
360365 }
361366 }
362367
363- for ( var i = 0 ; i < bestMatches . Count ; i ++ )
368+ if ( array is not null )
364369 {
365- candidates . SetValidity ( bestMatches [ i ] , true ) ;
370+ ArrayPool < Match > . Shared . Return ( array ) ;
366371 }
367372
368- return ( true , hasCandidates ) ;
373+ return ( matched , hasCandidates ) ;
369374 }
370375
371376 private ApiVersion TrySelectApiVersion ( HttpContext httpContext , CandidateSet candidates )
@@ -393,4 +398,24 @@ private ApiVersion TrySelectApiVersion( HttpContext httpContext, CandidateSet ca
393398
394399 bool INodeBuilderPolicy . AppliesToEndpoints ( IReadOnlyList < Endpoint > endpoints ) =>
395400 ! 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+ }
396421}
0 commit comments