Implement IEndpointSelectorPolicy for HttpMethodMatcherPolicy
This commit is contained in:
parent
8df3dc7ae4
commit
eca6a71754
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}'")) + "}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue