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);
|
||||
}
|
||||
|
||||
return new HttpMethodPolicyJumpTable(
|
||||
exitDestination,
|
||||
destinations,
|
||||
corsPreflightExitDestination,
|
||||
corsPreflightDestinations);
|
||||
if (destinations?.Count == 1)
|
||||
{
|
||||
// If there is only a single valid HTTP method then use an optimized jump table.
|
||||
// It avoids unnecessary dictionary lookups with the method name.
|
||||
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)
|
||||
|
|
@ -418,50 +445,15 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
|||
return false;
|
||||
}
|
||||
|
||||
private class HttpMethodPolicyJumpTable : PolicyJumpTable
|
||||
internal static bool IsCorsPreflightRequest(HttpContext httpContext, string httpMethod, out StringValues accessControlRequestMethod)
|
||||
{
|
||||
private readonly int _exitDestination;
|
||||
private readonly Dictionary<string, int>? _destinations;
|
||||
private readonly int _corsPreflightExitDestination;
|
||||
private readonly Dictionary<string, int>? _corsPreflightDestinations;
|
||||
accessControlRequestMethod = default;
|
||||
var headers = httpContext.Request.Headers;
|
||||
|
||||
private readonly bool _supportsCorsPreflight;
|
||||
|
||||
public HttpMethodPolicyJumpTable(
|
||||
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;
|
||||
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;
|
||||
}
|
||||
return HttpMethods.Equals(httpMethod, PreflightHttpMethod) &&
|
||||
headers.ContainsKey(HeaderNames.Origin) &&
|
||||
headers.TryGetValue(HeaderNames.AccessControlRequestMethod, out accessControlRequestMethod) &&
|
||||
!StringValues.IsNullOrEmpty(accessControlRequestMethod);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Match_HttpMethod_CaseInsensitive()
|
||||
[Theory]
|
||||
[InlineData("GeT", "GET")]
|
||||
[InlineData("unKNOWN", "UNKNOWN")]
|
||||
public async Task Match_HttpMethod_CaseInsensitive(string endpointMethod, string requestMethod)
|
||||
{
|
||||
// Arrange
|
||||
var endpoint = CreateEndpoint("/hello", httpMethods: new string[] { "GeT", });
|
||||
var endpoint = CreateEndpoint("/hello", httpMethods: new string[] { endpointMethod, });
|
||||
|
||||
var matcher = CreateMatcher(endpoint);
|
||||
var httpContext = CreateContext("/hello", "GET");
|
||||
var httpContext = CreateContext("/hello", requestMethod);
|
||||
|
||||
// Act
|
||||
await matcher.MatchAsync(httpContext);
|
||||
|
|
@ -100,14 +102,16 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
|||
MatcherAssert.AssertMatch(httpContext, endpoint);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Match_HttpMethod_CaseInsensitive_CORS_Preflight()
|
||||
[Theory]
|
||||
[InlineData("GeT", "GET")]
|
||||
[InlineData("unKNOWN", "UNKNOWN")]
|
||||
public async Task Match_HttpMethod_CaseInsensitive_CORS_Preflight(string endpointMethod, string requestMethod)
|
||||
{
|
||||
// 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 httpContext = CreateContext("/hello", "GET", corsPreflight: true);
|
||||
var httpContext = CreateContext("/hello", requestMethod, corsPreflight: true);
|
||||
|
||||
// Act
|
||||
await matcher.MatchAsync(httpContext);
|
||||
|
|
|
|||
Loading…
Reference in New Issue