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:
Ryan Nowak 2019-04-14 19:33:08 -07:00 committed by Ryan Nowak
parent e47fbbab9e
commit 4f015e2813
5 changed files with 524 additions and 74 deletions

View File

@ -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;

View File

@ -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>();

View File

@ -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);
}
}
}

View File

@ -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.

View File

@ -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;
}
}
}