Implement IEndpointSelectorPolicy for HttpMethodMatcherPolicy

This commit is contained in:
Ryan Nowak 2019-04-14 14:55:44 -07:00 committed by Ryan Nowak
parent 8df3dc7ae4
commit eca6a71754
7 changed files with 274 additions and 23 deletions

View File

@ -569,14 +569,16 @@ namespace Microsoft.AspNetCore.Routing.Matching
bool Microsoft.AspNetCore.Routing.Matching.IEndpointSelectorPolicy.AppliesToEndpoints(System.Collections.Generic.IReadOnlyList<Microsoft.AspNetCore.Http.Endpoint> endpoints) { throw null; }
bool Microsoft.AspNetCore.Routing.Matching.INodeBuilderPolicy.AppliesToEndpoints(System.Collections.Generic.IReadOnlyList<Microsoft.AspNetCore.Http.Endpoint> endpoints) { throw null; }
}
public sealed partial class HttpMethodMatcherPolicy : Microsoft.AspNetCore.Routing.MatcherPolicy, Microsoft.AspNetCore.Routing.Matching.IEndpointComparerPolicy, Microsoft.AspNetCore.Routing.Matching.INodeBuilderPolicy
public sealed partial class HttpMethodMatcherPolicy : Microsoft.AspNetCore.Routing.MatcherPolicy, Microsoft.AspNetCore.Routing.Matching.IEndpointComparerPolicy, Microsoft.AspNetCore.Routing.Matching.IEndpointSelectorPolicy, Microsoft.AspNetCore.Routing.Matching.INodeBuilderPolicy
{
public HttpMethodMatcherPolicy() { }
public System.Collections.Generic.IComparer<Microsoft.AspNetCore.Http.Endpoint> Comparer { get { throw null; } }
public override int Order { get { throw null; } }
public bool AppliesToEndpoints(System.Collections.Generic.IReadOnlyList<Microsoft.AspNetCore.Http.Endpoint> endpoints) { throw null; }
public System.Threading.Tasks.Task ApplyAsync(Microsoft.AspNetCore.Http.HttpContext httpContext, Microsoft.AspNetCore.Routing.EndpointSelectorContext context, Microsoft.AspNetCore.Routing.Matching.CandidateSet candidates) { throw null; }
public Microsoft.AspNetCore.Routing.Matching.PolicyJumpTable BuildJumpTable(int exitDestination, System.Collections.Generic.IReadOnlyList<Microsoft.AspNetCore.Routing.Matching.PolicyJumpTableEdge> edges) { throw null; }
public System.Collections.Generic.IReadOnlyList<Microsoft.AspNetCore.Routing.Matching.PolicyNodeEdge> GetEdges(System.Collections.Generic.IReadOnlyList<Microsoft.AspNetCore.Http.Endpoint> endpoints) { throw null; }
bool Microsoft.AspNetCore.Routing.Matching.IEndpointSelectorPolicy.AppliesToEndpoints(System.Collections.Generic.IReadOnlyList<Microsoft.AspNetCore.Http.Endpoint> endpoints) { throw null; }
bool Microsoft.AspNetCore.Routing.Matching.INodeBuilderPolicy.AppliesToEndpoints(System.Collections.Generic.IReadOnlyList<Microsoft.AspNetCore.Http.Endpoint> endpoints) { throw null; }
}
public partial interface IEndpointComparerPolicy
{

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Internal;
@ -14,7 +15,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
/// An <see cref="MatcherPolicy"/> that implements filtering and selection by
/// the HTTP method of a request.
/// </summary>
public sealed class HttpMethodMatcherPolicy : MatcherPolicy, IEndpointComparerPolicy, INodeBuilderPolicy
public sealed class HttpMethodMatcherPolicy : MatcherPolicy, IEndpointComparerPolicy, INodeBuilderPolicy, IEndpointSelectorPolicy
{
// Used in tests
internal static readonly string OriginHeader = "Origin";
@ -39,18 +40,34 @@ namespace Microsoft.AspNetCore.Routing.Matching
/// </summary>
public override int Order => -1000;
/// <summary>
/// For framework use only.
/// </summary>
/// <param name="endpoints"></param>
/// <returns></returns>
public bool AppliesToEndpoints(IReadOnlyList<Endpoint> endpoints)
bool INodeBuilderPolicy.AppliesToEndpoints(IReadOnlyList<Endpoint> endpoints)
{
if (endpoints == null)
{
throw new ArgumentNullException(nameof(endpoints));
}
if (ContainsDynamicEndpoints(endpoints))
{
return false;
}
return AppliesToEndpointsCore(endpoints);
}
bool IEndpointSelectorPolicy.AppliesToEndpoints(IReadOnlyList<Endpoint> endpoints)
{
if (endpoints == null)
{
throw new ArgumentNullException(nameof(endpoints));
}
// When the node contains dynamic endpoints we can't make any assumptions.
return ContainsDynamicEndpoints(endpoints);
}
private bool AppliesToEndpointsCore(IReadOnlyList<Endpoint> endpoints)
{
for (var i = 0; i < endpoints.Count; i++)
{
if (endpoints[i].Metadata.GetMetadata<IHttpMethodMetadata>()?.HttpMethods.Count > 0)
@ -62,6 +79,104 @@ namespace Microsoft.AspNetCore.Routing.Matching
return false;
}
/// <summary>
/// For framework use only.
/// </summary>
/// <param name="httpContext"></param>
/// <param name="context"></param>
/// <param name="candidates"></param>
/// <returns></returns>
public Task ApplyAsync(HttpContext httpContext, EndpointSelectorContext context, CandidateSet candidates)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (candidates == null)
{
throw new ArgumentNullException(nameof(candidates));
}
// Returning a 405 here requires us to return keep track of all 'seen' HTTP methods. We allocate to
// keep track of this beause we either need to keep track of the HTTP methods or keep track of the
// endpoints - both allocate.
//
// Those code only runs in the presence of dynamic endpoints anyway.
//
// We want to return a 405 iff we eliminated ALL of the currently valid endpoints due to HTTP method
// mismatch.
bool? needs405Endpoint = null;
HashSet<string> methods = null;
for (var i = 0; i < candidates.Count; i++)
{
// We do this check first for consistency with how 405 is implemented for the graph version
// of this code. We still want to know if any endpoints in this set require an HTTP method
// even if those endpoints are already invalid.
var metadata = candidates[i].Endpoint.Metadata.GetMetadata<IHttpMethodMetadata>();
if (metadata == null || metadata.HttpMethods.Count == 0)
{
// Can match any method.
needs405Endpoint = false;
continue;
}
// Saw a valid endpoint.
needs405Endpoint = needs405Endpoint ?? true;
if (!candidates.IsValidCandidate(i))
{
continue;
}
var httpMethod = httpContext.Request.Method;
if (metadata.AcceptCorsPreflight &&
string.Equals(httpMethod, PreflightHttpMethod, StringComparison.OrdinalIgnoreCase) &&
httpContext.Request.Headers.ContainsKey(OriginHeader) &&
httpContext.Request.Headers.TryGetValue(AccessControlRequestMethod, out var accessControlRequestMethod) &&
!StringValues.IsNullOrEmpty(accessControlRequestMethod))
{
needs405Endpoint = false; // We don't return a 405 for a CORS preflight request when the endpoints accept CORS preflight.
httpMethod = accessControlRequestMethod;
}
var matched = false;
for (var j = 0; j < metadata.HttpMethods.Count; j++)
{
var candidateMethod = metadata.HttpMethods[j];
if (!string.Equals(httpMethod, candidateMethod, StringComparison.OrdinalIgnoreCase))
{
methods = methods ?? new HashSet<string>(StringComparer.OrdinalIgnoreCase);
methods.Add(candidateMethod);
continue;
}
matched = true;
needs405Endpoint = false;
break;
}
if (!matched)
{
candidates.SetValidity(i, false);
}
}
if (needs405Endpoint == true)
{
// We saw some endpoints coming in, and we eliminated them all.
context.Endpoint = CreateRejectionEndpoint(methods.OrderBy(m => m, StringComparer.OrdinalIgnoreCase));
}
return Task.CompletedTask;
}
/// <summary>
/// For framework use only.
/// </summary>

View File

@ -0,0 +1,11 @@
// 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.
namespace Microsoft.AspNetCore.Routing.Matching
{
// End-to-end tests for the HTTP method matching functionality
public class HttpMethodMatcherPolicyIEndpointSelectorPolicyIntegrationTestBase : HttpMethodMatcherPolicyIntegrationTestBase
{
protected override bool HasDynamicMetadata => true;
}
}

View File

@ -0,0 +1,11 @@
// 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.
namespace Microsoft.AspNetCore.Routing.Matching
{
// End-to-end tests for the HTTP method matching functionality
public class HttpMethodMatcherPolicyINodeBuilderPolicyIntegrationTestBase : HttpMethodMatcherPolicyIntegrationTestBase
{
protected override bool HasDynamicMetadata => false;
}
}

View File

@ -14,8 +14,10 @@ using static Microsoft.AspNetCore.Routing.Matching.HttpMethodMatcherPolicy;
namespace Microsoft.AspNetCore.Routing.Matching
{
// End-to-end tests for the HTTP method matching functionality
public class HttpMethodMatcherPolicyIntegrationTest
public abstract class HttpMethodMatcherPolicyIntegrationTestBase
{
protected abstract bool HasDynamicMetadata { get; }
[Fact]
public async Task Match_HttpMethod()
{
@ -203,7 +205,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
Assert.Equal("DELETE, GET, PUT", httpContext.Response.Headers["Allow"]);
}
[Fact] // When all of the candidates handles specific verbs, use a 405 endpoint
[Fact]
public async Task NotMatch_HttpMethod_CORS_DoesNotReturn405()
{
// Arrange
@ -357,7 +359,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
return (httpContext, context);
}
internal static RouteEndpoint CreateEndpoint(
internal RouteEndpoint CreateEndpoint(
string template,
object defaults = null,
object constraints = null,
@ -371,6 +373,11 @@ namespace Microsoft.AspNetCore.Routing.Matching
metadata.Add(new HttpMethodMetadata(httpMethods ?? Array.Empty<string>(), acceptCorsPreflight));
}
if (HasDynamicMetadata)
{
metadata.Add(new DynamicEndpointMetadata());
}
var displayName = "endpoint: " + template + " " + string.Join(", ", httpMethods ?? new[] { "(any)" });
return new RouteEndpoint(
TestConstants.EmptyRequestDelegate,
@ -385,5 +392,10 @@ namespace Microsoft.AspNetCore.Routing.Matching
var endpoint = CreateEndpoint(template);
return (CreateMatcher(endpoint), endpoint);
}
private class DynamicEndpointMetadata : IDynamicEndpointMetadata
{
public bool IsDynamic => true;
}
}
}

View File

@ -1,4 +1,4 @@
// 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;
@ -14,12 +14,12 @@ namespace Microsoft.AspNetCore.Routing.Matching
public class HttpMethodMatcherPolicyTest
{
[Fact]
public void AppliesToNode_EndpointWithoutMetadata_ReturnsFalse()
public void INodeBuilderPolicy_AppliesToNode_EndpointWithoutMetadata_ReturnsFalse()
{
// Arrange
var endpoints = new[] { CreateEndpoint("/", null), };
var policy = CreatePolicy();
var policy = (INodeBuilderPolicy)CreatePolicy();
// Act
var result = policy.AppliesToEndpoints(endpoints);
@ -29,7 +29,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
}
[Fact]
public void AppliesToNode_EndpointWithoutHttpMethods_ReturnsFalse()
public void INodeBuilderPolicy_AppliesToNode_EndpointWithoutHttpMethods_ReturnsFalse()
{
// Arrange
var endpoints = new[]
@ -37,7 +37,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
CreateEndpoint("/", new HttpMethodMetadata(Array.Empty<string>())),
};
var policy = CreatePolicy();
var policy = (INodeBuilderPolicy)CreatePolicy();
// Act
var result = policy.AppliesToEndpoints(endpoints);
@ -47,7 +47,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
}
[Fact]
public void AppliesToNode_EndpointHasHttpMethods_ReturnsTrue()
public void INodeBuilderPolicy_AppliesToNode_EndpointHasHttpMethods_ReturnsTrue()
{
// Arrange
var endpoints = new[]
@ -56,7 +56,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
CreateEndpoint("/", new HttpMethodMetadata(new[] { "GET", })),
};
var policy = CreatePolicy();
var policy = (INodeBuilderPolicy)CreatePolicy();
// Act
var result = policy.AppliesToEndpoints(endpoints);
@ -65,6 +65,96 @@ namespace Microsoft.AspNetCore.Routing.Matching
Assert.True(result);
}
[Fact]
public void INodeBuilderPolicy_AppliesToNode_EndpointIsDynamic_ReturnsFalse()
{
// Arrange
var endpoints = new[]
{
CreateEndpoint("/", new HttpMethodMetadata(Array.Empty<string>())),
CreateEndpoint("/", new HttpMethodMetadata(new[] { "GET", }), new DynamicEndpointMetadata()),
};
var policy = (INodeBuilderPolicy)CreatePolicy();
// Act
var result = policy.AppliesToEndpoints(endpoints);
// Assert
Assert.False(result);
}
[Fact]
public void IEndpointSelectorPolicy_AppliesToNode_EndpointWithoutMetadata_ReturnsTrue()
{
// Arrange
var endpoints = new[] { CreateEndpoint("/", null, new DynamicEndpointMetadata()), };
var policy = (IEndpointSelectorPolicy)CreatePolicy();
// Act
var result = policy.AppliesToEndpoints(endpoints);
// Assert
Assert.True(result);
}
[Fact]
public void IEndpointSelectorPolicy_AppliesToNode_EndpointWithoutHttpMethods_ReturnsTrue()
{
// Arrange
var endpoints = new[]
{
CreateEndpoint("/", new HttpMethodMetadata(Array.Empty<string>()), new DynamicEndpointMetadata()),
};
var policy = (IEndpointSelectorPolicy)CreatePolicy();
// Act
var result = policy.AppliesToEndpoints(endpoints);
// Assert
Assert.True(result);
}
[Fact]
public void IEndpointSelectorPolicy_AppliesToNode_EndpointHasHttpMethods_ReturnsTrue()
{
// Arrange
var endpoints = new[]
{
CreateEndpoint("/", new HttpMethodMetadata(Array.Empty<string>()), new DynamicEndpointMetadata()),
CreateEndpoint("/", new HttpMethodMetadata(new[] { "GET", })),
};
var policy = (IEndpointSelectorPolicy)CreatePolicy();
// Act
var result = policy.AppliesToEndpoints(endpoints);
// Assert
Assert.True(result);
}
[Fact]
public void IEndpointSelectorPolicy_AppliesToNode_EndpointIsNotDynamic_ReturnsFalse()
{
// Arrange
var endpoints = new[]
{
CreateEndpoint("/", new HttpMethodMetadata(Array.Empty<string>())),
CreateEndpoint("/", new HttpMethodMetadata(new[] { "GET", })),
};
var policy = (IEndpointSelectorPolicy)CreatePolicy();
// Act
var result = policy.AppliesToEndpoints(endpoints);
// Assert
Assert.False(result);
}
[Fact]
public void GetEdges_GroupsByHttpMethod()
{
@ -277,7 +367,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
});
}
private static RouteEndpoint CreateEndpoint(string template, HttpMethodMetadata httpMethodMetadata)
private static RouteEndpoint CreateEndpoint(string template, HttpMethodMetadata httpMethodMetadata, params object[] more)
{
var metadata = new List<object>();
if (httpMethodMetadata != null)
@ -285,6 +375,11 @@ namespace Microsoft.AspNetCore.Routing.Matching
metadata.Add(httpMethodMetadata);
}
if (more != null)
{
metadata.AddRange(more);
}
return new RouteEndpoint(
TestConstants.EmptyRequestDelegate,
RoutePatternFactory.Parse(template),
@ -297,5 +392,10 @@ namespace Microsoft.AspNetCore.Routing.Matching
{
return new HttpMethodMatcherPolicy();
}
private class DynamicEndpointMetadata : IDynamicEndpointMetadata
{
public bool IsDynamic => true;
}
}
}

View File

@ -1,4 +1,4 @@
// 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;
@ -93,7 +93,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
private static string FormatRouteValues(RouteValueDictionary values)
{
return "{" + string.Join(", ", values.Select(kvp => $"{kvp.Key} = '{kvp.Value}'")) + "}";
return values == null ? "{}" : "{" + string.Join(", ", values.Select(kvp => $"{kvp.Key} = '{kvp.Value}'")) + "}";
}
}
}
}