Add ability to replace an endpoint with a collection
This is a crucial enabler for dynamic scenarios. A policy can replace an endpoint with a *group* of dynamic endpoints which will be disambiguated by other policies.
This commit is contained in:
parent
e47fbbab9e
commit
4f015e2813
|
|
@ -530,8 +530,9 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
|||
public sealed partial class CandidateSet
|
||||
{
|
||||
public CandidateSet(Microsoft.AspNetCore.Http.Endpoint[] endpoints, Microsoft.AspNetCore.Routing.RouteValueDictionary[] values, int[] scores) { }
|
||||
public int Count { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||
public int Count { get { throw null; } }
|
||||
public ref Microsoft.AspNetCore.Routing.Matching.CandidateState this[int index] { [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]get { throw null; } }
|
||||
public void ExpandEndpoint(int index, System.Collections.Generic.IReadOnlyList<Microsoft.AspNetCore.Http.Endpoint> endpoints, System.Collections.Generic.IComparer<Microsoft.AspNetCore.Http.Endpoint> comparer) { }
|
||||
public bool IsValidCandidate(int index) { throw null; }
|
||||
public void ReplaceEndpoint(int index, Microsoft.AspNetCore.Http.Endpoint endpoint, Microsoft.AspNetCore.Routing.RouteValueDictionary values) { }
|
||||
public void SetValidity(int index, bool value) { }
|
||||
|
|
@ -545,6 +546,11 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
|||
public int Score { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||
public Microsoft.AspNetCore.Routing.RouteValueDictionary Values { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||
}
|
||||
public sealed partial class EndpointMetadataComparer : System.Collections.Generic.IComparer<Microsoft.AspNetCore.Http.Endpoint>
|
||||
{
|
||||
internal EndpointMetadataComparer() { }
|
||||
int System.Collections.Generic.IComparer<Microsoft.AspNetCore.Http.Endpoint>.Compare(Microsoft.AspNetCore.Http.Endpoint x, Microsoft.AspNetCore.Http.Endpoint y) { throw null; }
|
||||
}
|
||||
public abstract partial class EndpointMetadataComparer<TMetadata> : System.Collections.Generic.IComparer<Microsoft.AspNetCore.Http.Endpoint> where TMetadata : class
|
||||
{
|
||||
public static readonly Microsoft.AspNetCore.Routing.Matching.EndpointMetadataComparer<TMetadata> Default;
|
||||
|
|
|
|||
|
|
@ -75,6 +75,11 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
services.TryAddTransient<DfaMatcherBuilder>();
|
||||
services.TryAddSingleton<DfaGraphWriter>();
|
||||
services.TryAddTransient<DataSourceDependentMatcher.Lifetime>();
|
||||
services.TryAddSingleton<EndpointMetadataComparer>(services =>
|
||||
{
|
||||
// This has no public constructor.
|
||||
return new EndpointMetadataComparer(services);
|
||||
});
|
||||
|
||||
// Link generation related services
|
||||
services.TryAddSingleton<LinkGenerator, DefaultLinkGenerator>();
|
||||
|
|
|
|||
|
|
@ -2,8 +2,12 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
|
|
@ -18,7 +22,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
|||
{
|
||||
private const int BitVectorSize = 32;
|
||||
|
||||
private readonly CandidateState[] _candidates;
|
||||
private CandidateState[] _candidates;
|
||||
|
||||
/// <summary>
|
||||
/// <para>
|
||||
|
|
@ -55,8 +59,6 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
|||
throw new ArgumentException($"The provided {nameof(endpoints)}, {nameof(values)}, and {nameof(scores)} must have the same length.");
|
||||
}
|
||||
|
||||
Count = endpoints.Length;
|
||||
|
||||
_candidates = new CandidateState[endpoints.Length];
|
||||
for (var i = 0; i < endpoints.Length; i++)
|
||||
{
|
||||
|
|
@ -66,8 +68,6 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
|||
|
||||
internal CandidateSet(Candidate[] candidates)
|
||||
{
|
||||
Count = candidates.Length;
|
||||
|
||||
_candidates = new CandidateState[candidates.Length];
|
||||
for (var i = 0; i < candidates.Length; i++)
|
||||
{
|
||||
|
|
@ -78,7 +78,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
|||
/// <summary>
|
||||
/// Gets the count of candidates in the set.
|
||||
/// </summary>
|
||||
public int Count { get; }
|
||||
public int Count => _candidates.Length;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="CandidateState"/> associated with the candidate <see cref="Endpoint"/>
|
||||
|
|
@ -153,7 +153,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
|||
/// <param name="index">The candidate index.</param>
|
||||
/// <param name="endpoint">
|
||||
/// The <see cref="Endpoint"/> to replace the original <see cref="Endpoint"/> at
|
||||
/// the <paramref name="index"/>. If <paramref name="endpoint"/> the candidate will be marked
|
||||
/// the <paramref name="index"/>. If <paramref name="endpoint"/> is <c>null</c>. the candidate will be marked
|
||||
/// as invalid.
|
||||
/// </param>
|
||||
/// <param name="values">
|
||||
|
|
@ -167,7 +167,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
|||
{
|
||||
ThrowIndexArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
|
||||
_candidates[index] = new CandidateState(endpoint, values, _candidates[index].Score);
|
||||
|
||||
if (endpoint == null)
|
||||
|
|
@ -176,9 +176,188 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces the <see cref="Endpoint"/> at the provided <paramref name="index"/> with the
|
||||
/// provided <paramref name="endpoints"/>.
|
||||
/// </summary>
|
||||
/// <param name="index">The candidate index.</param>
|
||||
/// <param name="endpoints">
|
||||
/// The list of endpoints <see cref="Endpoint"/> to replace the original <see cref="Endpoint"/> at
|
||||
/// the <paramref name="index"/>. If <paramref name="endpoints"/> is empty, the candidate will be marked
|
||||
/// as invalid.
|
||||
/// </param>
|
||||
/// <param name="comparer">
|
||||
/// The endpoint comparer used to order the endpoints. Can be retrieved from the service provider as
|
||||
/// type <see cref="EndpointMetadataComparer"/>.
|
||||
/// </param>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This method supports replacing a dynamic endpoint with a collection of endpoints, and relying on
|
||||
/// <see cref="IEndpointSelectorPolicy"/> implementations to disambiguate further.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The endpoint being replace should have a unique score value. The score is the combination of route
|
||||
/// patter precedence, order, and policy metadata evaluation. A dynamic endpoint will not function
|
||||
/// correctly if other endpoints exist with the same score.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public void ExpandEndpoint(int index, IReadOnlyList<Endpoint> endpoints, IComparer<Endpoint> comparer)
|
||||
{
|
||||
// Friendliness for inlining
|
||||
if ((uint)index >= Count)
|
||||
{
|
||||
ThrowIndexArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
if (endpoints == null)
|
||||
{
|
||||
ThrowArgumentNullException(nameof(endpoints));
|
||||
}
|
||||
|
||||
if (comparer == null)
|
||||
{
|
||||
ThrowArgumentNullException(nameof(comparer));
|
||||
}
|
||||
|
||||
// First we need to verify that the score of what we're replacing is unique.
|
||||
ValidateUniqueScore(index);
|
||||
|
||||
switch (endpoints.Count)
|
||||
{
|
||||
case 0:
|
||||
ReplaceEndpoint(index, null, null);
|
||||
break;
|
||||
|
||||
case 1:
|
||||
ReplaceEndpoint(index, endpoints[0], _candidates[index].Values);
|
||||
break;
|
||||
|
||||
default:
|
||||
|
||||
var score = GetOriginalScore(index);
|
||||
var values = _candidates[index].Values;
|
||||
|
||||
// Adding candidates requires expanding the array and computing new score values for the new candidates.
|
||||
var original = _candidates;
|
||||
var candidates = new CandidateState[original.Length - 1 + endpoints.Count];
|
||||
_candidates = candidates;
|
||||
|
||||
// Since the new endpoints have an unknown ordering relationship to each other, we need to:
|
||||
// - order them
|
||||
// - assign scores
|
||||
// - offset everything that comes after
|
||||
//
|
||||
// If the inputs look like:
|
||||
//
|
||||
// score 0: A1
|
||||
// score 0: A2
|
||||
// score 1: B
|
||||
// score 2: C <-- being expanded
|
||||
// score 3: D
|
||||
//
|
||||
// Then the result should look like:
|
||||
//
|
||||
// score 0: A1
|
||||
// score 0: A2
|
||||
// score 1: B
|
||||
// score 2: `C1
|
||||
// score 3: `C2
|
||||
// score 4: D
|
||||
|
||||
// Candidates before index can be copied unchanged.
|
||||
for (var i = 0; i < index; i++)
|
||||
{
|
||||
candidates[i] = original[i];
|
||||
}
|
||||
|
||||
var buffer = endpoints.ToArray();
|
||||
Array.Sort<Endpoint>(buffer, comparer);
|
||||
|
||||
// Add the first new endpoint with the current score
|
||||
candidates[index] = new CandidateState(buffer[0], values, score);
|
||||
|
||||
var scoreOffset = 0;
|
||||
for (var i = 1; i < buffer.Length; i++)
|
||||
{
|
||||
var cmp = comparer.Compare(buffer[i - 1], buffer[i]);
|
||||
|
||||
// This should not be possible. This would mean that sorting is wrong.
|
||||
Debug.Assert(cmp <= 0);
|
||||
if (cmp == 0)
|
||||
{
|
||||
// Score is unchanged.
|
||||
}
|
||||
else if (cmp < 0)
|
||||
{
|
||||
// Endpoint is lower priority, higher score.
|
||||
scoreOffset++;
|
||||
}
|
||||
|
||||
_candidates[i + index] = new CandidateState(buffer[i], values, score + scoreOffset);
|
||||
}
|
||||
|
||||
for (var i = index + 1; i < original.Length; i++)
|
||||
{
|
||||
_candidates[i + endpoints.Count - 1] = new CandidateState(original[i].Endpoint, original[i].Values, original[i].Score + scoreOffset);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the *positive* score value. Score is used to track valid/invalid which can cause it to be negative.
|
||||
//
|
||||
// This is the original score and used to determine if there are ambiguities.
|
||||
private int GetOriginalScore(int index)
|
||||
{
|
||||
var score = _candidates[index].Score;
|
||||
return score >= 0 ? score : ~score;
|
||||
}
|
||||
|
||||
private void ValidateUniqueScore(int index)
|
||||
{
|
||||
var score = GetOriginalScore(index);
|
||||
|
||||
var count = 0;
|
||||
var candidates = _candidates;
|
||||
for (var i = 0; i < candidates.Length; i++)
|
||||
{
|
||||
if (GetOriginalScore(i) == score)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Assert(count > 0);
|
||||
if (count > 1)
|
||||
{
|
||||
// Uh-oh. We don't allow duplicates with ExpandEndpoint because that will do unpredictable things.
|
||||
var duplicates = new List<Endpoint>();
|
||||
for (var i = 0; i < candidates.Length; i++)
|
||||
{
|
||||
if (GetOriginalScore(i) == score)
|
||||
{
|
||||
duplicates.Add(candidates[i].Endpoint);
|
||||
}
|
||||
}
|
||||
|
||||
var message =
|
||||
$"Using {nameof(ExpandEndpoint)} requires that the replaced endpoint have a unique priority. " +
|
||||
$"The following endpoints were found with the same priority:" + Environment.NewLine +
|
||||
string.Join(Environment.NewLine, duplicates.Select(e => e.DisplayName));
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ThrowIndexArgumentOutOfRangeException()
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("index");
|
||||
}
|
||||
|
||||
private static void ThrowArgumentNullException(string parameter)
|
||||
{
|
||||
throw new ArgumentNullException(parameter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,79 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matching
|
||||
{
|
||||
/// <summary>
|
||||
/// A comparer that can order <see cref="Endpoint"/> instances based on implementations of
|
||||
/// <see cref="IEndpointComparerPolicy" />. The implementation can be retrieved from the service
|
||||
/// provider and provided to <see cref="CandidateSet.ExpandEndpoint(int, IReadOnlyList{Endpoint}, IComparer{Endpoint})"/>.
|
||||
/// </summary>
|
||||
public sealed class EndpointMetadataComparer : IComparer<Endpoint>
|
||||
{
|
||||
private IServiceProvider _services;
|
||||
private IComparer<Endpoint>[] _comparers;
|
||||
|
||||
// This type is **INTENDED** for use in MatcherPolicy instances yet is also needs the MatcherPolicy instances.
|
||||
// using IServiceProvider to break the cycle.
|
||||
internal EndpointMetadataComparer(IServiceProvider services)
|
||||
{
|
||||
if (services == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(services));
|
||||
}
|
||||
|
||||
_services = services;
|
||||
}
|
||||
|
||||
private IComparer<Endpoint>[] Comparers
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_comparers == null)
|
||||
{
|
||||
_comparers = _services.GetServices<MatcherPolicy>()
|
||||
.OrderBy(p => p.Order)
|
||||
.OfType<IEndpointComparerPolicy>()
|
||||
.Select(p => p.Comparer)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
return _comparers;
|
||||
}
|
||||
}
|
||||
|
||||
int IComparer<Endpoint>.Compare(Endpoint x, Endpoint y)
|
||||
{
|
||||
if (x == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(x));
|
||||
}
|
||||
|
||||
if (y == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(y));
|
||||
}
|
||||
|
||||
var comparers = Comparers;
|
||||
for (var i = 0; i < comparers.Length; i++)
|
||||
{
|
||||
var compare = comparers[i].Compare(x, y);
|
||||
if (compare != 0)
|
||||
{
|
||||
return compare;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A base class for <see cref="IComparer{Endpoint}"/> implementations that use
|
||||
/// a specific type of metadata from <see cref="Endpoint.Metadata"/> for comparison.
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing.Patterns;
|
||||
|
|
@ -13,22 +14,11 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
|||
{
|
||||
public class CandidateSetTest
|
||||
{
|
||||
// We special case low numbers of candidates, so we want to verify that it works correctly for a variety
|
||||
// of input sizes.
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(1)]
|
||||
[InlineData(2)]
|
||||
[InlineData(3)]
|
||||
[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)
|
||||
[Fact]
|
||||
public void Create_CreatesCandidateSet()
|
||||
{
|
||||
// Arrange
|
||||
var count = 10;
|
||||
var endpoints = new RouteEndpoint[count];
|
||||
for (var i = 0; i < endpoints.Length; i++)
|
||||
{
|
||||
|
|
@ -55,22 +45,11 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
|||
}
|
||||
}
|
||||
|
||||
// We special case low numbers of candidates, so we want to verify that it works correctly for a variety
|
||||
// of input sizes.
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(1)]
|
||||
[InlineData(2)]
|
||||
[InlineData(3)]
|
||||
[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 ReplaceEndpoint_WithEndpoint(int count)
|
||||
[Fact]
|
||||
public void ReplaceEndpoint_WithEndpoint()
|
||||
{
|
||||
// Arrange
|
||||
var count = 10;
|
||||
var endpoints = new RouteEndpoint[count];
|
||||
for (var i = 0; i < endpoints.Length; i++)
|
||||
{
|
||||
|
|
@ -99,22 +78,11 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
|||
}
|
||||
}
|
||||
|
||||
// We special case low numbers of candidates, so we want to verify that it works correctly for a variety
|
||||
// of input sizes.
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(1)]
|
||||
[InlineData(2)]
|
||||
[InlineData(3)]
|
||||
[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 ReplaceEndpoint_WithEndpoint_Null(int count)
|
||||
[Fact]
|
||||
public void ReplaceEndpoint_WithEndpoint_Null()
|
||||
{
|
||||
// Arrange
|
||||
var count = 10;
|
||||
var endpoints = new RouteEndpoint[count];
|
||||
for (var i = 0; i < endpoints.Length; i++)
|
||||
{
|
||||
|
|
@ -131,7 +99,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
|||
ref var state = ref candidateSet[i];
|
||||
|
||||
// Act
|
||||
candidateSet.ReplaceEndpoint(i, null, null);
|
||||
candidateSet.ReplaceEndpoint(i, (Endpoint)null, null);
|
||||
|
||||
// Assert
|
||||
Assert.Null(state.Endpoint);
|
||||
|
|
@ -140,22 +108,235 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
|||
}
|
||||
}
|
||||
|
||||
// We special case low numbers of candidates, so we want to verify that it works correctly for a variety
|
||||
// of input sizes.
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(1)]
|
||||
[InlineData(2)]
|
||||
[InlineData(3)]
|
||||
[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)
|
||||
[Fact]
|
||||
public void ExpandEndpoint_EmptyList()
|
||||
{
|
||||
// Arrange
|
||||
var count = 10;
|
||||
var endpoints = new RouteEndpoint[count];
|
||||
for (var i = 0; i < endpoints.Length; i++)
|
||||
{
|
||||
endpoints[i] = CreateEndpoint($"/{i}", order: i);
|
||||
}
|
||||
|
||||
var builder = CreateDfaMatcherBuilder();
|
||||
var candidates = builder.CreateCandidates(endpoints);
|
||||
|
||||
var candidateSet = new CandidateSet(candidates);
|
||||
|
||||
var services = new Mock<IServiceProvider>();
|
||||
services.Setup(s => s.GetService(typeof(IEnumerable<MatcherPolicy>))).Returns(new[] { new TestMetadataMatcherPolicy(), });
|
||||
var comparer = new EndpointMetadataComparer(services.Object);
|
||||
|
||||
// Act
|
||||
candidateSet.ExpandEndpoint(0, Array.Empty<Endpoint>(), comparer);
|
||||
|
||||
// Assert
|
||||
|
||||
Assert.Null(candidateSet[0].Endpoint);
|
||||
Assert.False(candidateSet.IsValidCandidate(0));
|
||||
|
||||
for (var i = 1; i < candidateSet.Count; i++)
|
||||
{
|
||||
ref var state = ref candidateSet[i];
|
||||
|
||||
Assert.Same(endpoints[i], state.Endpoint);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExpandEndpoint_Beginning()
|
||||
{
|
||||
// Arrange
|
||||
var count = 10;
|
||||
var endpoints = new RouteEndpoint[count];
|
||||
for (var i = 0; i < endpoints.Length; i++)
|
||||
{
|
||||
endpoints[i] = CreateEndpoint($"/{i}", order: i);
|
||||
}
|
||||
|
||||
var builder = CreateDfaMatcherBuilder();
|
||||
var candidates = builder.CreateCandidates(endpoints);
|
||||
|
||||
var candidateSet = new CandidateSet(candidates);
|
||||
|
||||
var replacements = new RouteEndpoint[3]
|
||||
{
|
||||
CreateEndpoint($"new /A", metadata: new object[]{ new TestMetadata(), }),
|
||||
CreateEndpoint($"new /B", metadata: new object[]{ }),
|
||||
CreateEndpoint($"new /C", metadata: new object[]{ new TestMetadata(), }),
|
||||
};
|
||||
|
||||
var services = new Mock<IServiceProvider>();
|
||||
services.Setup(s => s.GetService(typeof(IEnumerable<MatcherPolicy>))).Returns(new[] { new TestMetadataMatcherPolicy(), });
|
||||
var comparer = new EndpointMetadataComparer(services.Object);
|
||||
|
||||
candidateSet.SetValidity(0, false); // Has no effect. We always count new stuff as valid by default.
|
||||
|
||||
// Act
|
||||
candidateSet.ExpandEndpoint(0, replacements, comparer);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(12, candidateSet.Count);
|
||||
|
||||
Assert.Same(replacements[0], candidateSet[0].Endpoint);
|
||||
Assert.Equal(0, candidateSet[0].Score);
|
||||
Assert.Same(replacements[2], candidateSet[1].Endpoint);
|
||||
Assert.Equal(0, candidateSet[1].Score);
|
||||
Assert.Same(replacements[1], candidateSet[2].Endpoint);
|
||||
Assert.Equal(1, candidateSet[2].Score);
|
||||
|
||||
for (var i = 3; i < candidateSet.Count; i++)
|
||||
{
|
||||
ref var state = ref candidateSet[i];
|
||||
Assert.Same(endpoints[i - 2], state.Endpoint);
|
||||
Assert.Equal(i - 1, candidateSet[i].Score);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExpandEndpoint_Middle()
|
||||
{
|
||||
// Arrange
|
||||
var count = 10;
|
||||
var endpoints = new RouteEndpoint[count];
|
||||
for (var i = 0; i < endpoints.Length; i++)
|
||||
{
|
||||
endpoints[i] = CreateEndpoint($"/{i}", order: i);
|
||||
}
|
||||
|
||||
var builder = CreateDfaMatcherBuilder();
|
||||
var candidates = builder.CreateCandidates(endpoints);
|
||||
|
||||
var candidateSet = new CandidateSet(candidates);
|
||||
|
||||
var replacements = new RouteEndpoint[3]
|
||||
{
|
||||
CreateEndpoint($"new /A", metadata: new object[]{ new TestMetadata(), }),
|
||||
CreateEndpoint($"new /B", metadata: new object[]{ }),
|
||||
CreateEndpoint($"new /C", metadata: new object[]{ new TestMetadata(), }),
|
||||
};
|
||||
|
||||
var services = new Mock<IServiceProvider>();
|
||||
services.Setup(s => s.GetService(typeof(IEnumerable<MatcherPolicy>))).Returns(new[] { new TestMetadataMatcherPolicy(), });
|
||||
var comparer = new EndpointMetadataComparer(services.Object);
|
||||
|
||||
candidateSet.SetValidity(5, false); // Has no effect. We always count new stuff as valid by default.
|
||||
|
||||
// Act
|
||||
candidateSet.ExpandEndpoint(5, replacements, comparer);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(12, candidateSet.Count);
|
||||
|
||||
for (var i = 0; i < 5; i++)
|
||||
{
|
||||
ref var state = ref candidateSet[i];
|
||||
Assert.Same(endpoints[i], state.Endpoint);
|
||||
Assert.Equal(i, candidateSet[i].Score);
|
||||
}
|
||||
|
||||
Assert.Same(replacements[0], candidateSet[5].Endpoint);
|
||||
Assert.Equal(5, candidateSet[5].Score);
|
||||
Assert.Same(replacements[2], candidateSet[6].Endpoint);
|
||||
Assert.Equal(5, candidateSet[6].Score);
|
||||
Assert.Same(replacements[1], candidateSet[7].Endpoint);
|
||||
Assert.Equal(6, candidateSet[7].Score);
|
||||
|
||||
for (var i = 8; i < candidateSet.Count; i++)
|
||||
{
|
||||
ref var state = ref candidateSet[i];
|
||||
Assert.Same(endpoints[i - 2], state.Endpoint);
|
||||
Assert.Equal(i - 1, candidateSet[i].Score);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExpandEndpoint_End()
|
||||
{
|
||||
// Arrange
|
||||
var count = 10;
|
||||
var endpoints = new RouteEndpoint[count];
|
||||
for (var i = 0; i < endpoints.Length; i++)
|
||||
{
|
||||
endpoints[i] = CreateEndpoint($"/{i}", order: i);
|
||||
}
|
||||
|
||||
var builder = CreateDfaMatcherBuilder();
|
||||
var candidates = builder.CreateCandidates(endpoints);
|
||||
|
||||
var candidateSet = new CandidateSet(candidates);
|
||||
|
||||
var replacements = new RouteEndpoint[3]
|
||||
{
|
||||
CreateEndpoint($"new /A", metadata: new object[]{ new TestMetadata(), }),
|
||||
CreateEndpoint($"new /B", metadata: new object[]{ }),
|
||||
CreateEndpoint($"new /C", metadata: new object[]{ new TestMetadata(), }),
|
||||
};
|
||||
|
||||
var services = new Mock<IServiceProvider>();
|
||||
services.Setup(s => s.GetService(typeof(IEnumerable<MatcherPolicy>))).Returns(new[] { new TestMetadataMatcherPolicy(), });
|
||||
var comparer = new EndpointMetadataComparer(services.Object);
|
||||
|
||||
candidateSet.SetValidity(9, false); // Has no effect. We always count new stuff as valid by default.
|
||||
|
||||
// Act
|
||||
candidateSet.ExpandEndpoint(9, replacements, comparer);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(12, candidateSet.Count);
|
||||
|
||||
for (var i = 0; i < 9; i++)
|
||||
{
|
||||
ref var state = ref candidateSet[i];
|
||||
Assert.Same(endpoints[i], state.Endpoint);
|
||||
Assert.Equal(i, candidateSet[i].Score);
|
||||
}
|
||||
|
||||
Assert.Same(replacements[0], candidateSet[9].Endpoint);
|
||||
Assert.Equal(9, candidateSet[9].Score);
|
||||
Assert.Same(replacements[2], candidateSet[10].Endpoint);
|
||||
Assert.Equal(9, candidateSet[10].Score);
|
||||
Assert.Same(replacements[1], candidateSet[11].Endpoint);
|
||||
Assert.Equal(10, candidateSet[11].Score);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExpandEndpoint_ThrowsForDuplicateScore()
|
||||
{
|
||||
// Arrange
|
||||
var count = 2;
|
||||
var endpoints = new RouteEndpoint[count];
|
||||
for (var i = 0; i < endpoints.Length; i++)
|
||||
{
|
||||
endpoints[i] = CreateEndpoint($"/{i}", order: 0);
|
||||
}
|
||||
|
||||
var builder = CreateDfaMatcherBuilder();
|
||||
var candidates = builder.CreateCandidates(endpoints);
|
||||
|
||||
var candidateSet = new CandidateSet(candidates);
|
||||
|
||||
var services = new Mock<IServiceProvider>();
|
||||
services.Setup(s => s.GetService(typeof(IEnumerable<MatcherPolicy>))).Returns(new[] { new TestMetadataMatcherPolicy(), });
|
||||
var comparer = new EndpointMetadataComparer(services.Object);
|
||||
|
||||
// Act
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => candidateSet.ExpandEndpoint(0, Array.Empty<Endpoint>(), comparer));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(@"
|
||||
Using ExpandEndpoint requires that the replaced endpoint have a unique priority. The following endpoints were found with the same priority:
|
||||
test: /0
|
||||
test: /1"
|
||||
.TrimStart(), ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_CreatesCandidateSet_TestConstructor()
|
||||
{
|
||||
// Arrange
|
||||
var count = 10;
|
||||
var endpoints = new RouteEndpoint[count];
|
||||
for (var i = 0; i < endpoints.Length; i++)
|
||||
{
|
||||
|
|
@ -189,14 +370,16 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
|||
}
|
||||
}
|
||||
|
||||
private RouteEndpoint CreateEndpoint(string template)
|
||||
private RouteEndpoint CreateEndpoint(string template, int order = 0, params object[] metadata)
|
||||
{
|
||||
return new RouteEndpoint(
|
||||
TestConstants.EmptyRequestDelegate,
|
||||
RoutePatternFactory.Parse(template),
|
||||
0,
|
||||
EndpointMetadataCollection.Empty,
|
||||
"test");
|
||||
var builder = new RouteEndpointBuilder(TestConstants.EmptyRequestDelegate, RoutePatternFactory.Parse(template), order);
|
||||
for (var i = 0; i < metadata.Length; i++)
|
||||
{
|
||||
builder.Metadata.Add(metadata[i]);
|
||||
}
|
||||
|
||||
builder.DisplayName = "test: " + template;
|
||||
return (RouteEndpoint)builder.Build();
|
||||
}
|
||||
|
||||
private static DfaMatcherBuilder CreateDfaMatcherBuilder(params MatcherPolicy[] policies)
|
||||
|
|
@ -208,5 +391,15 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
|||
Mock.Of<EndpointSelector>(),
|
||||
policies);
|
||||
}
|
||||
|
||||
private class TestMetadata
|
||||
{
|
||||
}
|
||||
|
||||
private class TestMetadataMatcherPolicy : MatcherPolicy, IEndpointComparerPolicy
|
||||
{
|
||||
public override int Order { get; }
|
||||
public IComparer<Endpoint> Comparer => EndpointMetadataComparer<TestMetadata>.Default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue