Make CandidateState (publicly) immutable

This commit is contained in:
Ryan Nowak 2018-09-27 15:52:01 -07:00
parent 1f5eec1d55
commit ed15bad5fb
10 changed files with 154 additions and 59 deletions

View File

@ -2,6 +2,8 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections;
using System.Collections.Specialized;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -14,6 +16,11 @@ namespace Microsoft.AspNetCore.Routing.Matching
/// </summary> /// </summary>
public sealed class CandidateSet public sealed class CandidateSet
{ {
private const int BitVectorSize = 32;
private BitVector32 _validity;
private BitArray _largeCapactityValidity;
// We inline storage for 4 candidates here to avoid allocations in common // We inline storage for 4 candidates here to avoid allocations in common
// cases. There's no real reason why 4 is important, it just seemed like // cases. There's no real reason why 4 is important, it just seemed like
// a plausible number. // a plausible number.
@ -26,8 +33,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
/// <summary> /// <summary>
/// <para> /// <para>
/// Initializes a new instances of the candidate set structure with the provided list of endpoints /// Initializes a new instances of the candidate set structure with the provided data.
/// and associated scores.
/// </para> /// </para>
/// <para> /// <para>
/// The constructor is provided to enable unit tests of implementations of <see cref="EndpointSelector"/> /// The constructor is provided to enable unit tests of implementations of <see cref="EndpointSelector"/>
@ -35,8 +41,9 @@ namespace Microsoft.AspNetCore.Routing.Matching
/// </para> /// </para>
/// </summary> /// </summary>
/// <param name="endpoints">The list of endpoints, sorted in descending priority order.</param> /// <param name="endpoints">The list of endpoints, sorted in descending priority order.</param>
/// <param name="values">The list of <see cref="RouteValueDictionary"/> instances.</param>
/// <param name="scores">The list of endpoint scores. <see cref="CandidateState.Score"/>.</param> /// <param name="scores">The list of endpoint scores. <see cref="CandidateState.Score"/>.</param>
public CandidateSet(Endpoint[] endpoints, int[] scores) public CandidateSet(Endpoint[] endpoints, RouteValueDictionary[] values, int[] scores)
{ {
Count = endpoints.Length; Count = endpoints.Length;
@ -80,6 +87,17 @@ namespace Microsoft.AspNetCore.Routing.Matching
} }
break; break;
} }
// Initialize validity to valid by default.
if (Count < BitVectorSize)
{
// Sets the bit for each candidate that exists (bits > Count will be 0).
_validity = new BitVector32(unchecked((int)~(0xFFFFFFFFu << Count)));
}
else
{
_largeCapactityValidity = new BitArray(Count, defaultValue: true);
}
} }
internal CandidateSet(Candidate[] candidates) internal CandidateSet(Candidate[] candidates)
@ -126,25 +144,35 @@ namespace Microsoft.AspNetCore.Routing.Matching
} }
break; break;
} }
// Initialize validity to valid by default.
if (Count < BitVectorSize)
{
// Sets the bit for each candidate that exists (bits > Count will be 0).
_validity = new BitVector32(unchecked((int)~(0xFFFFFFFFu << Count)));
}
else
{
_largeCapactityValidity = new BitArray(Count, defaultValue: true);
}
} }
/// <summary> /// <summary>
/// Gets the count of candidates in the set. /// Gets the count of candidates in the set.
/// </summary> /// </summary>
public int Count { get; } public int Count { get; }
/// <summary> /// <summary>
/// Gets the <see cref="CandidateState"/> associated with the candidate <see cref="Endpoint"/> /// Gets the <see cref="CandidateState"/> associated with the candidate <see cref="Endpoint"/>
/// at <paramref name="index"/>. /// at <paramref name="index"/>.
/// </summary> /// </summary>
/// <param name="index">The candidate index.</param> /// <param name="index">The candidate index.</param>
/// <returns> /// <returns>
/// A reference to the <see cref="CandidateState"/>. The result is returned by reference /// A reference to the <see cref="CandidateState"/>. The result is returned by reference.
/// and intended to be mutated.
/// </returns> /// </returns>
public ref CandidateState this[int index] public ref CandidateState this[int index]
{ {
// Note that this is a ref-return because of both mutability and performance. // Note that this is a ref-return because of performance.
// We don't want to copy these fat structs if it can be avoided. // We don't want to copy these fat structs if it can be avoided.
// PERF: Force inlining // PERF: Force inlining
@ -177,6 +205,56 @@ namespace Microsoft.AspNetCore.Routing.Matching
} }
} }
/// <summary>
/// Gets or sets a value which indicates where the <see cref="Http.Endpoint"/> is considered
/// a valid candiate for the current request. Set this value to <c>false</c> to exclude an
/// <see cref="Http.Endpoint"/> from consideration.
/// </summary>
public bool IsValidCandidate(int index)
{
// Friendliness for inlining
if ((uint)index >= Count)
{
ThrowIndexArgumentOutOfRangeException();
}
if (Count < BitVectorSize)
{
// Get the n-th bit
return _validity[0x00000001 << index];
}
else
{
return _largeCapactityValidity[index];
}
}
/// <summary>
/// Sets the validitity of the candidate at the provided index.
/// </summary>
/// <param name="index">The candidate index.</param>
/// <param name="value">
/// The value to set. If <c>true</c> the candidate is considered valid for the current request.
/// </param>
public void SetValidity(int index, bool value)
{
// Friendliness for inlining
if ((uint)index >= Count)
{
ThrowIndexArgumentOutOfRangeException();
}
if (Count < BitVectorSize)
{
// Set the n-th bit
_validity[0x00000001 << index] = value;
}
else
{
_largeCapactityValidity[index] = value;
}
}
private static void ThrowIndexArgumentOutOfRangeException() private static void ThrowIndexArgumentOutOfRangeException()
{ {
throw new ArgumentOutOfRangeException("index"); throw new ArgumentOutOfRangeException("index");

View File

@ -14,11 +14,16 @@ namespace Microsoft.AspNetCore.Routing.Matching
{ {
Endpoint = endpoint; Endpoint = endpoint;
Score = score; Score = score;
IsValidCandidate = true;
Values = null; Values = null;
} }
internal CandidateState(Endpoint endpoint, RouteValueDictionary values, int score)
{
Endpoint = endpoint;
Values = values;
Score = score;
}
/// <summary> /// <summary>
/// Gets the <see cref="Http.Endpoint"/>. /// Gets the <see cref="Http.Endpoint"/>.
/// </summary> /// </summary>
@ -41,17 +46,10 @@ namespace Microsoft.AspNetCore.Routing.Matching
/// </remarks> /// </remarks>
public int Score { get; } public int Score { get; }
/// <summary>
/// Gets or sets a value which indicates where the <see cref="Http.Endpoint"/> is considered
/// a valid candiate for the current request. Set this value to <c>false</c> to exclude an
/// <see cref="Http.Endpoint"/> from consideration.
/// </summary>
public bool IsValidCandidate { get; set; }
/// <summary> /// <summary>
/// Gets or sets the <see cref="RouteValueDictionary"/> associated with the /// Gets or sets the <see cref="RouteValueDictionary"/> associated with the
/// <see cref="Http.Endpoint"/> and the current request. /// <see cref="Http.Endpoint"/> and the current request.
/// </summary> /// </summary>
public RouteValueDictionary Values { get; set; } public RouteValueDictionary Values { get; internal set; }
} }
} }

View File

@ -53,10 +53,10 @@ namespace Microsoft.AspNetCore.Routing.Matching
for (var i = 0; i < candidateSet.Count; i++) for (var i = 0; i < candidateSet.Count; i++)
{ {
ref var state = ref candidateSet[i]; ref var state = ref candidateSet[i];
var isValid = candidateSet.IsValidCandidate(i);
var isValid = state.IsValidCandidate;
if (isValid && foundScore == null) if (isValid && foundScore == null)
{ {
// This is the first match we've seen - speculatively assign it. // This is the first match we've seen - speculatively assign it.
endpoint = state.Endpoint; endpoint = state.Endpoint;
values = state.Values; values = state.Values;
@ -98,10 +98,9 @@ namespace Microsoft.AspNetCore.Routing.Matching
var matches = new List<Endpoint>(); var matches = new List<Endpoint>();
for (var i = 0; i < candidates.Count; i++) for (var i = 0; i < candidates.Count; i++)
{ {
ref var state = ref candidates[i]; if (candidates.IsValidCandidate(i))
if (state.IsValidCandidate)
{ {
matches.Add(state.Endpoint); matches.Add(candidates[i].Endpoint);
} }
} }

View File

@ -5,7 +5,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Routing.Patterns;
namespace Microsoft.AspNetCore.Routing.Matching namespace Microsoft.AspNetCore.Routing.Matching
@ -118,18 +117,23 @@ namespace Microsoft.AspNetCore.Routing.Matching
// Now that we have the route values, we need to process complex segments. // Now that we have the route values, we need to process complex segments.
// Complex segments go through an old API that requires a fully-materialized // Complex segments go through an old API that requires a fully-materialized
// route value dictionary. // route value dictionary.
var isMatch = true;
if ((flags & Candidate.CandidateFlags.HasComplexSegments) != 0) if ((flags & Candidate.CandidateFlags.HasComplexSegments) != 0)
{ {
isMatch &= ProcessComplexSegments(candidate.ComplexSegments, path, segments, values); if (!ProcessComplexSegments(candidate.ComplexSegments, path, segments, values))
{
candidateSet.SetValidity(i, false);
continue;
}
} }
if ((flags & Candidate.CandidateFlags.HasConstraints) != 0) if ((flags & Candidate.CandidateFlags.HasConstraints) != 0)
{ {
isMatch &= ProcessConstraints(candidate.Constraints, httpContext, values); if (!ProcessConstraints(candidate.Constraints, httpContext, values))
{
candidateSet.SetValidity(i, false);
continue;
}
} }
state.IsValidCandidate = isMatch;
} }
return _selector.SelectAsync(httpContext, context, candidateSet); return _selector.SelectAsync(httpContext, context, candidateSet);

View File

@ -28,7 +28,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
/// <para> /// <para>
/// Implementations of <see cref="IEndpointSelectorPolicy"/> should implement this method /// Implementations of <see cref="IEndpointSelectorPolicy"/> should implement this method
/// and filter the set of candidates in the <paramref name="candidates"/> by setting /// and filter the set of candidates in the <paramref name="candidates"/> by setting
/// <see cref="CandidateState.IsValidCandidate"/> to <c>false</c> where desired. /// <see cref="CandidateSet.SetValidity(int, bool)"/> to <c>false</c> where desired.
/// </para> /// </para>
/// <para> /// <para>
/// To signal an error condition, set <see cref="EndpointSelectorContext.Endpoint"/> to an /// To signal an error condition, set <see cref="EndpointSelectorContext.Endpoint"/> to an

View File

@ -22,6 +22,9 @@ namespace Microsoft.AspNetCore.Routing.Matching
[InlineData(4)] [InlineData(4)]
[InlineData(5)] // this is the break-point where we start to use a list. [InlineData(5)] // this is the break-point where we start to use a list.
[InlineData(6)] [InlineData(6)]
[InlineData(31)]
[InlineData(32)] // this is the break point where we use a BitArray
[InlineData(33)]
public void Create_CreatesCandidateSet(int count) public void Create_CreatesCandidateSet(int count)
{ {
// Arrange // Arrange
@ -41,10 +44,13 @@ namespace Microsoft.AspNetCore.Routing.Matching
for (var i = 0; i < candidateSet.Count; i++) for (var i = 0; i < candidateSet.Count; i++)
{ {
ref var state = ref candidateSet[i]; ref var state = ref candidateSet[i];
Assert.True(state.IsValidCandidate); Assert.True(candidateSet.IsValidCandidate(i));
Assert.Same(endpoints[i], state.Endpoint); Assert.Same(endpoints[i], state.Endpoint);
Assert.Equal(candidates[i].Score, state.Score); Assert.Equal(candidates[i].Score, state.Score);
Assert.Null(state.Values); Assert.Null(state.Values);
candidateSet.SetValidity(i, false);
Assert.False(candidateSet.IsValidCandidate(i));
} }
} }
@ -58,6 +64,9 @@ namespace Microsoft.AspNetCore.Routing.Matching
[InlineData(4)] [InlineData(4)]
[InlineData(5)] // this is the break-point where we start to use a list. [InlineData(5)] // this is the break-point where we start to use a list.
[InlineData(6)] [InlineData(6)]
[InlineData(31)]
[InlineData(32)] // this is the break point where we use a BitArray
[InlineData(33)]
public void Create_CreatesCandidateSet_TestConstructor(int count) public void Create_CreatesCandidateSet_TestConstructor(int count)
{ {
// Arrange // Arrange
@ -68,16 +77,19 @@ namespace Microsoft.AspNetCore.Routing.Matching
} }
// Act // Act
var candidateSet = new CandidateSet(endpoints, Enumerable.Range(0, count).ToArray()); var candidateSet = new CandidateSet(endpoints, new RouteValueDictionary[count], Enumerable.Range(0, count).ToArray());
// Assert // Assert
for (var i = 0; i < candidateSet.Count; i++) for (var i = 0; i < candidateSet.Count; i++)
{ {
ref var state = ref candidateSet[i]; ref var state = ref candidateSet[i];
Assert.True(state.IsValidCandidate); Assert.True(candidateSet.IsValidCandidate(i));
Assert.Same(endpoints[i], state.Endpoint); Assert.Same(endpoints[i], state.Endpoint);
Assert.Equal(i, state.Score); Assert.Equal(i, state.Score);
Assert.Null(state.Values); Assert.Null(state.Values);
candidateSet.SetValidity(i, false);
Assert.False(candidateSet.IsValidCandidate(i));
} }
} }

View File

@ -40,7 +40,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
var candidateSet = CreateCandidateSet(endpoints, scores); var candidateSet = CreateCandidateSet(endpoints, scores);
candidateSet[0].Values = new RouteValueDictionary(); candidateSet[0].Values = new RouteValueDictionary();
candidateSet[0].IsValidCandidate = false; candidateSet.SetValidity(0, false);
var (httpContext, context) = CreateContext(); var (httpContext, context) = CreateContext();
var selector = CreateSelector(); var selector = CreateSelector();
@ -61,7 +61,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
var candidateSet = CreateCandidateSet(endpoints, scores); var candidateSet = CreateCandidateSet(endpoints, scores);
candidateSet[0].Values = new RouteValueDictionary(); candidateSet[0].Values = new RouteValueDictionary();
candidateSet[0].IsValidCandidate = true; candidateSet.SetValidity(0, true);
var (httpContext, context) = CreateContext(); var (httpContext, context) = CreateContext();
var selector = CreateSelector(); var selector = CreateSelector();
@ -81,8 +81,8 @@ namespace Microsoft.AspNetCore.Routing.Matching
var scores = new int[] { 0, 0 }; var scores = new int[] { 0, 0 };
var candidateSet = CreateCandidateSet(endpoints, scores); var candidateSet = CreateCandidateSet(endpoints, scores);
candidateSet[0].IsValidCandidate = false; candidateSet.SetValidity(0, false);
candidateSet[1].IsValidCandidate = true; candidateSet.SetValidity(1, true);
var (httpContext, context) = CreateContext(); var (httpContext, context) = CreateContext();
var selector = CreateSelector(); var selector = CreateSelector();
@ -102,9 +102,9 @@ namespace Microsoft.AspNetCore.Routing.Matching
var scores = new int[] { 0, 0, 1 }; var scores = new int[] { 0, 0, 1 };
var candidateSet = CreateCandidateSet(endpoints, scores); var candidateSet = CreateCandidateSet(endpoints, scores);
candidateSet[0].IsValidCandidate = false; candidateSet.SetValidity(0, false);
candidateSet[1].IsValidCandidate = true; candidateSet.SetValidity(1, true);
candidateSet[2].IsValidCandidate = true; candidateSet.SetValidity(2, true);
var (httpContext, context) = CreateContext(); var (httpContext, context) = CreateContext();
var selector = CreateSelector(); var selector = CreateSelector();
@ -131,11 +131,11 @@ namespace Microsoft.AspNetCore.Routing.Matching
var scores = new int[] { 0, 1, 2, 3, 4 }; var scores = new int[] { 0, 1, 2, 3, 4 };
var candidateSet = CreateCandidateSet(endpoints, scores); var candidateSet = CreateCandidateSet(endpoints, scores);
candidateSet[0].IsValidCandidate = false; candidateSet.SetValidity(0, false);
candidateSet[1].IsValidCandidate = false; candidateSet.SetValidity(1, false);
candidateSet[2].IsValidCandidate = false; candidateSet.SetValidity(2, false);
candidateSet[3].IsValidCandidate = false; candidateSet.SetValidity(3, false);
candidateSet[4].IsValidCandidate = true; candidateSet.SetValidity(4, true);
var (httpContext, context) = CreateContext(); var (httpContext, context) = CreateContext();
var selector = CreateSelector(); var selector = CreateSelector();
@ -155,9 +155,9 @@ namespace Microsoft.AspNetCore.Routing.Matching
var scores = new int[] { 0, 1, 1 }; var scores = new int[] { 0, 1, 1 };
var candidateSet = CreateCandidateSet(endpoints, scores); var candidateSet = CreateCandidateSet(endpoints, scores);
candidateSet[0].IsValidCandidate = false; candidateSet.SetValidity(0, false);
candidateSet[1].IsValidCandidate = true; candidateSet.SetValidity(1, true);
candidateSet[2].IsValidCandidate = true; candidateSet.SetValidity(2, true);
var (httpContext, context) = CreateContext(); var (httpContext, context) = CreateContext();
var selector = CreateSelector(); var selector = CreateSelector();
@ -188,13 +188,13 @@ test: /test3", ex.Message);
.Setup(p => p.ApplyAsync(It.IsAny<HttpContext>(), It.IsAny<EndpointSelectorContext>(), It.IsAny<CandidateSet>())) .Setup(p => p.ApplyAsync(It.IsAny<HttpContext>(), It.IsAny<EndpointSelectorContext>(), It.IsAny<CandidateSet>()))
.Returns<HttpContext, EndpointSelectorContext, CandidateSet>((c, f, cs) => .Returns<HttpContext, EndpointSelectorContext, CandidateSet>((c, f, cs) =>
{ {
cs[1].IsValidCandidate = false; cs.SetValidity(1, false);
return Task.CompletedTask; return Task.CompletedTask;
}); });
candidateSet[0].IsValidCandidate = false; candidateSet.SetValidity(0, false);
candidateSet[1].IsValidCandidate = true; candidateSet.SetValidity(1, true);
candidateSet[2].IsValidCandidate = true; candidateSet.SetValidity(2, true);
var (httpContext, context) = CreateContext(); var (httpContext, context) = CreateContext();
var selector = CreateSelector(policy.Object); var selector = CreateSelector(policy.Object);
@ -234,9 +234,9 @@ test: /test3", ex.Message);
.Setup(p => p.ApplyAsync(It.IsAny<HttpContext>(), It.IsAny<EndpointSelectorContext>(), It.IsAny<CandidateSet>())) .Setup(p => p.ApplyAsync(It.IsAny<HttpContext>(), It.IsAny<EndpointSelectorContext>(), It.IsAny<CandidateSet>()))
.Throws(new InvalidOperationException()); .Throws(new InvalidOperationException());
candidateSet[0].IsValidCandidate = false; candidateSet.SetValidity(0, false);
candidateSet[1].IsValidCandidate = true; candidateSet.SetValidity(1, true);
candidateSet[2].IsValidCandidate = true; candidateSet.SetValidity(2, true);
var (httpContext, context) = CreateContext(); var (httpContext, context) = CreateContext();
var selector = CreateSelector(policy1.Object, policy2.Object); var selector = CreateSelector(policy1.Object, policy2.Object);
@ -270,7 +270,7 @@ test: /test3", ex.Message);
private static CandidateSet CreateCandidateSet(RouteEndpoint[] endpoints, int[] scores) private static CandidateSet CreateCandidateSet(RouteEndpoint[] endpoints, int[] scores)
{ {
return new CandidateSet(endpoints, scores); return new CandidateSet(endpoints, new RouteValueDictionary[endpoints.Length], scores);
} }
private static DefaultEndpointSelector CreateSelector(params MatcherPolicy[] policies) private static DefaultEndpointSelector CreateSelector(params MatcherPolicy[] policies)

View File

@ -126,12 +126,12 @@ namespace Microsoft.AspNetCore.Routing.Matching
Assert.Equal(2, cs.Count); Assert.Equal(2, cs.Count);
Assert.Same(endpoint1, cs[0].Endpoint); Assert.Same(endpoint1, cs[0].Endpoint);
Assert.True(cs[0].IsValidCandidate); Assert.True(cs.IsValidCandidate(0));
Assert.Equal(0, cs[0].Score); Assert.Equal(0, cs[0].Score);
Assert.Empty(cs[0].Values); Assert.Empty(cs[0].Values);
Assert.Same(endpoint2, cs[1].Endpoint); Assert.Same(endpoint2, cs[1].Endpoint);
Assert.True(cs[1].IsValidCandidate); Assert.True(cs.IsValidCandidate(1));
Assert.Equal(1, cs[1].Score); Assert.Equal(1, cs[1].Score);
Assert.Empty(cs[1].Values); Assert.Empty(cs[1].Values);

View File

@ -76,6 +76,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
{ {
private readonly EndpointSelector _selector; private readonly EndpointSelector _selector;
private readonly RouteEndpoint[] _candidates; private readonly RouteEndpoint[] _candidates;
private readonly RouteValueDictionary[] _values;
private readonly int[] _scores; private readonly int[] _scores;
public SelectorRouter(EndpointSelector selector, RouteEndpoint[] candidates) public SelectorRouter(EndpointSelector selector, RouteEndpoint[] candidates)
@ -83,6 +84,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
_selector = selector; _selector = selector;
_candidates = candidates; _candidates = candidates;
_values = new RouteValueDictionary[_candidates.Length];
_scores = new int[_candidates.Length]; _scores = new int[_candidates.Length];
} }
@ -99,7 +101,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
// across requests. // across requests.
context.Endpoint = null; context.Endpoint = null;
await _selector.SelectAsync(routeContext.HttpContext, context, new CandidateSet(_candidates, _scores)); await _selector.SelectAsync(routeContext.HttpContext, context, new CandidateSet(_candidates, _values, _scores));
if (context.Endpoint != null) if (context.Endpoint != null)
{ {
routeContext.Handler = (_) => Task.CompletedTask; routeContext.Handler = (_) => Task.CompletedTask;

View File

@ -78,6 +78,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
{ {
private readonly EndpointSelector _selector; private readonly EndpointSelector _selector;
private readonly RouteEndpoint[] _candidates; private readonly RouteEndpoint[] _candidates;
private readonly RouteValueDictionary[] _values;
private readonly int[] _scores; private readonly int[] _scores;
public SelectorRouter(EndpointSelector selector, RouteEndpoint[] candidates) public SelectorRouter(EndpointSelector selector, RouteEndpoint[] candidates)
@ -85,6 +86,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
_selector = selector; _selector = selector;
_candidates = candidates; _candidates = candidates;
_values = new RouteValueDictionary[_candidates.Length];
_scores = new int[_candidates.Length]; _scores = new int[_candidates.Length];
} }
@ -100,7 +102,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
// This is needed due to a quirk of our tests - they reuse the endpoint feature. // This is needed due to a quirk of our tests - they reuse the endpoint feature.
context.Endpoint = null; context.Endpoint = null;
await _selector.SelectAsync(routeContext.HttpContext, context, new CandidateSet(_candidates, _scores)); await _selector.SelectAsync(routeContext.HttpContext, context, new CandidateSet(_candidates, _values, _scores));
if (context.Endpoint != null) if (context.Endpoint != null)
{ {
routeContext.Handler = (_) => Task.CompletedTask; routeContext.Handler = (_) => Task.CompletedTask;