HTTP method matching: Jump table optimized for a single method (#24953)

This commit is contained in:
James Newton-King 2020-08-19 07:41:48 +12:00 committed by GitHub
parent 5297d5fd39
commit edf25b7817
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 198 additions and 55 deletions

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}

View File

@ -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>

View File

@ -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;
}
}
}

View File

@ -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);