Skip to content

Commit c6a20bd

Browse files
Updates the matcher policy to weigh explicit versus implicit matches without considering the score and invalidating candidates that are still valid. Fixes #1101
1 parent bd469de commit c6a20bd

File tree

3 files changed

+54
-50
lines changed

3 files changed

+54
-50
lines changed

src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingAttributes/Controllers/OverlappingRouteTemplateController.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55

66
namespace Asp.Versioning.Mvc.UsingAttributes.Controllers;
77

8+
using Microsoft.AspNetCore.Http;
89
using Microsoft.AspNetCore.Mvc;
10+
using System.Net.Mime;
911

1012
[ApiController]
1113
[ApiVersion( "1.0" )]
@@ -31,4 +33,13 @@ public class OverlappingRouteTemplateController : ControllerBase
3133
[HttpGet( "[action]/{id}" )]
3234
[MapToApiVersion( "1.0" )]
3335
public string Echo( string id ) => id;
36+
37+
[HttpGet]
38+
[ProducesResponseType( StatusCodes.Status200OK )]
39+
public IActionResult Get() => Ok();
40+
41+
[HttpPost]
42+
[Consumes( MediaTypeNames.Application.Json )]
43+
[ProducesResponseType( StatusCodes.Status201Created )]
44+
public IActionResult Post( [FromBody] string body ) => CreatedAtAction( nameof( Get ), body );
3445
}

src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingAttributes/given a versioned Controller/when two route templates overlap.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,21 @@ public async Task then_route_with_same_score_and_different_versions_should_retur
8686
response.StatusCode.Should().Be( statusCode );
8787
}
8888

89+
[Fact]
90+
public async Task then_route_with_different_scores_and_same_version_should_return_expected_status()
91+
{
92+
// arrange
93+
94+
95+
// act
96+
var get = await GetAsync( "api/v1/values" );
97+
var post = await PostAsync( "api/v1/values", "test" );
98+
99+
// assert
100+
get.StatusCode.Should().Be( OK );
101+
post.StatusCode.Should().Be( Created );
102+
}
103+
89104
public when_two_route_templates_overlap( OverlappingRouteTemplateFixture fixture, ITestOutputHelper console )
90105
: base( fixture ) => console.WriteLine( fixture.DirectedGraphVisualizationUrl );
91106
}

src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionMatcherPolicy.cs

Lines changed: 28 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -372,14 +372,9 @@ private static void Collate(
372372
private static (bool Matched, bool HasCandidates) MatchApiVersion( CandidateSet candidates, ApiVersion? apiVersion )
373373
{
374374
var total = candidates.Count;
375-
var count = 0;
376-
var array = default( Match[] );
377-
var bestMatch = default( Match? );
375+
var matched = false;
376+
var implicitMatches = new Matches( stackalloc int[total] );
378377
var hasCandidates = false;
379-
Span<Match> matches =
380-
total <= 16
381-
? stackalloc Match[total]
382-
: ( array = ArrayPool<Match>.Shared.Rent( total ) ).AsSpan();
383378

384379
for ( var i = 0; i < total; i++ )
385380
{
@@ -397,56 +392,38 @@ private static (bool Matched, bool HasCandidates) MatchApiVersion( CandidateSet
397392
continue;
398393
}
399394

400-
var score = candidate.Score;
401-
bool isExplicit;
402-
403-
// perf: always make the candidate invalid so we only need to loop through the
404-
// final, best matches for any remaining candidates
405-
candidates.SetValidity( i, false );
406-
407395
switch ( metadata.MappingTo( apiVersion ) )
408396
{
409397
case Explicit:
410-
isExplicit = true;
398+
matched = true;
411399
break;
412400
case Implicit:
413-
isExplicit = metadata.IsApiVersionNeutral;
401+
if ( metadata.IsApiVersionNeutral )
402+
{
403+
matched = true;
404+
}
405+
else
406+
{
407+
implicitMatches.Add( i );
408+
}
409+
414410
break;
415411
default:
412+
candidates.SetValidity( i, false );
416413
continue;
417414
}
418-
419-
var match = new Match( i, score, isExplicit );
420-
421-
matches[count++] = match;
422-
423-
if ( !bestMatch.HasValue || match.CompareTo( bestMatch.Value ) > 0 )
424-
{
425-
bestMatch = match;
426-
}
427415
}
428416

429-
var matched = false;
430-
431-
if ( bestMatch.HasValue )
417+
if ( matched )
432418
{
433-
matched = true;
434-
var match = bestMatch.Value;
435-
436-
for ( var i = 0; i < count; i++ )
419+
for ( var i = 0; i < implicitMatches.Count; i++ )
437420
{
438-
ref readonly var otherMatch = ref matches[i];
439-
440-
if ( match.CompareTo( otherMatch ) == 0 )
441-
{
442-
candidates.SetValidity( otherMatch.Index, true );
443-
}
421+
candidates.SetValidity( implicitMatches[i], false );
444422
}
445423
}
446-
447-
if ( array is not null )
424+
else
448425
{
449-
ArrayPool<Match>.Shared.Return( array );
426+
matched = !implicitMatches.IsEmpty;
450427
}
451428

452429
return (matched, hasCandidates);
@@ -481,17 +458,18 @@ private ValueTask<ApiVersion> TrySelectApiVersionAsync( HttpContext httpContext,
481458
bool INodeBuilderPolicy.AppliesToEndpoints( IReadOnlyList<Endpoint> endpoints ) =>
482459
!ContainsDynamicEndpoints( endpoints ) && AppliesToEndpoints( endpoints );
483460

484-
private readonly struct Match( int index, int score, bool isExplicit )
461+
private ref struct Matches( Span<int> indexes )
485462
{
486-
internal readonly int Index = index;
487-
internal readonly int Score = score;
488-
internal readonly bool IsExplicit = isExplicit;
463+
private readonly Span<int> indexes = indexes;
464+
private int count;
489465

490-
internal int CompareTo( in Match other )
491-
{
492-
var result = -Score.CompareTo( other.Score );
493-
return result == 0 ? IsExplicit.CompareTo( other.IsExplicit ) : result;
494-
}
466+
public readonly int this[int index] => indexes[index];
467+
468+
public readonly bool IsEmpty => count == 0;
469+
470+
public readonly int Count => count;
471+
472+
public void Add( int index ) => indexes[count++] = index;
495473
}
496474

497475
private sealed class ApiVersionCollator(

0 commit comments

Comments
 (0)