Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using BenchmarkDotNet.Attributes;
using Microsoft.AspNetCore.Http;

namespace Microsoft.AspNetCore.Routing.Matching;

public class HttpMethodMatcherPolicyBenchmark
{
private static string[] TestHttpMethods = ["*", HttpMethods.Get, HttpMethods.Connect, HttpMethods.Delete, HttpMethods.Head, HttpMethods.Options, HttpMethods.Patch, HttpMethods.Put, HttpMethods.Post, HttpMethods.Trace, "MERGE"];
private HttpMethodMatcherPolicy _jumpTableBuilder = new();
private List<PolicyJumpTableEdge> _edges = new();

[Params(3, 5, 11)]
public int DestinationCount { get; set; }

[GlobalSetup]
public void Setup()
{
for (int i = 0; i < DestinationCount; i++)
{
_edges.Add(new PolicyJumpTableEdge(new HttpMethodMatcherPolicy.EdgeKey(TestHttpMethods[i], false), i + 1));
}
}

[Benchmark]
public PolicyJumpTable BuildJumpTable()
{
return _jumpTableBuilder.BuildJumpTable(1, _edges);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,44 @@ public class HttpMethodPolicyJumpTableBenchmark
private PolicyJumpTable _dictionaryJumptable;
private PolicyJumpTable _singleEntryJumptable;
private DefaultHttpContext _httpContext;
private Dictionary<string, int> _destinations = new();

[Params("GET", "POST", "Merge")]
public string TestHttpMethod { get; set; }

[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);
_destinations.Add("MERGE", 10);
var lookup = CreateLookup(_destinations);

_dictionaryJumptable = new HttpMethodDictionaryPolicyJumpTable(lookup, corsPreflightDestinations: null);
_singleEntryJumptable = new HttpMethodSingleEntryPolicyJumpTable(0, HttpMethods.Get, -1, supportsCorsPreflight: false, -1, 2);
_httpContext = new DefaultHttpContext();
_httpContext.Request.Method = HttpMethods.Get;
_httpContext.Request.Method = TestHttpMethod;
}

private static HttpMethodDestinationsLookup CreateLookup(Dictionary<string, int> extra)
{
var destinations = new List<KeyValuePair<string, int>>
{
KeyValuePair.Create(HttpMethods.Connect, 1),
KeyValuePair.Create(HttpMethods.Delete, 2),
KeyValuePair.Create(HttpMethods.Head, 3),
KeyValuePair.Create(HttpMethods.Get, 4),
KeyValuePair.Create(HttpMethods.Options, 5),
KeyValuePair.Create(HttpMethods.Patch, 6),
KeyValuePair.Create(HttpMethods.Put, 7),
KeyValuePair.Create(HttpMethods.Post, 8),
KeyValuePair.Create(HttpMethods.Trace, 9)
};

foreach (var item in extra)
{
destinations.Add(item);
}

return new HttpMethodDestinationsLookup(destinations, exitDestination: 0);
}

[Benchmark]
Expand Down
6 changes: 4 additions & 2 deletions src/Http/Routing/perf/Microbenchmarks/readme.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
Compile the solution in Release mode (so binaries are available in release)

Set environment variables with `. .\activate.ps1` or `activate.sh` command, which can be found at the root of the repository.

To run a specific benchmark add it as parameter.
```
dotnet run -c Release --framework <tfm> <benchmark_name>
dotnet run -c Release --framework <tfm> --filter <benchmark_name>
```

To run all benchmarks use '*' as the name.
Expand All @@ -13,4 +15,4 @@ dotnet run -c Release --framework <tfm> *
If you run without any parameters, you'll be offered the list of all benchmarks and get to choose.
```
dotnet run -c Release --framework <tfm>
```
```
131 changes: 131 additions & 0 deletions src/Http/Routing/src/Matching/HttpMethodDestinationsLookup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Http;

namespace Microsoft.AspNetCore.Routing.Matching;

internal sealed class HttpMethodDestinationsLookup
{
private readonly int _exitDestination;

private readonly int _connectDestination;
private readonly int _deleteDestination;
private readonly int _getDestination;
private readonly int _headDestination;
private readonly int _optionsDestination;
private readonly int _patchDestination;
private readonly int _postDestination;
private readonly int _putDestination;
private readonly int _traceDestination;
private readonly Dictionary<string, int>? _extraDestinations;

public HttpMethodDestinationsLookup(List<KeyValuePair<string, int>> destinations, int exitDestination)
{
_exitDestination = exitDestination;

int? connectDestination = null;
int? deleteDestination = null;
int? getDestination = null;
int? headDestination = null;
int? optionsDestination = null;
int? patchDestination = null;
int? postDestination = null;
int? putDestination = null;
int? traceDestination = null;

foreach (var (method, destination) in destinations)
{
if (method.Length >= 3) // 3 == smallest known method
{
switch (method[0] | 0x20)
{
case 'c' when method.Equals(HttpMethods.Connect, StringComparison.OrdinalIgnoreCase):
connectDestination = destination;
continue;
case 'd' when method.Equals(HttpMethods.Delete, StringComparison.OrdinalIgnoreCase):
deleteDestination = destination;
continue;
case 'g' when method.Equals(HttpMethods.Get, StringComparison.OrdinalIgnoreCase):
getDestination = destination;
continue;
case 'h' when method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase):
headDestination = destination;
continue;
case 'o' when method.Equals(HttpMethods.Options, StringComparison.OrdinalIgnoreCase):
optionsDestination = destination;
continue;
case 'p':
if (method.Equals(HttpMethods.Put, StringComparison.OrdinalIgnoreCase))
{
putDestination = destination;
continue;
}
else if (method.Equals(HttpMethods.Post, StringComparison.OrdinalIgnoreCase))
{
postDestination = destination;
continue;
}
else if (method.Equals(HttpMethods.Patch, StringComparison.OrdinalIgnoreCase))
{
patchDestination = destination;
continue;
}
break;
case 't' when method.Equals(HttpMethods.Trace, StringComparison.OrdinalIgnoreCase):
traceDestination = destination;
continue;
}
}

_extraDestinations ??= new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
_extraDestinations.Add(method, destination);
}

_connectDestination = connectDestination ?? _exitDestination;
_deleteDestination = deleteDestination ?? _exitDestination;
_getDestination = getDestination ?? _exitDestination;
_headDestination = headDestination ?? _exitDestination;
_optionsDestination = optionsDestination ?? _exitDestination;
_patchDestination = patchDestination ?? _exitDestination;
_postDestination = postDestination ?? _exitDestination;
_putDestination = putDestination ?? _exitDestination;
_traceDestination = traceDestination ?? _exitDestination;
}

public int GetDestination(string method)
{
// Implementation skeleton taken from https://github.com/dotnet/runtime/blob/13b43155c31beb844b1b04766fea65235ccd8363/src/libraries/System.Net.Http/src/System/Net/Http/HttpMethod.cs#L179
if (method.Length >= 3) // 3 == smallest known method
{
(var matchedMethod, var destination) = (method[0] | 0x20) switch
{
'c' => (HttpMethods.Connect, _connectDestination),
'd' => (HttpMethods.Delete, _deleteDestination),
'g' => (HttpMethods.Get, _getDestination),
'h' => (HttpMethods.Head, _headDestination),
'o' => (HttpMethods.Options, _optionsDestination),
'p' => method.Length switch
{
3 => (HttpMethods.Put, _putDestination),
4 => (HttpMethods.Post, _postDestination),
_ => (HttpMethods.Patch, _patchDestination),
},
't' => (HttpMethods.Trace, _traceDestination),
_ => (null, 0),
};

if (matchedMethod is not null && method.Equals(matchedMethod, StringComparison.OrdinalIgnoreCase))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if we can speed-up the method.Equals(matchedMethod, StringComparison.OrdinalIgnoreCase) since we know we are targeting a well-known method. Maybe we can leverage the fact that it's very likely ASCII on the header to perform two memory comparisons and or the results for the individual characters?

Copy link
Contributor Author

@ladeak ladeak Nov 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean to compare with upper-case and lower-case, OR the results and check if all values set to 1? I actually wonder if there is something like this in System.Text.Ascii already that I could use straight away. And I assume we cannot expect it as ASCII, so if the match fails, fall back to regular string comparison, right?

  • I will implement it, measure it and come back to you with the results.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, there is potential to speed this up:

  • One option is to generate a trie to compare the strings. That's probably overkill.
  • A simpler optimization would be to have a fast path that assumes upper case method (which basically everyone uses) and compares against hardcoded chars using SequenceEqual. If it doesn't match then fallback to string.Equals + OrdinalIgnoreCase.

However, this PR has a nice improvement and sets up good infrastructure. Merge now and iterate in the future.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have the two versions of the method in a buffer, (upper and lowercase), compare against it and or the results. If done with simd it's about 4 ops.

Disclaimer that I haven't done it myself, but I know in other places we use things like SWAR (Simd within a register) so it's likely useful to apply it here too, rely on SIMD instructions directly or use any specialized helper.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am happy to come explore @javiercn , I have written some vectorized code in the past, let me see if I can do something "simple". But Ascii type also uses vectorization.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have the two versions of the method in a buffer, (upper and lowercase), compare against it and or the results. If done with simd it's about 4 ops.

It would be a bit more complicated because mix case is valid, e.g. Post.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JamesNK and @javiercn I promised to come back to this thread. TLDR; none of the above suggestions were performing better to what is already merged.

Using ASCII

Relevant code:

if (matchedMethod is not null && (System.Text.Ascii.EqualsIgnoreCase(matchedMethod, method) || method.Equals(matchedMethod, StringComparison.OrdinalIgnoreCase)))
{
    return destination;
}

ASCII Results

|                     Method | TestHttpMethod |      Mean |     Error |    StdDev |    Median |          Op/s | Gen 0 | Gen 1 | Gen 2 | Allocated |
|--------------------------- |--------------- |----------:|----------:|----------:|----------:|--------------:|------:|------:|------:|----------:|
|  DictionaryPolicyJumpTable |            GET |  6.182 ns | 0.0793 ns | 0.0703 ns |  6.147 ns | 161,768,263.8 |     - |     - |     - |         - |
| SingleEntryPolicyJumpTable |            GET |  2.721 ns | 0.0557 ns | 0.1344 ns |  2.674 ns | 367,483,825.2 |     - |     - |     - |         - |
|  DictionaryPolicyJumpTable |          Merge | 12.403 ns | 0.1286 ns | 0.1140 ns | 12.396 ns |  80,624,929.0 |     - |     - |     - |         - |
| SingleEntryPolicyJumpTable |          Merge |  3.363 ns | 0.0221 ns | 0.0196 ns |  3.363 ns | 297,385,268.2 |     - |     - |     - |         - |
|  DictionaryPolicyJumpTable |           POST |  6.330 ns | 0.0457 ns | 0.0405 ns |  6.336 ns | 157,968,543.3 |     - |     - |     - |         - |
| SingleEntryPolicyJumpTable |           POST |  3.442 ns | 0.0702 ns | 0.0721 ns |  3.444 ns | 290,548,813.4 |     - |     - |     - |         - |

Using SequenceEquals Ordinal comparison

Relevant code:

if (matchedMethod is not null && (method.AsSpan().SequenceEqual(matchedMethod)
        || method.Equals(matchedMethod, StringComparison.OrdinalIgnoreCase)))
{
    return destination;
}

SequenceEqual Results

|                     Method | TestHttpMethod |      Mean |     Error |    StdDev |          Op/s | Gen 0 | Gen 1 | Gen 2 | Allocated |
|--------------------------- |--------------- |----------:|----------:|----------:|--------------:|------:|------:|------:|----------:|
|  DictionaryPolicyJumpTable |            GET |  5.325 ns | 0.0951 ns | 0.0794 ns | 187,810,373.8 |     - |     - |     - |         - |
| SingleEntryPolicyJumpTable |            GET |  3.136 ns | 0.0436 ns | 0.0387 ns | 318,917,169.1 |     - |     - |     - |         - |
|  DictionaryPolicyJumpTable |          Merge | 12.382 ns | 0.1478 ns | 0.1383 ns |  80,761,722.6 |     - |     - |     - |         - |
| SingleEntryPolicyJumpTable |          Merge |  3.360 ns | 0.0353 ns | 0.0330 ns | 297,646,602.7 |     - |     - |     - |         - |
|  DictionaryPolicyJumpTable |           POST |  4.894 ns | 0.0540 ns | 0.0505 ns | 204,328,749.4 |     - |     - |     - |         - |
| SingleEntryPolicyJumpTable |           POST |  3.343 ns | 0.0078 ns | 0.0065 ns | 299,173,275.5 |     - |     - |     - |         - |

Vector

A slightly different implementation I take more optimistic approach that only compares with upper-case vectorized, and if no match falls back to regular approach. The problem with vectors is that they are a bit more difficult to create when the size of the vector is larger than the input.

So what I ended up doing is creating vectorized versions of the known HTTP verbs and a vector mask corresponding to their length.

       // Set values to vConnect, vDelete, vGet etc. fields in ctor

        vConnect = Vector256.LoadUnsafe<ushort>(ref Unsafe.As<char, ushort>(ref MemoryMarshal.GetReference(HttpMethods.Connect.AsSpan()))) & vMask7;
        vDelete = Vector256.LoadUnsafe<ushort>(ref Unsafe.As<char, ushort>(ref MemoryMarshal.GetReference(HttpMethods.Delete.AsSpan()))) & vMask6;
        vGet = Vector256.LoadUnsafe<ushort>(ref Unsafe.As<char, ushort>(ref MemoryMarshal.GetReference(HttpMethods.Get.AsSpan()))) & vMask3;
        // ...
        vMask3 = new Vector<ushort>(new ushort[16] { ushort.MaxValue, ushort.MaxValue, ushort.MaxValue, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }).AsVector256();
        vMask4 = new Vector<ushort>(new ushort[16] { ushort.MaxValue, ushort.MaxValue, ushort.MaxValue, ushort.MaxValue, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }).AsVector256();
         //...
    }

    public int GetDestination(string method)
    {
        if (Vector256.IsHardwareAccelerated)
        {
            (var vMatched, var vMask, var expectedLength, var destination) = (method[0]) switch
            {
                'C' => (vConnect, vMask7, 7, _connectDestination),
                'D' => (vDelete, vMask6, 6, _deleteDestination),
                'G' => (vGet, vMask3, 3, _getDestination),
                'H' => (vHead, vMask4, 4, _headDestination),
                'O' => (vOptions, vMask7, 7, _optionsDestination),
                'P' => method.Length switch
                {
                    3 => (vPut, vMask3, 3, _putDestination),
                    4 => (vPost, vMask4, 4, _postDestination),
                    _ => (vPatch, vMask5, 5, _patchDestination),
                },
                'T' => (vTrace, vMask5, 5, _traceDestination),
                _ => (Vector256<ushort>.Zero, Vector256<ushort>.Zero, 0, 0),
            };

            if (expectedLength == method.Length)
            {
                ref var source = ref Unsafe.As<char, ushort>(ref MemoryMarshal.GetReference(method.AsSpan()));
                var vSource = Vector256.LoadUnsafe<ushort>(ref source);
                vSource = vSource & vMask;

                if (Vector256.Equals(vSource, vMatched) == Vector256<ushort>.AllBitsSet)
                {
                    return destination;
                }
            }
        }

       // else fallback to non-vectorized approach

}

I was considering to implement the lower-case comparison too but given the performance tests indicate this solution is already slower to the string.Equals even when the input is upper cased, extending it won't get any faster.

Vectorized Results:

|                     Method | TestHttpMethod |      Mean |     Error |    StdDev |    Median |          Op/s | Gen 0 | Gen 1 | Gen 2 | Allocated |
|--------------------------- |--------------- |----------:|----------:|----------:|----------:|--------------:|------:|------:|------:|----------:|
|  DictionaryPolicyJumpTable |            GET |  4.669 ns | 0.0417 ns | 0.0369 ns |  4.669 ns | 214,196,679.8 |     - |     - |     - |         - |
| SingleEntryPolicyJumpTable |            GET |  3.119 ns | 0.0193 ns | 0.0181 ns |  3.116 ns | 320,565,879.3 |     - |     - |     - |         - |
|  DictionaryPolicyJumpTable |          Merge | 13.416 ns | 0.1905 ns | 0.1688 ns | 13.446 ns |  74,538,092.7 |     - |     - |     - |         - |
| SingleEntryPolicyJumpTable |          Merge |  3.375 ns | 0.0281 ns | 0.0249 ns |  3.367 ns | 296,253,388.9 |     - |     - |     - |         - |
|  DictionaryPolicyJumpTable |           POST |  4.382 ns | 0.0920 ns | 0.1682 ns |  4.292 ns | 228,209,040.8 |     - |     - |     - |         - |
| SingleEntryPolicyJumpTable |           POST |  3.364 ns | 0.0283 ns | 0.0237 ns |  3.374 ns | 297,300,993.8 |     - |     - |     - |         - |

{
return destination;
}
}

if (_extraDestinations != null && _extraDestinations.TryGetValue(method, out var extraDestination))
{
return extraDestination;
}

return _exitDestination;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,40 +7,25 @@ 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;
private readonly HttpMethodDestinationsLookup _httpMethodDestinations;
private readonly HttpMethodDestinationsLookup? _corsHttpMethodDestinations;

public HttpMethodDictionaryPolicyJumpTable(
int exitDestination,
Dictionary<string, int>? destinations,
int corsPreflightExitDestination,
Dictionary<string, int>? corsPreflightDestinations)
HttpMethodDestinationsLookup destinations,
HttpMethodDestinationsLookup? corsPreflightDestinations)
{
_exitDestination = exitDestination;
_destinations = destinations;
_corsPreflightExitDestination = corsPreflightExitDestination;
_corsPreflightDestinations = corsPreflightDestinations;

_supportsCorsPreflight = _corsPreflightDestinations != null && _corsPreflightDestinations.Count > 0;
_httpMethodDestinations = destinations;
_corsHttpMethodDestinations = corsPreflightDestinations;
}

public override int GetDestination(HttpContext httpContext)
{
int destination;

var httpMethod = httpContext.Request.Method;
if (_supportsCorsPreflight && HttpMethodMatcherPolicy.IsCorsPreflightRequest(httpContext, httpMethod, out var accessControlRequestMethod))
if (_corsHttpMethodDestinations != null && HttpMethodMatcherPolicy.IsCorsPreflightRequest(httpContext, httpMethod, out var accessControlRequestMethod))
{
return _corsPreflightDestinations!.TryGetValue(accessControlRequestMethod.ToString(), out destination)
? destination
: _corsPreflightExitDestination;
var corsHttpMethod = accessControlRequestMethod.ToString();
return _corsHttpMethodDestinations.GetDestination(corsHttpMethod);
}

return _destinations != null &&
_destinations.TryGetValue(httpMethod, out destination) ? destination : _exitDestination;
return _httpMethodDestinations.GetDestination(httpMethod);
}
}
62 changes: 27 additions & 35 deletions src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -310,52 +310,31 @@ public IReadOnlyList<PolicyNodeEdge> GetEdges(IReadOnlyList<Endpoint> endpoints)
/// <returns></returns>
public PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList<PolicyJumpTableEdge> edges)
{
Dictionary<string, int>? destinations = null;
Dictionary<string, int>? corsPreflightDestinations = null;
List<KeyValuePair<string, int>>? destinations = null;
List<KeyValuePair<string, int>>? corsPreflightDestinations = null;
var corsPreflightExitDestination = exitDestination;

for (var i = 0; i < edges.Count; i++)
{
// We create this data, so it's safe to cast it.
var key = (EdgeKey)edges[i].State;
var destination = edges[i].Destination;

if (key.IsCorsPreflightRequest)
{
if (corsPreflightDestinations == null)
{
corsPreflightDestinations = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
}

corsPreflightDestinations.Add(key.HttpMethod, edges[i].Destination);
ProcessEdge(key.HttpMethod, destination, ref corsPreflightExitDestination, ref corsPreflightDestinations);
}
else
{
if (destinations == null)
{
destinations = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
}

destinations.Add(key.HttpMethod, edges[i].Destination);
ProcessEdge(key.HttpMethod, destination, ref exitDestination, ref destinations);
}
}

int corsPreflightExitDestination = exitDestination;
if (corsPreflightDestinations != null && corsPreflightDestinations.TryGetValue(AnyMethod, out var matchesAnyVerb))
{
// If we have endpoints that match any HTTP method, use that as the exit.
corsPreflightExitDestination = matchesAnyVerb;
corsPreflightDestinations.Remove(AnyMethod);
}

if (destinations != null && destinations.TryGetValue(AnyMethod, out matchesAnyVerb))
{
// If we have endpoints that match any HTTP method, use that as the exit.
exitDestination = matchesAnyVerb;
destinations.Remove(AnyMethod);
}

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 httpMethodDestination = destinations[0];
var method = httpMethodDestination.Key;
var destination = httpMethodDestination.Value;
var supportsCorsPreflight = false;
Expand All @@ -364,7 +343,7 @@ public PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList<PolicyJ
if (corsPreflightDestinations?.Count > 0)
{
supportsCorsPreflight = true;
corsPreflightDestination = corsPreflightDestinations.Single().Value;
corsPreflightDestination = corsPreflightDestinations[0].Value;
}

return new HttpMethodSingleEntryPolicyJumpTable(
Expand All @@ -378,10 +357,23 @@ public PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList<PolicyJ
else
{
return new HttpMethodDictionaryPolicyJumpTable(
exitDestination,
destinations,
corsPreflightExitDestination,
corsPreflightDestinations);
new HttpMethodDestinationsLookup(destinations ?? new(), exitDestination),
corsPreflightDestinations != null ? new HttpMethodDestinationsLookup(corsPreflightDestinations, corsPreflightExitDestination) : null);
}

static void ProcessEdge(string httpMethod, int destination, ref int exitDestination, ref List<KeyValuePair<string, int>>? destinations)
{
// If we have endpoints that match any HTTP method, use that as the exit.
if (string.Equals(httpMethod, AnyMethod, StringComparison.OrdinalIgnoreCase))
{
exitDestination = destination;
}
else
{

destinations ??= new();
destinations.Add(KeyValuePair.Create(httpMethod, destination));
}
}
}

Expand Down
Loading