diff --git a/src/Microsoft.AspNetCore.Routing/Matching/CandidateSet.cs b/src/Microsoft.AspNetCore.Routing/Matching/CandidateSet.cs
index 6b2e66ab56..0f38c8690d 100644
--- a/src/Microsoft.AspNetCore.Routing/Matching/CandidateSet.cs
+++ b/src/Microsoft.AspNetCore.Routing/Matching/CandidateSet.cs
@@ -2,6 +2,8 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
+using System.Collections;
+using System.Collections.Specialized;
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Http;
@@ -14,6 +16,11 @@ namespace Microsoft.AspNetCore.Routing.Matching
///
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
// cases. There's no real reason why 4 is important, it just seemed like
// a plausible number.
@@ -26,8 +33,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
///
///
- /// Initializes a new instances of the candidate set structure with the provided list of endpoints
- /// and associated scores.
+ /// Initializes a new instances of the candidate set structure with the provided data.
///
///
/// The constructor is provided to enable unit tests of implementations of
@@ -35,8 +41,9 @@ namespace Microsoft.AspNetCore.Routing.Matching
///
///
/// The list of endpoints, sorted in descending priority order.
+ /// The list of instances.
/// The list of endpoint scores. .
- public CandidateSet(Endpoint[] endpoints, int[] scores)
+ public CandidateSet(Endpoint[] endpoints, RouteValueDictionary[] values, int[] scores)
{
Count = endpoints.Length;
@@ -80,6 +87,17 @@ namespace Microsoft.AspNetCore.Routing.Matching
}
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)
@@ -126,25 +144,35 @@ namespace Microsoft.AspNetCore.Routing.Matching
}
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);
+ }
}
///
/// Gets the count of candidates in the set.
///
public int Count { get; }
-
+
///
/// Gets the associated with the candidate
/// at .
///
/// The candidate index.
///
- /// A reference to the . The result is returned by reference
- /// and intended to be mutated.
+ /// A reference to the . The result is returned by reference.
///
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.
// PERF: Force inlining
@@ -177,6 +205,56 @@ namespace Microsoft.AspNetCore.Routing.Matching
}
}
+ ///
+ /// Gets or sets a value which indicates where the is considered
+ /// a valid candiate for the current request. Set this value to false to exclude an
+ /// from consideration.
+ ///
+ 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];
+ }
+ }
+
+ ///
+ /// Sets the validitity of the candidate at the provided index.
+ ///
+ /// The candidate index.
+ ///
+ /// The value to set. If true the candidate is considered valid for the current request.
+ ///
+ 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()
{
throw new ArgumentOutOfRangeException("index");
diff --git a/src/Microsoft.AspNetCore.Routing/Matching/CandidateState.cs b/src/Microsoft.AspNetCore.Routing/Matching/CandidateState.cs
index 8cc0c1319e..03bccd517b 100644
--- a/src/Microsoft.AspNetCore.Routing/Matching/CandidateState.cs
+++ b/src/Microsoft.AspNetCore.Routing/Matching/CandidateState.cs
@@ -14,11 +14,16 @@ namespace Microsoft.AspNetCore.Routing.Matching
{
Endpoint = endpoint;
Score = score;
-
- IsValidCandidate = true;
Values = null;
}
+ internal CandidateState(Endpoint endpoint, RouteValueDictionary values, int score)
+ {
+ Endpoint = endpoint;
+ Values = values;
+ Score = score;
+ }
+
///
/// Gets the .
///
@@ -41,17 +46,10 @@ namespace Microsoft.AspNetCore.Routing.Matching
///
public int Score { get; }
- ///
- /// Gets or sets a value which indicates where the is considered
- /// a valid candiate for the current request. Set this value to false to exclude an
- /// from consideration.
- ///
- public bool IsValidCandidate { get; set; }
-
///
/// Gets or sets the associated with the
/// and the current request.
///
- public RouteValueDictionary Values { get; set; }
+ public RouteValueDictionary Values { get; internal set; }
}
}
diff --git a/src/Microsoft.AspNetCore.Routing/Matching/DefaultEndpointSelector.cs b/src/Microsoft.AspNetCore.Routing/Matching/DefaultEndpointSelector.cs
index 0b3daa97b1..613b94624f 100644
--- a/src/Microsoft.AspNetCore.Routing/Matching/DefaultEndpointSelector.cs
+++ b/src/Microsoft.AspNetCore.Routing/Matching/DefaultEndpointSelector.cs
@@ -53,10 +53,10 @@ namespace Microsoft.AspNetCore.Routing.Matching
for (var i = 0; i < candidateSet.Count; i++)
{
ref var state = ref candidateSet[i];
-
- var isValid = state.IsValidCandidate;
+ var isValid = candidateSet.IsValidCandidate(i);
if (isValid && foundScore == null)
{
+
// This is the first match we've seen - speculatively assign it.
endpoint = state.Endpoint;
values = state.Values;
@@ -98,10 +98,9 @@ namespace Microsoft.AspNetCore.Routing.Matching
var matches = new List();
for (var i = 0; i < candidates.Count; i++)
{
- ref var state = ref candidates[i];
- if (state.IsValidCandidate)
+ if (candidates.IsValidCandidate(i))
{
- matches.Add(state.Endpoint);
+ matches.Add(candidates[i].Endpoint);
}
}
diff --git a/src/Microsoft.AspNetCore.Routing/Matching/DfaMatcher.cs b/src/Microsoft.AspNetCore.Routing/Matching/DfaMatcher.cs
index fbdf96687c..212acaf1b5 100644
--- a/src/Microsoft.AspNetCore.Routing/Matching/DfaMatcher.cs
+++ b/src/Microsoft.AspNetCore.Routing/Matching/DfaMatcher.cs
@@ -5,7 +5,6 @@ using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Routing.Patterns;
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.
// Complex segments go through an old API that requires a fully-materialized
// route value dictionary.
- var isMatch = true;
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)
{
- 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);
diff --git a/src/Microsoft.AspNetCore.Routing/Matching/IEndpointSelectorPolicy.cs b/src/Microsoft.AspNetCore.Routing/Matching/IEndpointSelectorPolicy.cs
index 6a3675ba20..f1488bc98d 100644
--- a/src/Microsoft.AspNetCore.Routing/Matching/IEndpointSelectorPolicy.cs
+++ b/src/Microsoft.AspNetCore.Routing/Matching/IEndpointSelectorPolicy.cs
@@ -28,7 +28,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
///
/// Implementations of should implement this method
/// and filter the set of candidates in the by setting
- /// to false where desired.
+ /// to false where desired.
///
///
/// To signal an error condition, set to an
diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matching/CandidateSetTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matching/CandidateSetTest.cs
index b0f3eacd91..a80e4dd119 100644
--- a/test/Microsoft.AspNetCore.Routing.Tests/Matching/CandidateSetTest.cs
+++ b/test/Microsoft.AspNetCore.Routing.Tests/Matching/CandidateSetTest.cs
@@ -22,6 +22,9 @@ namespace Microsoft.AspNetCore.Routing.Matching
[InlineData(4)]
[InlineData(5)] // this is the break-point where we start to use a list.
[InlineData(6)]
+ [InlineData(31)]
+ [InlineData(32)] // this is the break point where we use a BitArray
+ [InlineData(33)]
public void Create_CreatesCandidateSet(int count)
{
// Arrange
@@ -41,10 +44,13 @@ namespace Microsoft.AspNetCore.Routing.Matching
for (var i = 0; i < candidateSet.Count; i++)
{
ref var state = ref candidateSet[i];
- Assert.True(state.IsValidCandidate);
+ Assert.True(candidateSet.IsValidCandidate(i));
Assert.Same(endpoints[i], state.Endpoint);
Assert.Equal(candidates[i].Score, state.Score);
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(5)] // this is the break-point where we start to use a list.
[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)
{
// Arrange
@@ -68,16 +77,19 @@ namespace Microsoft.AspNetCore.Routing.Matching
}
// 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
for (var i = 0; i < candidateSet.Count; i++)
{
ref var state = ref candidateSet[i];
- Assert.True(state.IsValidCandidate);
+ Assert.True(candidateSet.IsValidCandidate(i));
Assert.Same(endpoints[i], state.Endpoint);
Assert.Equal(i, state.Score);
Assert.Null(state.Values);
+
+ candidateSet.SetValidity(i, false);
+ Assert.False(candidateSet.IsValidCandidate(i));
}
}
diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matching/DefaultEndpointSelectorTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matching/DefaultEndpointSelectorTest.cs
index 8d41296010..ae67bf53fb 100644
--- a/test/Microsoft.AspNetCore.Routing.Tests/Matching/DefaultEndpointSelectorTest.cs
+++ b/test/Microsoft.AspNetCore.Routing.Tests/Matching/DefaultEndpointSelectorTest.cs
@@ -40,7 +40,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
var candidateSet = CreateCandidateSet(endpoints, scores);
candidateSet[0].Values = new RouteValueDictionary();
- candidateSet[0].IsValidCandidate = false;
+ candidateSet.SetValidity(0, false);
var (httpContext, context) = CreateContext();
var selector = CreateSelector();
@@ -61,7 +61,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
var candidateSet = CreateCandidateSet(endpoints, scores);
candidateSet[0].Values = new RouteValueDictionary();
- candidateSet[0].IsValidCandidate = true;
+ candidateSet.SetValidity(0, true);
var (httpContext, context) = CreateContext();
var selector = CreateSelector();
@@ -81,8 +81,8 @@ namespace Microsoft.AspNetCore.Routing.Matching
var scores = new int[] { 0, 0 };
var candidateSet = CreateCandidateSet(endpoints, scores);
- candidateSet[0].IsValidCandidate = false;
- candidateSet[1].IsValidCandidate = true;
+ candidateSet.SetValidity(0, false);
+ candidateSet.SetValidity(1, true);
var (httpContext, context) = CreateContext();
var selector = CreateSelector();
@@ -102,9 +102,9 @@ namespace Microsoft.AspNetCore.Routing.Matching
var scores = new int[] { 0, 0, 1 };
var candidateSet = CreateCandidateSet(endpoints, scores);
- candidateSet[0].IsValidCandidate = false;
- candidateSet[1].IsValidCandidate = true;
- candidateSet[2].IsValidCandidate = true;
+ candidateSet.SetValidity(0, false);
+ candidateSet.SetValidity(1, true);
+ candidateSet.SetValidity(2, true);
var (httpContext, context) = CreateContext();
var selector = CreateSelector();
@@ -131,11 +131,11 @@ namespace Microsoft.AspNetCore.Routing.Matching
var scores = new int[] { 0, 1, 2, 3, 4 };
var candidateSet = CreateCandidateSet(endpoints, scores);
- candidateSet[0].IsValidCandidate = false;
- candidateSet[1].IsValidCandidate = false;
- candidateSet[2].IsValidCandidate = false;
- candidateSet[3].IsValidCandidate = false;
- candidateSet[4].IsValidCandidate = true;
+ candidateSet.SetValidity(0, false);
+ candidateSet.SetValidity(1, false);
+ candidateSet.SetValidity(2, false);
+ candidateSet.SetValidity(3, false);
+ candidateSet.SetValidity(4, true);
var (httpContext, context) = CreateContext();
var selector = CreateSelector();
@@ -155,9 +155,9 @@ namespace Microsoft.AspNetCore.Routing.Matching
var scores = new int[] { 0, 1, 1 };
var candidateSet = CreateCandidateSet(endpoints, scores);
- candidateSet[0].IsValidCandidate = false;
- candidateSet[1].IsValidCandidate = true;
- candidateSet[2].IsValidCandidate = true;
+ candidateSet.SetValidity(0, false);
+ candidateSet.SetValidity(1, true);
+ candidateSet.SetValidity(2, true);
var (httpContext, context) = CreateContext();
var selector = CreateSelector();
@@ -188,13 +188,13 @@ test: /test3", ex.Message);
.Setup(p => p.ApplyAsync(It.IsAny(), It.IsAny(), It.IsAny()))
.Returns((c, f, cs) =>
{
- cs[1].IsValidCandidate = false;
+ cs.SetValidity(1, false);
return Task.CompletedTask;
});
- candidateSet[0].IsValidCandidate = false;
- candidateSet[1].IsValidCandidate = true;
- candidateSet[2].IsValidCandidate = true;
+ candidateSet.SetValidity(0, false);
+ candidateSet.SetValidity(1, true);
+ candidateSet.SetValidity(2, true);
var (httpContext, context) = CreateContext();
var selector = CreateSelector(policy.Object);
@@ -234,9 +234,9 @@ test: /test3", ex.Message);
.Setup(p => p.ApplyAsync(It.IsAny(), It.IsAny(), It.IsAny()))
.Throws(new InvalidOperationException());
- candidateSet[0].IsValidCandidate = false;
- candidateSet[1].IsValidCandidate = true;
- candidateSet[2].IsValidCandidate = true;
+ candidateSet.SetValidity(0, false);
+ candidateSet.SetValidity(1, true);
+ candidateSet.SetValidity(2, true);
var (httpContext, context) = CreateContext();
var selector = CreateSelector(policy1.Object, policy2.Object);
@@ -270,7 +270,7 @@ test: /test3", ex.Message);
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)
diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matching/DfaMatcherTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matching/DfaMatcherTest.cs
index a0b28ec8bf..924a11b733 100644
--- a/test/Microsoft.AspNetCore.Routing.Tests/Matching/DfaMatcherTest.cs
+++ b/test/Microsoft.AspNetCore.Routing.Tests/Matching/DfaMatcherTest.cs
@@ -126,12 +126,12 @@ namespace Microsoft.AspNetCore.Routing.Matching
Assert.Equal(2, cs.Count);
Assert.Same(endpoint1, cs[0].Endpoint);
- Assert.True(cs[0].IsValidCandidate);
+ Assert.True(cs.IsValidCandidate(0));
Assert.Equal(0, cs[0].Score);
Assert.Empty(cs[0].Values);
Assert.Same(endpoint2, cs[1].Endpoint);
- Assert.True(cs[1].IsValidCandidate);
+ Assert.True(cs.IsValidCandidate(1));
Assert.Equal(1, cs[1].Score);
Assert.Empty(cs[1].Values);
diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matching/RouteMatcherBuilder.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matching/RouteMatcherBuilder.cs
index 97906a2c26..a47aafac5b 100644
--- a/test/Microsoft.AspNetCore.Routing.Tests/Matching/RouteMatcherBuilder.cs
+++ b/test/Microsoft.AspNetCore.Routing.Tests/Matching/RouteMatcherBuilder.cs
@@ -76,6 +76,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
{
private readonly EndpointSelector _selector;
private readonly RouteEndpoint[] _candidates;
+ private readonly RouteValueDictionary[] _values;
private readonly int[] _scores;
public SelectorRouter(EndpointSelector selector, RouteEndpoint[] candidates)
@@ -83,6 +84,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
_selector = selector;
_candidates = candidates;
+ _values = new RouteValueDictionary[_candidates.Length];
_scores = new int[_candidates.Length];
}
@@ -99,7 +101,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
// across requests.
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)
{
routeContext.Handler = (_) => Task.CompletedTask;
diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matching/TreeRouterMatcherBuilder.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matching/TreeRouterMatcherBuilder.cs
index dd1e28a825..0924e3bfd8 100644
--- a/test/Microsoft.AspNetCore.Routing.Tests/Matching/TreeRouterMatcherBuilder.cs
+++ b/test/Microsoft.AspNetCore.Routing.Tests/Matching/TreeRouterMatcherBuilder.cs
@@ -78,6 +78,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
{
private readonly EndpointSelector _selector;
private readonly RouteEndpoint[] _candidates;
+ private readonly RouteValueDictionary[] _values;
private readonly int[] _scores;
public SelectorRouter(EndpointSelector selector, RouteEndpoint[] candidates)
@@ -85,6 +86,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
_selector = selector;
_candidates = candidates;
+ _values = new RouteValueDictionary[_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.
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)
{
routeContext.Handler = (_) => Task.CompletedTask;