Skip to content

Commit 9f16e38

Browse files
Chris Martinezcommonsensesoftware
authored andcommitted
Optimize assignment and look up of the best candidate match
1 parent 8fe636f commit 9f16e38

File tree

3 files changed

+63
-23
lines changed

3 files changed

+63
-23
lines changed

src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/ActionSelectionResult.cs

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Linq;
7+
using static System.Threading.Interlocked;
78

89
/// <summary>
910
/// Represents an API versioning action selection result for which a versioning policy can be applied.
@@ -12,6 +13,7 @@ public class ActionSelectionResult
1213
{
1314
readonly Dictionary<int, ICollection<ActionDescriptor>> candidateActions = new Dictionary<int, ICollection<ActionDescriptor>>();
1415
readonly Dictionary<int, ICollection<ActionDescriptorMatch>> matchingActions = new Dictionary<int, ICollection<ActionDescriptorMatch>>();
16+
ActionDescriptorMatch bestMatch;
1517

1618
/// <summary>
1719
/// Gets the number of action selection iterations that have occurred.
@@ -26,26 +28,7 @@ public class ActionSelectionResult
2628
/// <remarks>This property returns the first occurrence of a single match in the earliest iteration. If
2729
/// no matches exist in any iteration or multiple matches exist, this property returns <c>null</c>.</remarks>
2830
[CLSCompliant( false )]
29-
public ActionDescriptorMatch BestMatch
30-
{
31-
get
32-
{
33-
foreach ( var iteration in matchingActions )
34-
{
35-
switch ( iteration.Value.Count )
36-
{
37-
case 0:
38-
break;
39-
case 1:
40-
return iteration.Value.ElementAt( 0 );
41-
default:
42-
return null;
43-
}
44-
}
45-
46-
return null;
47-
}
48-
}
31+
public ActionDescriptorMatch BestMatch => bestMatch;
4932

5033
/// <summary>
5134
/// Gets a collection of candidate actions grouped by action selection iteration.
@@ -160,5 +143,23 @@ public void EndIteration()
160143

161144
++Iterations;
162145
}
146+
147+
/// <summary>
148+
/// Attempts to update the best match.
149+
/// </summary>
150+
/// <param name="match">The <see cref="ActionDescriptorMatch">match</see> to attempt to set as the best match.
151+
/// This value can be <c>null</c>.</param>
152+
/// <returns>True if the match is successfully set; otherwise, false.</returns>
153+
[CLSCompliant( false )]
154+
public bool TrySetBestMatch( ActionDescriptorMatch match )
155+
{
156+
if ( match == null )
157+
{
158+
return false;
159+
}
160+
161+
const ActionDescriptorMatch NoMatch = default( ActionDescriptorMatch );
162+
return CompareExchange( ref bestMatch, match, NoMatch ) == NoMatch;
163+
}
163164
}
164165
}

src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/ApiVersionActionSelector.cs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
using System.Diagnostics.Contracts;
1414
using System.Linq;
1515
using static ErrorCodes;
16+
using System.Collections;
1617

1718
/// <summary>
1819
/// Represents the logic for selecting an API-versioned, action method.
@@ -129,7 +130,10 @@ public virtual ActionDescriptor SelectBestCandidate( RouteContext context, IRead
129130
if ( finalMatches.Count > 0 )
130131
{
131132
var routeData = new RouteData( context.RouteData );
132-
selectionResult.AddMatches( finalMatches.Select( action => new ActionDescriptorMatch( action, routeData ) ) );
133+
var matchingActions = new MatchingActionSequence( finalMatches, routeData );
134+
135+
selectionResult.AddMatches( matchingActions );
136+
selectionResult.TrySetBestMatch( matchingActions.BestMatch );
133137
}
134138

135139
// note: even though we may have had a successful match, this method could be called multiple times. the final decision
@@ -360,5 +364,41 @@ IReadOnlyList<ActionSelectorCandidate> EvaluateActionConstraintsCore( RouteConte
360364
return EvaluateActionConstraintsCore( context, actionsWithoutConstraint, order );
361365
}
362366
}
367+
368+
sealed class MatchingActionSequence : IEnumerable<ActionDescriptorMatch>
369+
{
370+
readonly IReadOnlyList<ActionDescriptor> matches;
371+
readonly RouteData routeData;
372+
373+
internal MatchingActionSequence( IReadOnlyList<ActionDescriptor> matches, RouteData routeData )
374+
{
375+
Contract.Requires( matches != null );
376+
Contract.Requires( matches.Count > 0 );
377+
Contract.Requires( routeData != null );
378+
379+
this.matches = matches;
380+
this.routeData = routeData;
381+
}
382+
383+
internal ActionDescriptorMatch BestMatch { get; private set; }
384+
385+
public IEnumerator<ActionDescriptorMatch> GetEnumerator()
386+
{
387+
if ( matches.Count == 1 )
388+
{
389+
BestMatch = new ActionDescriptorMatch( matches[0], routeData );
390+
yield return BestMatch;
391+
}
392+
else
393+
{
394+
for ( var i = 0; i < matches.Count; i++ )
395+
{
396+
yield return new ActionDescriptorMatch( matches[i], routeData );
397+
}
398+
}
399+
}
400+
401+
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
402+
}
363403
}
364404
}

test/Microsoft.AspNetCore.Mvc.Versioning.Tests/Versioning/TestApiVersionActionSelector.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,8 @@ public override ActionDescriptor SelectBestCandidate( RouteContext context, IRea
2121
{
2222
var bestCandidate = base.SelectBestCandidate( context, candidates );
2323
var selectionResult = context.HttpContext.ApiVersionProperties().SelectionResult;
24-
var matchingActions = selectionResult.MatchingActions.OrderBy( kvp => kvp.Key ).SelectMany( kvp => kvp.Value ).Distinct();
2524

26-
SelectedCandidate = matchingActions.FirstOrDefault()?.Action;
25+
SelectedCandidate = selectionResult.BestMatch?.Action;
2726

2827
return bestCandidate;
2928
}

0 commit comments

Comments
 (0)