Merge branch 'merge/release/2.2-to-master'

This commit is contained in:
James Newton-King 2018-08-24 09:52:47 +12:00
commit dd1dcaebee
No known key found for this signature in database
GPG Key ID: 0A66B2F456BF5526
11 changed files with 324 additions and 180 deletions

View File

@ -3,13 +3,14 @@
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Patterns;
namespace Microsoft.AspNetCore.Routing.Matching
{
internal readonly struct Candidate
{
public readonly RouteEndpoint Endpoint;
public readonly Endpoint Endpoint;
// Used to optimize out operations that modify route values.
public readonly CandidateFlags Flags;
@ -48,7 +49,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
public readonly int Score;
// Used in tests.
public Candidate(RouteEndpoint endpoint)
public Candidate(Endpoint endpoint)
{
Endpoint = endpoint;
@ -63,7 +64,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
}
public Candidate(
RouteEndpoint endpoint,
Endpoint endpoint,
int score,
KeyValuePair<string, object>[] slots,
(string parameterName, int segmentIndex, int slotIndex)[] captures,

View File

@ -36,7 +36,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
/// </summary>
/// <param name="endpoints">The list of endpoints, sorted in descending priority order.</param>
/// <param name="scores">The list of endpoint scores. <see cref="CandidateState.Score"/>.</param>
public CandidateSet(RouteEndpoint[] endpoints, int[] scores)
public CandidateSet(Endpoint[] endpoints, int[] scores)
{
Count = endpoints.Length;

View File

@ -1,6 +1,8 @@
// 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 Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing.Matching
{
/// <summary>
@ -8,7 +10,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
/// </summary>
public struct CandidateState
{
internal CandidateState(RouteEndpoint endpoint, int score)
internal CandidateState(Endpoint endpoint, int score)
{
Endpoint = endpoint;
Score = score;
@ -20,7 +22,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
/// <summary>
/// Gets the <see cref="Http.Endpoint"/>.
/// </summary>
public RouteEndpoint Endpoint { get; }
public Endpoint Endpoint { get; }
/// <summary>
/// Gets the score of the <see cref="Http.Endpoint"/> within the current

View File

@ -48,7 +48,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
EndpointFeature feature,
CandidateSet candidateSet)
{
RouteEndpoint endpoint = null;
Endpoint endpoint = null;
RouteValueDictionary values = null;
int? foundScore = null;
for (var i = 0; i < candidateSet.Count; i++)
@ -96,7 +96,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
{
// If we get here it's the result of an ambiguity - we're OK with this
// being a littler slower and more allocatey.
var matches = new List<RouteEndpoint>();
var matches = new List<Endpoint>();
for (var i = 0; i < candidates.Count; i++)
{
ref var state = ref candidates[i];

View File

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Patterns;
namespace Microsoft.AspNetCore.Routing.Matching
@ -16,7 +17,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
private readonly EndpointSelector _selector;
private readonly MatcherPolicy[] _policies;
private readonly INodeBuilderPolicy[] _nodeBuilders;
private readonly RouteEndpointComparer _comparer;
private readonly EndpointComparer _comparer;
public DfaMatcherBuilder(
ParameterPolicyFactory parameterPolicyFactory,
@ -29,7 +30,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
// Taking care to use _policies, which has been sorted.
_nodeBuilders = _policies.OfType<INodeBuilderPolicy>().ToArray();
_comparer = new RouteEndpointComparer(_policies.OfType<IEndpointComparerPolicy>().ToArray());
_comparer = new EndpointComparer(_policies.OfType<IEndpointComparerPolicy>().ToArray());
}
public override void AddEndpoint(RouteEndpoint endpoint)
@ -337,7 +338,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
// Builds an array of candidates for a node, assigns a 'score' for each
// endpoint.
internal Candidate[] CreateCandidates(IReadOnlyList<RouteEndpoint> endpoints)
internal Candidate[] CreateCandidates(IReadOnlyList<Endpoint> endpoints)
{
if (endpoints.Count == 0)
{
@ -367,76 +368,80 @@ namespace Microsoft.AspNetCore.Routing.Matching
}
// internal for tests
internal Candidate CreateCandidate(RouteEndpoint endpoint, int score)
internal Candidate CreateCandidate(Endpoint endpoint, int score)
{
var assignments = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
var slots = new List<KeyValuePair<string, object>>();
var captures = new List<(string parameterName, int segmentIndex, int slotIndex)>();
(string parameterName, int segmentIndex, int slotIndex) catchAll = default;
foreach (var kvp in endpoint.RoutePattern.Defaults)
{
assignments.Add(kvp.Key, assignments.Count);
slots.Add(kvp);
}
for (var i = 0; i < endpoint.RoutePattern.PathSegments.Count; i++)
{
var segment = endpoint.RoutePattern.PathSegments[i];
if (!segment.IsSimple)
{
continue;
}
var parameterPart = segment.Parts[0] as RoutePatternParameterPart;
if (parameterPart == null)
{
continue;
}
if (!assignments.TryGetValue(parameterPart.Name, out var slotIndex))
{
slotIndex = assignments.Count;
assignments.Add(parameterPart.Name, slotIndex);
var hasDefaultValue = parameterPart.Default != null || parameterPart.IsCatchAll;
slots.Add(hasDefaultValue ? new KeyValuePair<string, object>(parameterPart.Name, parameterPart.Default) : default);
}
if (parameterPart.IsCatchAll)
{
catchAll = (parameterPart.Name, i, slotIndex);
}
else
{
captures.Add((parameterPart.Name, i, slotIndex));
}
}
var complexSegments = new List<(RoutePatternPathSegment pathSegment, int segmentIndex)>();
for (var i = 0; i < endpoint.RoutePattern.PathSegments.Count; i++)
var constraints = new List<KeyValuePair<string, IRouteConstraint>>();
if (endpoint is RouteEndpoint routeEndpoint)
{
var segment = endpoint.RoutePattern.PathSegments[i];
if (segment.IsSimple)
foreach (var kvp in routeEndpoint.RoutePattern.Defaults)
{
continue;
assignments.Add(kvp.Key, assignments.Count);
slots.Add(kvp);
}
complexSegments.Add((segment, i));
}
var constraints = new List<KeyValuePair<string, IRouteConstraint>>();
foreach (var kvp in endpoint.RoutePattern.ParameterPolicies)
{
var parameter = endpoint.RoutePattern.GetParameter(kvp.Key); // may be null, that's ok
var parameterPolicyReferences = kvp.Value;
for (var i = 0; i < parameterPolicyReferences.Count; i++)
for (var i = 0; i < routeEndpoint.RoutePattern.PathSegments.Count; i++)
{
var reference = parameterPolicyReferences[i];
var parameterPolicy = _parameterPolicyFactory.Create(parameter, reference);
if (parameterPolicy is IRouteConstraint routeConstraint)
var segment = routeEndpoint.RoutePattern.PathSegments[i];
if (!segment.IsSimple)
{
constraints.Add(new KeyValuePair<string, IRouteConstraint>(kvp.Key, routeConstraint));
continue;
}
var parameterPart = segment.Parts[0] as RoutePatternParameterPart;
if (parameterPart == null)
{
continue;
}
if (!assignments.TryGetValue(parameterPart.Name, out var slotIndex))
{
slotIndex = assignments.Count;
assignments.Add(parameterPart.Name, slotIndex);
var hasDefaultValue = parameterPart.Default != null || parameterPart.IsCatchAll;
slots.Add(hasDefaultValue ? new KeyValuePair<string, object>(parameterPart.Name, parameterPart.Default) : default);
}
if (parameterPart.IsCatchAll)
{
catchAll = (parameterPart.Name, i, slotIndex);
}
else
{
captures.Add((parameterPart.Name, i, slotIndex));
}
}
for (var i = 0; i < routeEndpoint.RoutePattern.PathSegments.Count; i++)
{
var segment = routeEndpoint.RoutePattern.PathSegments[i];
if (segment.IsSimple)
{
continue;
}
complexSegments.Add((segment, i));
}
foreach (var kvp in routeEndpoint.RoutePattern.ParameterPolicies)
{
var parameter = routeEndpoint.RoutePattern.GetParameter(kvp.Key); // may be null, that's ok
var parameterPolicyReferences = kvp.Value;
for (var i = 0; i < parameterPolicyReferences.Count; i++)
{
var reference = parameterPolicyReferences[i];
var parameterPolicy = _parameterPolicyFactory.Create(parameter, reference);
if (parameterPolicy is IRouteConstraint routeConstraint)
{
constraints.Add(new KeyValuePair<string, IRouteConstraint>(kvp.Key, routeConstraint));
}
}
}
}
@ -549,8 +554,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
Label = parent.Label + " " + edge.State.ToString(),
};
// TODO: https://github.com/aspnet/Routing/issues/648
next.Matches.AddRange(edge.Endpoints.Cast<RouteEndpoint>().ToArray());
next.Matches.AddRange(edge.Endpoints);
nextWork.Add(next);
parent.PolicyEdges.Add(edge.State, next);

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing.Matching
{
@ -16,7 +17,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
public DfaNode()
{
Literals = new Dictionary<string, DfaNode>(StringComparer.OrdinalIgnoreCase);
Matches = new List<RouteEndpoint>();
Matches = new List<Endpoint>();
PolicyEdges = new Dictionary<object, DfaNode>();
}
@ -29,7 +30,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
// Just for diagnostics and debugging
public string Label { get; set; }
public List<RouteEndpoint> Matches { get; }
public List<Endpoint> Matches { get; }
public Dictionary<string, DfaNode> Literals { get; }

View File

@ -0,0 +1,163 @@
// 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.Diagnostics;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing.Matching
{
// Use to sort and group Endpoints. RouteEndpoints are sorted before other implementations.
//
// NOTE:
// When ordering endpoints, we compare the route templates as an absolute last resort.
// This is used as a factor to ensure that we always have a predictable ordering
// for tests, errors, etc.
//
// When we group endpoints we don't consider the route template, because we're trying
// to group endpoints not separate them.
//
// TLDR:
// IComparer implementation considers the template string as a tiebreaker.
// IEqualityComparer implementation does not.
// This is cool and good.
internal class EndpointComparer : IComparer<Endpoint>, IEqualityComparer<Endpoint>
{
private readonly IComparer<Endpoint>[] _comparers;
public EndpointComparer(IEndpointComparerPolicy[] policies)
{
// Order, Precedence, (others)...
_comparers = new IComparer<Endpoint>[2 + policies.Length];
_comparers[0] = OrderComparer.Instance;
_comparers[1] = PrecedenceComparer.Instance;
for (var i = 0; i < policies.Length; i++)
{
_comparers[i + 2] = policies[i].Comparer;
}
}
public int Compare(Endpoint x, Endpoint y)
{
// We don't expose this publicly, and we should never call it on
// a null endpoint.
Debug.Assert(x != null);
Debug.Assert(y != null);
var compare = CompareCore(x, y);
// Since we're sorting, use the route template as a last resort.
return compare == 0 ? ComparePattern(x, y) : compare;
}
private int ComparePattern(Endpoint x, Endpoint y)
{
// A RouteEndpoint always comes before a non-RouteEndpoint, regardless of its RawText value
var routeEndpointX = x as RouteEndpoint;
var routeEndpointY = y as RouteEndpoint;
if (routeEndpointX != null)
{
if (routeEndpointY != null)
{
return routeEndpointX.RoutePattern.RawText.CompareTo(routeEndpointY.RoutePattern.RawText);
}
return 1;
}
else if (routeEndpointY != null)
{
return -1;
}
return 0;
}
public bool Equals(Endpoint x, Endpoint y)
{
// We don't expose this publicly, and we should never call it on
// a null endpoint.
Debug.Assert(x != null);
Debug.Assert(y != null);
return CompareCore(x, y) == 0;
}
public int GetHashCode(Endpoint obj)
{
// This should not be possible to call publicly.
Debug.Fail("We don't expect this to be called.");
throw new System.NotImplementedException();
}
private int CompareCore(Endpoint x, Endpoint y)
{
for (var i = 0; i < _comparers.Length; i++)
{
var compare = _comparers[i].Compare(x, y);
if (compare != 0)
{
return compare;
}
}
return 0;
}
private class OrderComparer : IComparer<Endpoint>
{
public static readonly IComparer<Endpoint> Instance = new OrderComparer();
public int Compare(Endpoint x, Endpoint y)
{
var routeEndpointX = x as RouteEndpoint;
var routeEndpointY = y as RouteEndpoint;
if (routeEndpointX != null)
{
if (routeEndpointY != null)
{
return routeEndpointX.Order.CompareTo(routeEndpointY.Order);
}
return 1;
}
else if (routeEndpointY != null)
{
return -1;
}
return 0;
}
}
private class PrecedenceComparer : IComparer<Endpoint>
{
public static readonly IComparer<Endpoint> Instance = new PrecedenceComparer();
public int Compare(Endpoint x, Endpoint y)
{
var routeEndpointX = x as RouteEndpoint;
var routeEndpointY = y as RouteEndpoint;
if (routeEndpointX != null)
{
if (routeEndpointY != null)
{
return routeEndpointX.RoutePattern.InboundPrecedence
.CompareTo(routeEndpointY.RoutePattern.InboundPrecedence);
}
return 1;
}
else if (routeEndpointY != null)
{
return -1;
}
return 0;
}
}
}
}

View File

@ -250,15 +250,13 @@ namespace Microsoft.AspNetCore.Routing.Matching
private Endpoint CreateRejectionEndpoint(IEnumerable<string> httpMethods)
{
var allow = string.Join(", ", httpMethods);
return new RouteEndpoint(
return new Endpoint(
(context) =>
{
context.Response.StatusCode = 405;
context.Response.Headers.Add("Allow", allow);
return Task.CompletedTask;
},
RoutePatternFactory.Parse("/"),
0,
EndpointMetadataCollection.Empty,
Http405EndpointDisplayName);
}

View File

@ -1,103 +0,0 @@
// 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.Collections.Generic;
using System.Diagnostics;
namespace Microsoft.AspNetCore.Routing.Matching
{
// Use to sort and group RouteEndpoints.
//
// NOTE:
// When ordering endpoints, we compare the route templates as an absolute last resort.
// This is used as a factor to ensure that we always have a predictable ordering
// for tests, errors, etc.
//
// When we group endpoints we don't consider the route template, because we're trying
// to group endpoints not separate them.
//
// TLDR:
// IComparer implementation considers the template string as a tiebreaker.
// IEqualityComparer implementation does not.
// This is cool and good.
internal class RouteEndpointComparer : IComparer<RouteEndpoint>, IEqualityComparer<RouteEndpoint>
{
private readonly IComparer<RouteEndpoint>[] _comparers;
public RouteEndpointComparer(IEndpointComparerPolicy[] policies)
{
// Order, Precedence, (others)...
_comparers = new IComparer<RouteEndpoint>[2 + policies.Length];
_comparers[0] = OrderComparer.Instance;
_comparers[1] = PrecedenceComparer.Instance;
for (var i = 0; i < policies.Length; i++)
{
_comparers[i + 2] = policies[i].Comparer;
}
}
public int Compare(RouteEndpoint x, RouteEndpoint y)
{
// We don't expose this publicly, and we should never call it on
// a null endpoint.
Debug.Assert(x != null);
Debug.Assert(y != null);
var compare = CompareCore(x, y);
// Since we're sorting, use the route template as a last resort.
return compare == 0 ? x.RoutePattern.RawText.CompareTo(y.RoutePattern.RawText) : compare;
}
public bool Equals(RouteEndpoint x, RouteEndpoint y)
{
// We don't expose this publicly, and we should never call it on
// a null endpoint.
Debug.Assert(x != null);
Debug.Assert(y != null);
return CompareCore(x, y) == 0;
}
public int GetHashCode(RouteEndpoint obj)
{
// This should not be possible to call publicly.
Debug.Fail("We don't expect this to be called.");
throw new System.NotImplementedException();
}
private int CompareCore(RouteEndpoint x, RouteEndpoint y)
{
for (var i = 0; i < _comparers.Length; i++)
{
var compare = _comparers[i].Compare(x, y);
if (compare != 0)
{
return compare;
}
}
return 0;
}
private class OrderComparer : IComparer<RouteEndpoint>
{
public static readonly IComparer<RouteEndpoint> Instance = new OrderComparer();
public int Compare(RouteEndpoint x, RouteEndpoint y)
{
return x.Order.CompareTo(y.Order);
}
}
private class PrecedenceComparer : IComparer<RouteEndpoint>
{
public static readonly IComparer<RouteEndpoint> Instance = new PrecedenceComparer();
public int Compare(RouteEndpoint x, RouteEndpoint y)
{
return x.RoutePattern.InboundPrecedence.CompareTo(y.RoutePattern.InboundPrecedence);
}
}
}
}

View File

@ -595,6 +595,54 @@ namespace Microsoft.AspNetCore.Routing.Matching
Assert.Empty(test1_1.PolicyEdges);
}
[Fact]
public void BuildDfaTree_WithPolicies_AndBranches_NonRouteEndpoint()
{
// Arrange
var builder = CreateDfaMatcherBuilder(new TestNonRoutePatternMatcherPolicy());
var endpoint1 = CreateEndpoint("/a", metadata: new object[] { new TestMetadata1(0), });
builder.AddEndpoint(endpoint1);
var endpoint2 = CreateEndpoint("/a", metadata: new object[] { new TestMetadata1(1), });
builder.AddEndpoint(endpoint2);
var endpoint3 = CreateEndpoint("/a", metadata: new object[] { new TestMetadata1(1), });
builder.AddEndpoint(endpoint3);
// Act
var root = builder.BuildDfaTree();
// Assert
Assert.Empty(root.Matches);
Assert.Null(root.Parameters);
var next = Assert.Single(root.Literals);
Assert.Equal("a", next.Key);
var a = next.Value;
Assert.Empty(a.Matches);
Assert.IsType<TestNonRoutePatternMatcherPolicy>(a.NodeBuilder);
Assert.Collection(
a.PolicyEdges.OrderBy(e => e.Key),
e => Assert.Equal(0, e.Key),
e => Assert.Equal(1, e.Key),
e => Assert.Equal(int.MaxValue, e.Key));
var test1_0 = a.PolicyEdges[0];
Assert.Equal(new[] { endpoint1, }, test1_0.Matches);
Assert.Null(test1_0.NodeBuilder);
Assert.Empty(test1_0.PolicyEdges);
var test1_1 = a.PolicyEdges[1];
Assert.Equal(new[] { endpoint2, endpoint3, }, test1_1.Matches);
Assert.Null(test1_1.NodeBuilder);
Assert.Empty(test1_1.PolicyEdges);
var nonRouteEndpoint = a.PolicyEdges[int.MaxValue];
Assert.Equal("MaxValueEndpoint", Assert.Single(nonRouteEndpoint.Matches).DisplayName);
}
[Fact]
public void BuildDfaTree_WithPolicies_AndBranches_BothPoliciesSkipped()
{
@ -968,5 +1016,35 @@ namespace Microsoft.AspNetCore.Routing.Matching
.ToArray();
}
}
private class TestNonRoutePatternMatcherPolicy : MatcherPolicy, IEndpointComparerPolicy, INodeBuilderPolicy
{
public override int Order => 100;
public IComparer<Endpoint> Comparer => EndpointMetadataComparer<TestMetadata1>.Default;
public bool AppliesToNode(IReadOnlyList<Endpoint> endpoints)
{
return endpoints.Any(e => e.Metadata.GetMetadata<TestMetadata1>() != null);
}
public PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList<PolicyJumpTableEdge> edges)
{
throw new NotImplementedException();
}
public IReadOnlyList<PolicyNodeEdge> GetEdges(IReadOnlyList<Endpoint> endpoints)
{
var edges = endpoints
.GroupBy(e => e.Metadata.GetMetadata<TestMetadata1>().State)
.Select(g => new PolicyNodeEdge(g.Key, g.ToArray()))
.ToList();
var maxValueEndpoint = new Endpoint(TestConstants.EmptyRequestDelegate, EndpointMetadataCollection.Empty, "MaxValueEndpoint");
edges.Add(new PolicyNodeEdge(int.MaxValue, new[] { maxValueEndpoint }));
return edges;
}
}
}
}

View File

@ -228,9 +228,9 @@ namespace Microsoft.AspNetCore.Routing.Matching
"test: " + template);
}
private static RouteEndpointComparer CreateComparer(params IEndpointComparerPolicy[] policies)
private static EndpointComparer CreateComparer(params IEndpointComparerPolicy[] policies)
{
return new RouteEndpointComparer(policies);
return new EndpointComparer(policies);
}
private class TestMetadata1