HTTP method matching: Jump table optimized for a single method (#24953)
This commit is contained in:
parent
5297d5fd39
commit
edf25b7817
|
|
@ -0,0 +1,54 @@
|
||||||
|
// 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 BenchmarkDotNet.Attributes;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
|
{
|
||||||
|
public class HttpMethodPolicyJumpTableBenchmark
|
||||||
|
{
|
||||||
|
private PolicyJumpTable _dictionaryJumptable;
|
||||||
|
private PolicyJumpTable _singleEntryJumptable;
|
||||||
|
private DefaultHttpContext _httpContext;
|
||||||
|
|
||||||
|
[GlobalSetup]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
_dictionaryJumptable = new HttpMethodDictionaryPolicyJumpTable(
|
||||||
|
0,
|
||||||
|
new Dictionary<string, int>
|
||||||
|
{
|
||||||
|
[HttpMethods.Get] = 1
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
new Dictionary<string, int>
|
||||||
|
{
|
||||||
|
[HttpMethods.Get] = 2
|
||||||
|
});
|
||||||
|
_singleEntryJumptable = new HttpMethodSingleEntryPolicyJumpTable(
|
||||||
|
0,
|
||||||
|
HttpMethods.Get,
|
||||||
|
-1,
|
||||||
|
supportsCorsPreflight: true,
|
||||||
|
-1,
|
||||||
|
2);
|
||||||
|
|
||||||
|
_httpContext = new DefaultHttpContext();
|
||||||
|
_httpContext.Request.Method = HttpMethods.Get;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
public int DictionaryPolicyJumpTable()
|
||||||
|
{
|
||||||
|
return _dictionaryJumptable.GetDestination(_httpContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
public int SingleEntryPolicyJumpTable()
|
||||||
|
{
|
||||||
|
return _singleEntryJumptable.GetDestination(_httpContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
// 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 Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
|
{
|
||||||
|
internal sealed class HttpMethodDictionaryPolicyJumpTable : PolicyJumpTable
|
||||||
|
{
|
||||||
|
private readonly int _exitDestination;
|
||||||
|
private readonly Dictionary<string, int>? _destinations;
|
||||||
|
private readonly int _corsPreflightExitDestination;
|
||||||
|
private readonly Dictionary<string, int>? _corsPreflightDestinations;
|
||||||
|
|
||||||
|
private readonly bool _supportsCorsPreflight;
|
||||||
|
|
||||||
|
public HttpMethodDictionaryPolicyJumpTable(
|
||||||
|
int exitDestination,
|
||||||
|
Dictionary<string, int>? destinations,
|
||||||
|
int corsPreflightExitDestination,
|
||||||
|
Dictionary<string, int>? corsPreflightDestinations)
|
||||||
|
{
|
||||||
|
_exitDestination = exitDestination;
|
||||||
|
_destinations = destinations;
|
||||||
|
_corsPreflightExitDestination = corsPreflightExitDestination;
|
||||||
|
_corsPreflightDestinations = corsPreflightDestinations;
|
||||||
|
|
||||||
|
_supportsCorsPreflight = _corsPreflightDestinations != null && _corsPreflightDestinations.Count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetDestination(HttpContext httpContext)
|
||||||
|
{
|
||||||
|
int destination;
|
||||||
|
|
||||||
|
var httpMethod = httpContext.Request.Method;
|
||||||
|
if (_supportsCorsPreflight && HttpMethodMatcherPolicy.IsCorsPreflightRequest(httpContext, httpMethod, out var accessControlRequestMethod))
|
||||||
|
{
|
||||||
|
return _corsPreflightDestinations!.TryGetValue(accessControlRequestMethod, out destination)
|
||||||
|
? destination
|
||||||
|
: _corsPreflightExitDestination;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _destinations != null &&
|
||||||
|
_destinations.TryGetValue(httpMethod, out destination) ? destination : _exitDestination;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -370,11 +370,38 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
destinations.Remove(AnyMethod);
|
destinations.Remove(AnyMethod);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new HttpMethodPolicyJumpTable(
|
if (destinations?.Count == 1)
|
||||||
exitDestination,
|
{
|
||||||
destinations,
|
// If there is only a single valid HTTP method then use an optimized jump table.
|
||||||
corsPreflightExitDestination,
|
// It avoids unnecessary dictionary lookups with the method name.
|
||||||
corsPreflightDestinations);
|
var httpMethodDestination = destinations.Single();
|
||||||
|
var method = httpMethodDestination.Key;
|
||||||
|
var destination = httpMethodDestination.Value;
|
||||||
|
var supportsCorsPreflight = false;
|
||||||
|
var corsPreflightDestination = 0;
|
||||||
|
|
||||||
|
if (corsPreflightDestinations?.Count > 0)
|
||||||
|
{
|
||||||
|
supportsCorsPreflight = true;
|
||||||
|
corsPreflightDestination = corsPreflightDestinations.Single().Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new HttpMethodSingleEntryPolicyJumpTable(
|
||||||
|
exitDestination,
|
||||||
|
method,
|
||||||
|
destination,
|
||||||
|
supportsCorsPreflight,
|
||||||
|
corsPreflightExitDestination,
|
||||||
|
corsPreflightDestination);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return new HttpMethodDictionaryPolicyJumpTable(
|
||||||
|
exitDestination,
|
||||||
|
destinations,
|
||||||
|
corsPreflightExitDestination,
|
||||||
|
corsPreflightDestinations);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Endpoint CreateRejectionEndpoint(IEnumerable<string> httpMethods)
|
private Endpoint CreateRejectionEndpoint(IEnumerable<string> httpMethods)
|
||||||
|
|
@ -418,50 +445,15 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class HttpMethodPolicyJumpTable : PolicyJumpTable
|
internal static bool IsCorsPreflightRequest(HttpContext httpContext, string httpMethod, out StringValues accessControlRequestMethod)
|
||||||
{
|
{
|
||||||
private readonly int _exitDestination;
|
accessControlRequestMethod = default;
|
||||||
private readonly Dictionary<string, int>? _destinations;
|
var headers = httpContext.Request.Headers;
|
||||||
private readonly int _corsPreflightExitDestination;
|
|
||||||
private readonly Dictionary<string, int>? _corsPreflightDestinations;
|
|
||||||
|
|
||||||
private readonly bool _supportsCorsPreflight;
|
return HttpMethods.Equals(httpMethod, PreflightHttpMethod) &&
|
||||||
|
headers.ContainsKey(HeaderNames.Origin) &&
|
||||||
public HttpMethodPolicyJumpTable(
|
headers.TryGetValue(HeaderNames.AccessControlRequestMethod, out accessControlRequestMethod) &&
|
||||||
int exitDestination,
|
!StringValues.IsNullOrEmpty(accessControlRequestMethod);
|
||||||
Dictionary<string, int>? destinations,
|
|
||||||
int corsPreflightExitDestination,
|
|
||||||
Dictionary<string, int>? corsPreflightDestinations)
|
|
||||||
{
|
|
||||||
_exitDestination = exitDestination;
|
|
||||||
_destinations = destinations;
|
|
||||||
_corsPreflightExitDestination = corsPreflightExitDestination;
|
|
||||||
_corsPreflightDestinations = corsPreflightDestinations;
|
|
||||||
|
|
||||||
_supportsCorsPreflight = _corsPreflightDestinations != null && _corsPreflightDestinations.Count > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetDestination(HttpContext httpContext)
|
|
||||||
{
|
|
||||||
int destination;
|
|
||||||
|
|
||||||
var httpMethod = httpContext.Request.Method;
|
|
||||||
var headers = httpContext.Request.Headers;
|
|
||||||
if (_supportsCorsPreflight &&
|
|
||||||
HttpMethods.Equals(httpMethod, PreflightHttpMethod) &&
|
|
||||||
headers.ContainsKey(HeaderNames.Origin) &&
|
|
||||||
headers.TryGetValue(HeaderNames.AccessControlRequestMethod, out var accessControlRequestMethod) &&
|
|
||||||
!StringValues.IsNullOrEmpty(accessControlRequestMethod))
|
|
||||||
{
|
|
||||||
return _corsPreflightDestinations != null &&
|
|
||||||
_corsPreflightDestinations.TryGetValue(accessControlRequestMethod, out destination)
|
|
||||||
? destination
|
|
||||||
: _corsPreflightExitDestination;
|
|
||||||
}
|
|
||||||
|
|
||||||
return _destinations != null &&
|
|
||||||
_destinations.TryGetValue(httpMethod, out destination) ? destination : _exitDestination;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class HttpMethodMetadataEndpointComparer : EndpointMetadataComparer<IHttpMethodMetadata>
|
private class HttpMethodMetadataEndpointComparer : EndpointMetadataComparer<IHttpMethodMetadata>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
// 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
|
||||||
|
{
|
||||||
|
internal sealed class HttpMethodSingleEntryPolicyJumpTable : PolicyJumpTable
|
||||||
|
{
|
||||||
|
private readonly int _exitDestination;
|
||||||
|
private readonly string _method;
|
||||||
|
private readonly int _destination;
|
||||||
|
private readonly int _corsPreflightExitDestination;
|
||||||
|
private readonly int _corsPreflightDestination;
|
||||||
|
|
||||||
|
private readonly bool _supportsCorsPreflight;
|
||||||
|
|
||||||
|
public HttpMethodSingleEntryPolicyJumpTable(
|
||||||
|
int exitDestination,
|
||||||
|
string method,
|
||||||
|
int destination,
|
||||||
|
bool supportsCorsPreflight,
|
||||||
|
int corsPreflightExitDestination,
|
||||||
|
int corsPreflightDestination)
|
||||||
|
{
|
||||||
|
_exitDestination = exitDestination;
|
||||||
|
_method = method;
|
||||||
|
_destination = destination;
|
||||||
|
_supportsCorsPreflight = supportsCorsPreflight;
|
||||||
|
_corsPreflightExitDestination = corsPreflightExitDestination;
|
||||||
|
_corsPreflightDestination = corsPreflightDestination;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetDestination(HttpContext httpContext)
|
||||||
|
{
|
||||||
|
var httpMethod = httpContext.Request.Method;
|
||||||
|
if (_supportsCorsPreflight && HttpMethodMatcherPolicy.IsCorsPreflightRequest(httpContext, httpMethod, out var accessControlRequestMethod))
|
||||||
|
{
|
||||||
|
return HttpMethods.Equals(accessControlRequestMethod, _method) ? _corsPreflightDestination : _corsPreflightExitDestination;
|
||||||
|
}
|
||||||
|
|
||||||
|
return HttpMethods.Equals(httpMethod, _method) ? _destination : _exitDestination;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -84,14 +84,16 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
Assert.Same(HttpMethodMatcherPolicy.Http405EndpointDisplayName, httpContext.GetEndpoint().DisplayName);
|
Assert.Same(HttpMethodMatcherPolicy.Http405EndpointDisplayName, httpContext.GetEndpoint().DisplayName);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Theory]
|
||||||
public async Task Match_HttpMethod_CaseInsensitive()
|
[InlineData("GeT", "GET")]
|
||||||
|
[InlineData("unKNOWN", "UNKNOWN")]
|
||||||
|
public async Task Match_HttpMethod_CaseInsensitive(string endpointMethod, string requestMethod)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var endpoint = CreateEndpoint("/hello", httpMethods: new string[] { "GeT", });
|
var endpoint = CreateEndpoint("/hello", httpMethods: new string[] { endpointMethod, });
|
||||||
|
|
||||||
var matcher = CreateMatcher(endpoint);
|
var matcher = CreateMatcher(endpoint);
|
||||||
var httpContext = CreateContext("/hello", "GET");
|
var httpContext = CreateContext("/hello", requestMethod);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await matcher.MatchAsync(httpContext);
|
await matcher.MatchAsync(httpContext);
|
||||||
|
|
@ -100,14 +102,16 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
MatcherAssert.AssertMatch(httpContext, endpoint);
|
MatcherAssert.AssertMatch(httpContext, endpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Theory]
|
||||||
public async Task Match_HttpMethod_CaseInsensitive_CORS_Preflight()
|
[InlineData("GeT", "GET")]
|
||||||
|
[InlineData("unKNOWN", "UNKNOWN")]
|
||||||
|
public async Task Match_HttpMethod_CaseInsensitive_CORS_Preflight(string endpointMethod, string requestMethod)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var endpoint = CreateEndpoint("/hello", httpMethods: new string[] { "GeT", }, acceptCorsPreflight: true);
|
var endpoint = CreateEndpoint("/hello", httpMethods: new string[] { endpointMethod, }, acceptCorsPreflight: true);
|
||||||
|
|
||||||
var matcher = CreateMatcher(endpoint);
|
var matcher = CreateMatcher(endpoint);
|
||||||
var httpContext = CreateContext("/hello", "GET", corsPreflight: true);
|
var httpContext = CreateContext("/hello", requestMethod, corsPreflight: true);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await matcher.MatchAsync(httpContext);
|
await matcher.MatchAsync(httpContext);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue