Skip to content

Commit edf25b7

Browse files
authored
HTTP method matching: Jump table optimized for a single method (#24953)
1 parent 5297d5f commit edf25b7

File tree

5 files changed

+198
-55
lines changed

5 files changed

+198
-55
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Collections.Generic;
5+
using BenchmarkDotNet.Attributes;
6+
using Microsoft.AspNetCore.Http;
7+
8+
namespace Microsoft.AspNetCore.Routing.Matching
9+
{
10+
public class HttpMethodPolicyJumpTableBenchmark
11+
{
12+
private PolicyJumpTable _dictionaryJumptable;
13+
private PolicyJumpTable _singleEntryJumptable;
14+
private DefaultHttpContext _httpContext;
15+
16+
[GlobalSetup]
17+
public void Setup()
18+
{
19+
_dictionaryJumptable = new HttpMethodDictionaryPolicyJumpTable(
20+
0,
21+
new Dictionary<string, int>
22+
{
23+
[HttpMethods.Get] = 1
24+
},
25+
-1,
26+
new Dictionary<string, int>
27+
{
28+
[HttpMethods.Get] = 2
29+
});
30+
_singleEntryJumptable = new HttpMethodSingleEntryPolicyJumpTable(
31+
0,
32+
HttpMethods.Get,
33+
-1,
34+
supportsCorsPreflight: true,
35+
-1,
36+
2);
37+
38+
_httpContext = new DefaultHttpContext();
39+
_httpContext.Request.Method = HttpMethods.Get;
40+
}
41+
42+
[Benchmark]
43+
public int DictionaryPolicyJumpTable()
44+
{
45+
return _dictionaryJumptable.GetDestination(_httpContext);
46+
}
47+
48+
[Benchmark]
49+
public int SingleEntryPolicyJumpTable()
50+
{
51+
return _singleEntryJumptable.GetDestination(_httpContext);
52+
}
53+
}
54+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Collections.Generic;
5+
using Microsoft.AspNetCore.Http;
6+
7+
namespace Microsoft.AspNetCore.Routing.Matching
8+
{
9+
internal sealed class HttpMethodDictionaryPolicyJumpTable : PolicyJumpTable
10+
{
11+
private readonly int _exitDestination;
12+
private readonly Dictionary<string, int>? _destinations;
13+
private readonly int _corsPreflightExitDestination;
14+
private readonly Dictionary<string, int>? _corsPreflightDestinations;
15+
16+
private readonly bool _supportsCorsPreflight;
17+
18+
public HttpMethodDictionaryPolicyJumpTable(
19+
int exitDestination,
20+
Dictionary<string, int>? destinations,
21+
int corsPreflightExitDestination,
22+
Dictionary<string, int>? corsPreflightDestinations)
23+
{
24+
_exitDestination = exitDestination;
25+
_destinations = destinations;
26+
_corsPreflightExitDestination = corsPreflightExitDestination;
27+
_corsPreflightDestinations = corsPreflightDestinations;
28+
29+
_supportsCorsPreflight = _corsPreflightDestinations != null && _corsPreflightDestinations.Count > 0;
30+
}
31+
32+
public override int GetDestination(HttpContext httpContext)
33+
{
34+
int destination;
35+
36+
var httpMethod = httpContext.Request.Method;
37+
if (_supportsCorsPreflight && HttpMethodMatcherPolicy.IsCorsPreflightRequest(httpContext, httpMethod, out var accessControlRequestMethod))
38+
{
39+
return _corsPreflightDestinations!.TryGetValue(accessControlRequestMethod, out destination)
40+
? destination
41+
: _corsPreflightExitDestination;
42+
}
43+
44+
return _destinations != null &&
45+
_destinations.TryGetValue(httpMethod, out destination) ? destination : _exitDestination;
46+
}
47+
}
48+
}

src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs

Lines changed: 39 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -370,11 +370,38 @@ public PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList<PolicyJ
370370
destinations.Remove(AnyMethod);
371371
}
372372

373-
return new HttpMethodPolicyJumpTable(
374-
exitDestination,
375-
destinations,
376-
corsPreflightExitDestination,
377-
corsPreflightDestinations);
373+
if (destinations?.Count == 1)
374+
{
375+
// If there is only a single valid HTTP method then use an optimized jump table.
376+
// It avoids unnecessary dictionary lookups with the method name.
377+
var httpMethodDestination = destinations.Single();
378+
var method = httpMethodDestination.Key;
379+
var destination = httpMethodDestination.Value;
380+
var supportsCorsPreflight = false;
381+
var corsPreflightDestination = 0;
382+
383+
if (corsPreflightDestinations?.Count > 0)
384+
{
385+
supportsCorsPreflight = true;
386+
corsPreflightDestination = corsPreflightDestinations.Single().Value;
387+
}
388+
389+
return new HttpMethodSingleEntryPolicyJumpTable(
390+
exitDestination,
391+
method,
392+
destination,
393+
supportsCorsPreflight,
394+
corsPreflightExitDestination,
395+
corsPreflightDestination);
396+
}
397+
else
398+
{
399+
return new HttpMethodDictionaryPolicyJumpTable(
400+
exitDestination,
401+
destinations,
402+
corsPreflightExitDestination,
403+
corsPreflightDestinations);
404+
}
378405
}
379406

380407
private Endpoint CreateRejectionEndpoint(IEnumerable<string> httpMethods)
@@ -418,50 +445,15 @@ private static bool ContainsHttpMethod(List<string> httpMethods, string httpMeth
418445
return false;
419446
}
420447

421-
private class HttpMethodPolicyJumpTable : PolicyJumpTable
448+
internal static bool IsCorsPreflightRequest(HttpContext httpContext, string httpMethod, out StringValues accessControlRequestMethod)
422449
{
423-
private readonly int _exitDestination;
424-
private readonly Dictionary<string, int>? _destinations;
425-
private readonly int _corsPreflightExitDestination;
426-
private readonly Dictionary<string, int>? _corsPreflightDestinations;
427-
428-
private readonly bool _supportsCorsPreflight;
429-
430-
public HttpMethodPolicyJumpTable(
431-
int exitDestination,
432-
Dictionary<string, int>? destinations,
433-
int corsPreflightExitDestination,
434-
Dictionary<string, int>? corsPreflightDestinations)
435-
{
436-
_exitDestination = exitDestination;
437-
_destinations = destinations;
438-
_corsPreflightExitDestination = corsPreflightExitDestination;
439-
_corsPreflightDestinations = corsPreflightDestinations;
440-
441-
_supportsCorsPreflight = _corsPreflightDestinations != null && _corsPreflightDestinations.Count > 0;
442-
}
443-
444-
public override int GetDestination(HttpContext httpContext)
445-
{
446-
int destination;
450+
accessControlRequestMethod = default;
451+
var headers = httpContext.Request.Headers;
447452

448-
var httpMethod = httpContext.Request.Method;
449-
var headers = httpContext.Request.Headers;
450-
if (_supportsCorsPreflight &&
451-
HttpMethods.Equals(httpMethod, PreflightHttpMethod) &&
452-
headers.ContainsKey(HeaderNames.Origin) &&
453-
headers.TryGetValue(HeaderNames.AccessControlRequestMethod, out var accessControlRequestMethod) &&
454-
!StringValues.IsNullOrEmpty(accessControlRequestMethod))
455-
{
456-
return _corsPreflightDestinations != null &&
457-
_corsPreflightDestinations.TryGetValue(accessControlRequestMethod, out destination)
458-
? destination
459-
: _corsPreflightExitDestination;
460-
}
461-
462-
return _destinations != null &&
463-
_destinations.TryGetValue(httpMethod, out destination) ? destination : _exitDestination;
464-
}
453+
return HttpMethods.Equals(httpMethod, PreflightHttpMethod) &&
454+
headers.ContainsKey(HeaderNames.Origin) &&
455+
headers.TryGetValue(HeaderNames.AccessControlRequestMethod, out accessControlRequestMethod) &&
456+
!StringValues.IsNullOrEmpty(accessControlRequestMethod);
465457
}
466458

467459
private class HttpMethodMetadataEndpointComparer : EndpointMetadataComparer<IHttpMethodMetadata>
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Microsoft.AspNetCore.Http;
5+
6+
namespace Microsoft.AspNetCore.Routing.Matching
7+
{
8+
internal sealed class HttpMethodSingleEntryPolicyJumpTable : PolicyJumpTable
9+
{
10+
private readonly int _exitDestination;
11+
private readonly string _method;
12+
private readonly int _destination;
13+
private readonly int _corsPreflightExitDestination;
14+
private readonly int _corsPreflightDestination;
15+
16+
private readonly bool _supportsCorsPreflight;
17+
18+
public HttpMethodSingleEntryPolicyJumpTable(
19+
int exitDestination,
20+
string method,
21+
int destination,
22+
bool supportsCorsPreflight,
23+
int corsPreflightExitDestination,
24+
int corsPreflightDestination)
25+
{
26+
_exitDestination = exitDestination;
27+
_method = method;
28+
_destination = destination;
29+
_supportsCorsPreflight = supportsCorsPreflight;
30+
_corsPreflightExitDestination = corsPreflightExitDestination;
31+
_corsPreflightDestination = corsPreflightDestination;
32+
}
33+
34+
public override int GetDestination(HttpContext httpContext)
35+
{
36+
var httpMethod = httpContext.Request.Method;
37+
if (_supportsCorsPreflight && HttpMethodMatcherPolicy.IsCorsPreflightRequest(httpContext, httpMethod, out var accessControlRequestMethod))
38+
{
39+
return HttpMethods.Equals(accessControlRequestMethod, _method) ? _corsPreflightDestination : _corsPreflightExitDestination;
40+
}
41+
42+
return HttpMethods.Equals(httpMethod, _method) ? _destination : _exitDestination;
43+
}
44+
}
45+
}

src/Http/Routing/test/UnitTests/Matching/HttpMethodMatcherPolicyIntegrationTestBase.cs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -84,14 +84,16 @@ public async Task NotMatch_HttpMethod_CORS_Preflight()
8484
Assert.Same(HttpMethodMatcherPolicy.Http405EndpointDisplayName, httpContext.GetEndpoint().DisplayName);
8585
}
8686

87-
[Fact]
88-
public async Task Match_HttpMethod_CaseInsensitive()
87+
[Theory]
88+
[InlineData("GeT", "GET")]
89+
[InlineData("unKNOWN", "UNKNOWN")]
90+
public async Task Match_HttpMethod_CaseInsensitive(string endpointMethod, string requestMethod)
8991
{
9092
// Arrange
91-
var endpoint = CreateEndpoint("/hello", httpMethods: new string[] { "GeT", });
93+
var endpoint = CreateEndpoint("/hello", httpMethods: new string[] { endpointMethod, });
9294

9395
var matcher = CreateMatcher(endpoint);
94-
var httpContext = CreateContext("/hello", "GET");
96+
var httpContext = CreateContext("/hello", requestMethod);
9597

9698
// Act
9799
await matcher.MatchAsync(httpContext);
@@ -100,14 +102,16 @@ public async Task Match_HttpMethod_CaseInsensitive()
100102
MatcherAssert.AssertMatch(httpContext, endpoint);
101103
}
102104

103-
[Fact]
104-
public async Task Match_HttpMethod_CaseInsensitive_CORS_Preflight()
105+
[Theory]
106+
[InlineData("GeT", "GET")]
107+
[InlineData("unKNOWN", "UNKNOWN")]
108+
public async Task Match_HttpMethod_CaseInsensitive_CORS_Preflight(string endpointMethod, string requestMethod)
105109
{
106110
// Arrange
107-
var endpoint = CreateEndpoint("/hello", httpMethods: new string[] { "GeT", }, acceptCorsPreflight: true);
111+
var endpoint = CreateEndpoint("/hello", httpMethods: new string[] { endpointMethod, }, acceptCorsPreflight: true);
108112

109113
var matcher = CreateMatcher(endpoint);
110-
var httpContext = CreateContext("/hello", "GET", corsPreflight: true);
114+
var httpContext = CreateContext("/hello", requestMethod, corsPreflight: true);
111115

112116
// Act
113117
await matcher.MatchAsync(httpContext);

0 commit comments

Comments
 (0)