Flesh out experimental matchers

This change improves this area a bit by consolidating the matcher
implementations between the benchmarks project and the conformance
tests.

Additionally I split the minimal matcher into a really trivial
implementation for the simple tests and a more complex one for the
larger tests. This allows us to keep the plaintext/techempower scenario
in sight while also having a good baseline for the more sophisticated
tests.

Also starting to add tests that verify that matchers behave as expected.

The matchers now successfully execute all of these benchmarks, which
means that they support literals and parameters.

Missing features:
- complex segments
- catchall
- default values
- optional parameters
- constraints
- complex segments with file extensions

This is a good place to iterate a bit more of perf and try to make a
decision about what we want to implement.
This commit is contained in:
Ryan Nowak 2018-06-09 13:38:53 -07:00
parent d3ddc1709a
commit 00e99dbbb2
34 changed files with 5656 additions and 4784 deletions

View File

@ -1,6 +1,7 @@
// 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;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
@ -11,6 +12,9 @@ namespace Microsoft.AspNetCore.Routing.Matchers
{
private const int SampleCount = 100;
private BarebonesMatcher _baseline;
private Matcher _dfa;
private Matcher _instruction;
private Matcher _route;
private Matcher _tree;
@ -28,12 +32,58 @@ namespace Microsoft.AspNetCore.Routing.Matchers
// of the request data.
_samples = SampleRequests(EndpointCount, SampleCount);
_route = SetupMatcher(RouteMatcher.CreateBuilder());
_tree = SetupMatcher(TreeRouterMatcher.CreateBuilder());
_baseline = (BarebonesMatcher)SetupMatcher(new BarebonesMatcherBuilder());
_dfa = SetupMatcher(new DfaMatcherBuilder());
_instruction = SetupMatcher(new InstructionMatcherBuilder());
_route = SetupMatcher(new RouteMatcherBuilder());
_tree = SetupMatcher(new TreeRouterMatcherBuilder());
_feature = new EndpointFeature();
}
[Benchmark(Baseline = true, OperationsPerInvoke = SampleCount)]
public async Task Baseline()
{
var feature = _feature;
for (var i = 0; i < SampleCount; i++)
{
var sample = _samples[i];
var httpContext = _requests[sample];
await _baseline._matchers[sample].MatchAsync(httpContext, feature);
Validate(httpContext, _endpoints[sample], feature.Endpoint);
}
}
[Benchmark(OperationsPerInvoke = SampleCount)]
public async Task Dfa()
{
var feature = _feature;
for (var i = 0; i < SampleCount; i++)
{
var sample = _samples[i];
if (sample == 805)
{
GC.KeepAlive(5);
}
var httpContext = _requests[sample];
await _dfa.MatchAsync(httpContext, feature);
Validate(httpContext, _endpoints[sample], feature.Endpoint);
}
}
[Benchmark(OperationsPerInvoke = SampleCount)]
public async Task Instruction()
{
var feature = _feature;
for (var i = 0; i < SampleCount; i++)
{
var sample = _samples[i];
var httpContext = _requests[sample];
await _instruction.MatchAsync(httpContext, feature);
Validate(httpContext, _endpoints[sample], feature.Endpoint);
}
}
[Benchmark(OperationsPerInvoke = SampleCount)]
public async Task LegacyRoute()
{
@ -42,6 +92,10 @@ namespace Microsoft.AspNetCore.Routing.Matchers
{
var sample = _samples[i];
var httpContext = _requests[sample];
// This is required to make the legacy router implementation work with dispatcher.
httpContext.Features.Set<IEndpointFeature>(feature);
await _route.MatchAsync(httpContext, feature);
Validate(httpContext, _endpoints[sample], feature.Endpoint);
}
@ -55,6 +109,10 @@ namespace Microsoft.AspNetCore.Routing.Matchers
{
var sample = _samples[i];
var httpContext = _requests[sample];
// This is required to make the legacy router implementation work with dispatcher.
httpContext.Features.Set<IEndpointFeature>(feature);
await _tree.MatchAsync(httpContext, feature);
Validate(httpContext, _endpoints[sample], feature.Endpoint);
}

View File

@ -1,353 +0,0 @@
// 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;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Template;
namespace Microsoft.AspNetCore.Routing.Matchers
{
internal class DfaMatcher : Matcher
{
public static MatcherBuilder CreateBuilder() => new Builder();
private readonly State[] _states;
private DfaMatcher(State[] states)
{
_states = states;
}
public override Task MatchAsync(HttpContext httpContext, IEndpointFeature feature)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
if (feature == null)
{
throw new ArgumentNullException(nameof(feature));
}
var states = _states;
var current = 0;
var path = httpContext.Request.Path.Value;
var start = 1; // PathString always has a leading slash
var length = 0;
var end = 0;
while ((end = path.IndexOf('/', start + 1)) >= 0)
{
current = states[current].Transitions.GetDestination(path, start, end - start);
start = end;
}
// residue
length = path.Length - start;
if (length > 0)
{
current = states[current].Transitions.GetDestination(path, start, length);
}
var matches = states[current].Matches;
feature.Endpoint = matches.Length == 0 ? null : matches[0];
return Task.CompletedTask;
}
private struct State
{
public bool IsAccepting;
public Endpoint[] Matches;
public JumpTable Transitions;
}
private abstract class JumpTable
{
public abstract int GetDestination(string text, int start, int length);
}
private class JumpTableBuilder
{
private readonly List<(string text, int destination)> _entries = new List<(string text, int destination)>();
public int Depth { get; set; }
public int Exit { get; set; }
public void AddEntry(string text, int destination)
{
_entries.Add((text, destination));
}
public JumpTable Build()
{
return new SimpleJumpTable(Depth, Exit, _entries.ToArray());
}
}
private class SimpleJumpTable : JumpTable
{
private readonly (string text, int destination)[] _entries;
private readonly int _depth;
private readonly int _exit;
public SimpleJumpTable(int depth, int exit, (string text, int destination)[] entries)
{
_depth = depth;
_exit = exit;
_entries = entries;
}
public override int GetDestination(string text, int start, int length)
{
for (var i = 0; i < _entries.Length; i++)
{
if (length == _entries[i].text.Length &&
string.Compare(
text,
start,
_entries[i].text,
0,
length,
StringComparison.OrdinalIgnoreCase) == 0)
{
return _entries[i].destination;
}
}
return _exit;
}
}
private class Entry
{
public int Order;
public decimal Precedence;
public RouteTemplate Pattern;
public Endpoint Endpoint;
}
private class Node
{
public int Depth { get; set; }
public List<Entry> Matches { get; } = new List<Entry>();
public Dictionary<string, Node> Literals { get; } = new Dictionary<string, Node>();
}
private class Builder : MatcherBuilder
{
private List<Entry> _entries = new List<Entry>();
public override void AddEntry(string pattern, MatcherEndpoint endpoint)
{
var parsed = TemplateParser.Parse(pattern);
_entries.Add(new Entry()
{
Order = 0,
Pattern = parsed,
Precedence = RoutePrecedence.ComputeInbound(parsed),
Endpoint = endpoint,
});
}
public override Matcher Build()
{
Sort(_entries);
var root = new Node() { Depth = -1 };
// Since we overlay parameters onto the literal entries, we do two passes, first we create
// all of the literal nodes, then we 'spread' parameters
for (var i = 0; i < _entries.Count; i++)
{
var entry = _entries[i];
var parent = root;
for (var depth = 0; depth < entry.Pattern.Segments.Count; depth++)
{
var segment = entry.Pattern.Segments[depth];
if (segment.IsSimple && segment.Parts[0].IsLiteral)
{
if (!parent.Literals.TryGetValue(segment.Parts[0].Text, out var next))
{
next = new Node() { Depth = depth, };
parent.Literals.Add(segment.Parts[0].Text, next);
}
parent = next;
}
else if (segment.IsSimple && segment.Parts[0].IsParameter)
{
if (!parent.Literals.TryGetValue("*", out var next))
{
next = new Node() { Depth = depth, };
parent.Literals.Add("*", next);
}
parent = next;
}
}
parent.Matches.Add(entry);
}
for (var i = 0; i < _entries.Count; i++)
{
var entry = _entries[i];
var parents = new List<Node>() { root, };
for (var depth = 0; depth < entry.Pattern.Segments.Count; depth++)
{
var segment = entry.Pattern.Segments[depth];
if (segment.IsSimple && segment.Parts[0].IsLiteral)
{
var next = new List<Node>();
for (var j = 0; j < parents.Count; j++)
{
if (!parents[j].Literals.TryGetValue(segment.Parts[0].Text, out var child))
{
child = new Node() { Depth = depth, };
if (parents[j].Literals.TryGetValue("*", out var parameter))
{
child.Matches.AddRange(parameter.Matches);
foreach (var kvp in parameter.Literals)
{
child.Literals.Add(kvp.Key, DeepCopy(kvp.Value));
}
}
parents[j].Literals.Add(segment.Parts[0].Text, child);
}
next.Add(child);
}
parents = next;
}
else if (segment.IsSimple && segment.Parts[0].IsParameter)
{
var next = new List<Node>();
for (var j = 0; j < parents.Count; j++)
{
next.AddRange(parents[j].Literals.Values);
}
parents = next;
}
}
for (var j = 0; j < parents.Count; j++)
{
if (!parents[j].Matches.Contains(entry))
{
parents[j].Matches.Add(entry);
}
}
}
var states = new List<State>();
var tables = new List<JumpTableBuilder>();
AddNode(root, states, tables);
var exit = states.Count;
states.Add(new State() { IsAccepting = false, Matches = Array.Empty<Endpoint>(), });
tables.Add(new JumpTableBuilder() { Exit = exit, });
for (var i = 0; i < tables.Count; i++)
{
if (tables[i].Exit == -1)
{
tables[i].Exit = exit;
}
}
for (var i = 0; i < states.Count; i++)
{
states[i] = new State()
{
IsAccepting = states[i].IsAccepting,
Matches = states[i].Matches,
Transitions = tables[i].Build(),
};
}
return new DfaMatcher(states.ToArray());
}
private static int AddNode(Node node, List<State> states, List<JumpTableBuilder> tables)
{
Sort(node.Matches);
var index = states.Count;
states.Add(new State() { Matches = node.Matches.Select(e => e.Endpoint).ToArray(), IsAccepting = node.Matches.Count > 0 });
var table = new JumpTableBuilder() { Depth = node.Depth, };
tables.Add(table);
foreach (var kvp in node.Literals)
{
if (kvp.Key == "*")
{
continue;
}
var transition = AddNode(kvp.Value, states, tables);
table.AddEntry(kvp.Key, transition);
}
var exitIndex = -1;
if (node.Literals.TryGetValue("*", out var exit))
{
exitIndex = AddNode(exit, states, tables);
}
table.Exit = exitIndex;
return index;
}
private static void Sort(List<Entry> entries)
{
entries.Sort((x, y) =>
{
var comparison = x.Order.CompareTo(y.Order);
if (comparison != 0)
{
return comparison;
}
comparison = y.Precedence.CompareTo(x.Precedence);
if (comparison != 0)
{
return comparison;
}
return x.Pattern.TemplateText.CompareTo(y.Pattern.TemplateText);
});
}
private static Node DeepCopy(Node node)
{
var copy = new Node() { Depth = node.Depth, };
copy.Matches.AddRange(node.Matches);
foreach (var kvp in node.Literals)
{
copy.Literals.Add(kvp.Key, DeepCopy(kvp.Value));
}
return node;
}
}
}
}

View File

@ -10,6 +10,9 @@ namespace Microsoft.AspNetCore.Routing.Matchers
// Use https://editor2.swagger.io/ to convert from yaml to json-
public partial class GithubMatcherBenchmark : MatcherBenchmarkBase
{
private BarebonesMatcher _baseline;
private Matcher _dfa;
private Matcher _instruction;
private Matcher _route;
private Matcher _tree;
@ -22,12 +25,51 @@ namespace Microsoft.AspNetCore.Routing.Matchers
SetupRequests();
_route = SetupMatcher(RouteMatcher.CreateBuilder());
_tree = SetupMatcher(TreeRouterMatcher.CreateBuilder());
_baseline = (BarebonesMatcher)SetupMatcher(new BarebonesMatcherBuilder());
_dfa = SetupMatcher(new DfaMatcherBuilder());
_instruction = SetupMatcher(new InstructionMatcherBuilder());
_route = SetupMatcher(new RouteMatcherBuilder());
_tree = SetupMatcher(new TreeRouterMatcherBuilder());
_feature = new EndpointFeature();
}
[Benchmark(Baseline = true, OperationsPerInvoke = EndpointCount)]
public async Task Baseline()
{
var feature = _feature;
for (var i = 0; i < EndpointCount; i++)
{
var httpContext = _requests[i];
await _baseline._matchers[i].MatchAsync(httpContext, feature);
Validate(httpContext, _endpoints[i], feature.Endpoint);
}
}
[Benchmark( OperationsPerInvoke = EndpointCount)]
public async Task Dfa()
{
var feature = _feature;
for (var i = 0; i < EndpointCount; i++)
{
var httpContext = _requests[i];
await _dfa.MatchAsync(httpContext, feature);
Validate(httpContext, _endpoints[i], feature.Endpoint);
}
}
[Benchmark(OperationsPerInvoke = EndpointCount)]
public async Task Instruction()
{
var feature = _feature;
for (var i = 0; i < EndpointCount; i++)
{
var httpContext = _requests[i];
await _instruction.MatchAsync(httpContext, feature);
Validate(httpContext, _endpoints[i], feature.Endpoint);
}
}
[Benchmark(OperationsPerInvoke = EndpointCount)]
public async Task LegacyRoute()
{
@ -35,6 +77,10 @@ namespace Microsoft.AspNetCore.Routing.Matchers
for (var i = 0; i < EndpointCount; i++)
{
var httpContext = _requests[i];
// This is required to make the legacy router implementation work with dispatcher.
httpContext.Features.Set<IEndpointFeature>(feature);
await _route.MatchAsync(httpContext, feature);
Validate(httpContext, _endpoints[i], feature.Endpoint);
}
@ -47,6 +93,10 @@ namespace Microsoft.AspNetCore.Routing.Matchers
for (var i = 0; i < EndpointCount; i++)
{
var httpContext = _requests[i];
// This is required to make the legacy router implementation work with dispatcher.
httpContext.Features.Set<IEndpointFeature>(feature);
await _tree.MatchAsync(httpContext, feature);
Validate(httpContext, _endpoints[i], feature.Endpoint);
}

View File

@ -798,161 +798,161 @@ namespace Microsoft.AspNetCore.Routing.Matchers
private Matcher SetupMatcher(MatcherBuilder builder)
{
builder.AddEntry("/emojis", _endpoints[0]);
builder.AddEntry("/events", _endpoints[1]);
builder.AddEntry("/feeds", _endpoints[2]);
builder.AddEntry("/gists", _endpoints[3]);
builder.AddEntry("/issues", _endpoints[4]);
builder.AddEntry("/markdown", _endpoints[5]);
builder.AddEntry("/meta", _endpoints[6]);
builder.AddEntry("/notifications", _endpoints[7]);
builder.AddEntry("/rate_limit", _endpoints[8]);
builder.AddEntry("/repositories", _endpoints[9]);
builder.AddEntry("/user", _endpoints[10]);
builder.AddEntry("/users", _endpoints[11]);
builder.AddEntry("/gists/public", _endpoints[12]);
builder.AddEntry("/gists/starred", _endpoints[13]);
builder.AddEntry("/gitignore/templates", _endpoints[14]);
builder.AddEntry("/markdown/raw", _endpoints[15]);
builder.AddEntry("/search/code", _endpoints[16]);
builder.AddEntry("/search/issues", _endpoints[17]);
builder.AddEntry("/search/repositories", _endpoints[18]);
builder.AddEntry("/search/users", _endpoints[19]);
builder.AddEntry("/user/emails", _endpoints[20]);
builder.AddEntry("/user/followers", _endpoints[21]);
builder.AddEntry("/user/following", _endpoints[22]);
builder.AddEntry("/user/issues", _endpoints[23]);
builder.AddEntry("/user/keys", _endpoints[24]);
builder.AddEntry("/user/orgs", _endpoints[25]);
builder.AddEntry("/user/repos", _endpoints[26]);
builder.AddEntry("/user/starred", _endpoints[27]);
builder.AddEntry("/user/subscriptions", _endpoints[28]);
builder.AddEntry("/user/teams", _endpoints[29]);
builder.AddEntry("/legacy/repos/search/{keyword}", _endpoints[30]);
builder.AddEntry("/legacy/user/email/{email}", _endpoints[31]);
builder.AddEntry("/legacy/user/search/{keyword}", _endpoints[32]);
builder.AddEntry("/legacy/issues/search/{owner}/{repository}/{state}/{keyword}", _endpoints[33]);
builder.AddEntry("/gitignore/templates/{language}", _endpoints[34]);
builder.AddEntry("/notifications/threads/{id}", _endpoints[35]);
builder.AddEntry("/user/following/{username}", _endpoints[36]);
builder.AddEntry("/user/keys/{keyId}", _endpoints[37]);
builder.AddEntry("/notifications/threads/{id}/subscription", _endpoints[38]);
builder.AddEntry("/user/starred/{owner}/{repo}", _endpoints[39]);
builder.AddEntry("/user/subscriptions/{owner}/{repo}", _endpoints[40]);
builder.AddEntry("/gists/{id}", _endpoints[41]);
builder.AddEntry("/orgs/{org}", _endpoints[42]);
builder.AddEntry("/teams/{teamId}", _endpoints[43]);
builder.AddEntry("/users/{username}", _endpoints[44]);
builder.AddEntry("/gists/{id}/comments", _endpoints[45]);
builder.AddEntry("/gists/{id}/forks", _endpoints[46]);
builder.AddEntry("/gists/{id}/star", _endpoints[47]);
builder.AddEntry("/orgs/{org}/events", _endpoints[48]);
builder.AddEntry("/orgs/{org}/issues", _endpoints[49]);
builder.AddEntry("/orgs/{org}/members", _endpoints[50]);
builder.AddEntry("/orgs/{org}/public_members", _endpoints[51]);
builder.AddEntry("/orgs/{org}/repos", _endpoints[52]);
builder.AddEntry("/orgs/{org}/teams", _endpoints[53]);
builder.AddEntry("/teams/{teamId}/members", _endpoints[54]);
builder.AddEntry("/teams/{teamId}/repos", _endpoints[55]);
builder.AddEntry("/users/{username}/events", _endpoints[56]);
builder.AddEntry("/users/{username}/followers", _endpoints[57]);
builder.AddEntry("/users/{username}/gists", _endpoints[58]);
builder.AddEntry("/users/{username}/keys", _endpoints[59]);
builder.AddEntry("/users/{username}/orgs", _endpoints[60]);
builder.AddEntry("/users/{username}/received_events", _endpoints[61]);
builder.AddEntry("/users/{username}/repos", _endpoints[62]);
builder.AddEntry("/users/{username}/starred", _endpoints[63]);
builder.AddEntry("/users/{username}/subscriptions", _endpoints[64]);
builder.AddEntry("/users/{username}/received_events/public", _endpoints[65]);
builder.AddEntry("/users/{username}/events/orgs/{org}", _endpoints[66]);
builder.AddEntry("/gists/{id}/comments/{commentId}", _endpoints[67]);
builder.AddEntry("/orgs/{org}/members/{username}", _endpoints[68]);
builder.AddEntry("/orgs/{org}/public_members/{username}", _endpoints[69]);
builder.AddEntry("/teams/{teamId}/members/{username}", _endpoints[70]);
builder.AddEntry("/teams/{teamId}/memberships/{username}", _endpoints[71]);
builder.AddEntry("/users/{username}/following/{targetUser}", _endpoints[72]);
builder.AddEntry("/teams/{teamId}/repos/{owner}/{repo}", _endpoints[73]);
builder.AddEntry("/repos/{owner}/{repo}", _endpoints[74]);
builder.AddEntry("/networks/{owner}/{repo}/events", _endpoints[75]);
builder.AddEntry("/repos/{owner}/{repo}/assignees", _endpoints[76]);
builder.AddEntry("/repos/{owner}/{repo}/branches", _endpoints[77]);
builder.AddEntry("/repos/{owner}/{repo}/collaborators", _endpoints[78]);
builder.AddEntry("/repos/{owner}/{repo}/comments", _endpoints[79]);
builder.AddEntry("/repos/{owner}/{repo}/commits", _endpoints[80]);
builder.AddEntry("/repos/{owner}/{repo}/contributors", _endpoints[81]);
builder.AddEntry("/repos/{owner}/{repo}/deployments", _endpoints[82]);
builder.AddEntry("/repos/{owner}/{repo}/downloads", _endpoints[83]);
builder.AddEntry("/repos/{owner}/{repo}/events", _endpoints[84]);
builder.AddEntry("/repos/{owner}/{repo}/forks", _endpoints[85]);
builder.AddEntry("/repos/{owner}/{repo}/hooks", _endpoints[86]);
builder.AddEntry("/repos/{owner}/{repo}/issues", _endpoints[87]);
builder.AddEntry("/repos/{owner}/{repo}/keys", _endpoints[88]);
builder.AddEntry("/repos/{owner}/{repo}/labels", _endpoints[89]);
builder.AddEntry("/repos/{owner}/{repo}/languages", _endpoints[90]);
builder.AddEntry("/repos/{owner}/{repo}/merges", _endpoints[91]);
builder.AddEntry("/repos/{owner}/{repo}/milestones", _endpoints[92]);
builder.AddEntry("/repos/{owner}/{repo}/notifications", _endpoints[93]);
builder.AddEntry("/repos/{owner}/{repo}/pulls", _endpoints[94]);
builder.AddEntry("/repos/{owner}/{repo}/readme", _endpoints[95]);
builder.AddEntry("/repos/{owner}/{repo}/releases", _endpoints[96]);
builder.AddEntry("/repos/{owner}/{repo}/stargazers", _endpoints[97]);
builder.AddEntry("/repos/{owner}/{repo}/subscribers", _endpoints[98]);
builder.AddEntry("/repos/{owner}/{repo}/subscription", _endpoints[99]);
builder.AddEntry("/repos/{owner}/{repo}/tags", _endpoints[100]);
builder.AddEntry("/repos/{owner}/{repo}/teams", _endpoints[101]);
builder.AddEntry("/repos/{owner}/{repo}/watchers", _endpoints[102]);
builder.AddEntry("/repos/{owner}/{repo}/git/blobs", _endpoints[103]);
builder.AddEntry("/repos/{owner}/{repo}/git/commits", _endpoints[104]);
builder.AddEntry("/repos/{owner}/{repo}/git/refs", _endpoints[105]);
builder.AddEntry("/repos/{owner}/{repo}/git/tags", _endpoints[106]);
builder.AddEntry("/repos/{owner}/{repo}/git/trees", _endpoints[107]);
builder.AddEntry("/repos/{owner}/{repo}/issues/comments", _endpoints[108]);
builder.AddEntry("/repos/{owner}/{repo}/issues/events", _endpoints[109]);
builder.AddEntry("/repos/{owner}/{repo}/pulls/comments", _endpoints[110]);
builder.AddEntry("/repos/{owner}/{repo}/stats/code_frequency", _endpoints[111]);
builder.AddEntry("/repos/{owner}/{repo}/stats/commit_activity", _endpoints[112]);
builder.AddEntry("/repos/{owner}/{repo}/stats/contributors", _endpoints[113]);
builder.AddEntry("/repos/{owner}/{repo}/stats/participation", _endpoints[114]);
builder.AddEntry("/repos/{owner}/{repo}/stats/punch_card", _endpoints[115]);
builder.AddEntry("/repos/{owner}/{repo}/git/blobs/{shaCode}", _endpoints[116]);
builder.AddEntry("/repos/{owner}/{repo}/git/commits/{shaCode}", _endpoints[117]);
builder.AddEntry("/repos/{owner}/{repo}/git/refs/{ref}", _endpoints[118]);
builder.AddEntry("/repos/{owner}/{repo}/git/tags/{shaCode}", _endpoints[119]);
builder.AddEntry("/repos/{owner}/{repo}/git/trees/{shaCode}", _endpoints[120]);
builder.AddEntry("/repos/{owner}/{repo}/issues/comments/{commentId}", _endpoints[121]);
builder.AddEntry("/repos/{owner}/{repo}/issues/events/{eventId}", _endpoints[122]);
builder.AddEntry("/repos/{owner}/{repo}/pulls/comments/{commentId}", _endpoints[123]);
builder.AddEntry("/repos/{owner}/{repo}/releases/assets/{id}", _endpoints[124]);
builder.AddEntry("/repos/{owner}/{repo}/assignees/{assignee}", _endpoints[125]);
builder.AddEntry("/repos/{owner}/{repo}/branches/{branch}", _endpoints[126]);
builder.AddEntry("/repos/{owner}/{repo}/collaborators/{user}", _endpoints[127]);
builder.AddEntry("/repos/{owner}/{repo}/comments/{commentId}", _endpoints[128]);
builder.AddEntry("/repos/{owner}/{repo}/commits/{shaCode}", _endpoints[129]);
builder.AddEntry("/repos/{owner}/{repo}/contents/{path}", _endpoints[130]);
builder.AddEntry("/repos/{owner}/{repo}/downloads/{downloadId}", _endpoints[131]);
builder.AddEntry("/repos/{owner}/{repo}/hooks/{hookId}", _endpoints[132]);
builder.AddEntry("/repos/{owner}/{repo}/issues/{number}", _endpoints[133]);
builder.AddEntry("/repos/{owner}/{repo}/keys/{keyId}", _endpoints[134]);
builder.AddEntry("/repos/{owner}/{repo}/labels/{name}", _endpoints[135]);
builder.AddEntry("/repos/{owner}/{repo}/milestones/{number}", _endpoints[136]);
builder.AddEntry("/repos/{owner}/{repo}/pulls/{number}", _endpoints[137]);
builder.AddEntry("/repos/{owner}/{repo}/releases/{id}", _endpoints[138]);
builder.AddEntry("/repos/{owner}/{repo}/statuses/{ref}", _endpoints[139]);
builder.AddEntry("/repos/{owner}/{repo}/commits/{ref}/status", _endpoints[140]);
builder.AddEntry("/repos/{owner}/{repo}/commits/{shaCode}/comments", _endpoints[141]);
builder.AddEntry("/repos/{owner}/{repo}/deployments/{id}/statuses", _endpoints[142]);
builder.AddEntry("/repos/{owner}/{repo}/hooks/{hookId}/tests", _endpoints[143]);
builder.AddEntry("/repos/{owner}/{repo}/issues/{number}/comments", _endpoints[144]);
builder.AddEntry("/repos/{owner}/{repo}/issues/{number}/events", _endpoints[145]);
builder.AddEntry("/repos/{owner}/{repo}/issues/{number}/labels", _endpoints[146]);
builder.AddEntry("/repos/{owner}/{repo}/milestones/{number}/labels", _endpoints[147]);
builder.AddEntry("/repos/{owner}/{repo}/pulls/{number}/comments", _endpoints[148]);
builder.AddEntry("/repos/{owner}/{repo}/pulls/{number}/commits", _endpoints[149]);
builder.AddEntry("/repos/{owner}/{repo}/pulls/{number}/files", _endpoints[150]);
builder.AddEntry("/repos/{owner}/{repo}/pulls/{number}/merge", _endpoints[151]);
builder.AddEntry("/repos/{owner}/{repo}/releases/{id}/assets", _endpoints[152]);
builder.AddEntry("/repos/{owner}/{repo}/issues/{number}/labels/{name}", _endpoints[153]);
builder.AddEntry("/repos/{owner}/{repo}/{archive_format}/{path}", _endpoints[154]);
builder.AddEndpoint(_endpoints[0]);
builder.AddEndpoint(_endpoints[1]);
builder.AddEndpoint(_endpoints[2]);
builder.AddEndpoint(_endpoints[3]);
builder.AddEndpoint(_endpoints[4]);
builder.AddEndpoint(_endpoints[5]);
builder.AddEndpoint(_endpoints[6]);
builder.AddEndpoint(_endpoints[7]);
builder.AddEndpoint(_endpoints[8]);
builder.AddEndpoint(_endpoints[9]);
builder.AddEndpoint(_endpoints[10]);
builder.AddEndpoint(_endpoints[11]);
builder.AddEndpoint(_endpoints[12]);
builder.AddEndpoint(_endpoints[13]);
builder.AddEndpoint(_endpoints[14]);
builder.AddEndpoint(_endpoints[15]);
builder.AddEndpoint(_endpoints[16]);
builder.AddEndpoint(_endpoints[17]);
builder.AddEndpoint(_endpoints[18]);
builder.AddEndpoint(_endpoints[19]);
builder.AddEndpoint(_endpoints[20]);
builder.AddEndpoint(_endpoints[21]);
builder.AddEndpoint(_endpoints[22]);
builder.AddEndpoint(_endpoints[23]);
builder.AddEndpoint(_endpoints[24]);
builder.AddEndpoint(_endpoints[25]);
builder.AddEndpoint(_endpoints[26]);
builder.AddEndpoint(_endpoints[27]);
builder.AddEndpoint(_endpoints[28]);
builder.AddEndpoint(_endpoints[29]);
builder.AddEndpoint(_endpoints[30]);
builder.AddEndpoint(_endpoints[31]);
builder.AddEndpoint(_endpoints[32]);
builder.AddEndpoint(_endpoints[33]);
builder.AddEndpoint(_endpoints[34]);
builder.AddEndpoint(_endpoints[35]);
builder.AddEndpoint(_endpoints[36]);
builder.AddEndpoint(_endpoints[37]);
builder.AddEndpoint(_endpoints[38]);
builder.AddEndpoint(_endpoints[39]);
builder.AddEndpoint(_endpoints[40]);
builder.AddEndpoint(_endpoints[41]);
builder.AddEndpoint(_endpoints[42]);
builder.AddEndpoint(_endpoints[43]);
builder.AddEndpoint(_endpoints[44]);
builder.AddEndpoint(_endpoints[45]);
builder.AddEndpoint(_endpoints[46]);
builder.AddEndpoint(_endpoints[47]);
builder.AddEndpoint(_endpoints[48]);
builder.AddEndpoint(_endpoints[49]);
builder.AddEndpoint(_endpoints[50]);
builder.AddEndpoint(_endpoints[51]);
builder.AddEndpoint(_endpoints[52]);
builder.AddEndpoint(_endpoints[53]);
builder.AddEndpoint(_endpoints[54]);
builder.AddEndpoint(_endpoints[55]);
builder.AddEndpoint(_endpoints[56]);
builder.AddEndpoint(_endpoints[57]);
builder.AddEndpoint(_endpoints[58]);
builder.AddEndpoint(_endpoints[59]);
builder.AddEndpoint(_endpoints[60]);
builder.AddEndpoint(_endpoints[61]);
builder.AddEndpoint(_endpoints[62]);
builder.AddEndpoint(_endpoints[63]);
builder.AddEndpoint(_endpoints[64]);
builder.AddEndpoint(_endpoints[65]);
builder.AddEndpoint(_endpoints[66]);
builder.AddEndpoint(_endpoints[67]);
builder.AddEndpoint(_endpoints[68]);
builder.AddEndpoint(_endpoints[69]);
builder.AddEndpoint(_endpoints[70]);
builder.AddEndpoint(_endpoints[71]);
builder.AddEndpoint(_endpoints[72]);
builder.AddEndpoint(_endpoints[73]);
builder.AddEndpoint(_endpoints[74]);
builder.AddEndpoint(_endpoints[75]);
builder.AddEndpoint(_endpoints[76]);
builder.AddEndpoint(_endpoints[77]);
builder.AddEndpoint(_endpoints[78]);
builder.AddEndpoint(_endpoints[79]);
builder.AddEndpoint(_endpoints[80]);
builder.AddEndpoint(_endpoints[81]);
builder.AddEndpoint(_endpoints[82]);
builder.AddEndpoint(_endpoints[83]);
builder.AddEndpoint(_endpoints[84]);
builder.AddEndpoint(_endpoints[85]);
builder.AddEndpoint(_endpoints[86]);
builder.AddEndpoint(_endpoints[87]);
builder.AddEndpoint(_endpoints[88]);
builder.AddEndpoint(_endpoints[89]);
builder.AddEndpoint(_endpoints[90]);
builder.AddEndpoint(_endpoints[91]);
builder.AddEndpoint(_endpoints[92]);
builder.AddEndpoint(_endpoints[93]);
builder.AddEndpoint(_endpoints[94]);
builder.AddEndpoint(_endpoints[95]);
builder.AddEndpoint(_endpoints[96]);
builder.AddEndpoint(_endpoints[97]);
builder.AddEndpoint(_endpoints[98]);
builder.AddEndpoint(_endpoints[99]);
builder.AddEndpoint(_endpoints[100]);
builder.AddEndpoint(_endpoints[101]);
builder.AddEndpoint(_endpoints[102]);
builder.AddEndpoint(_endpoints[103]);
builder.AddEndpoint(_endpoints[104]);
builder.AddEndpoint(_endpoints[105]);
builder.AddEndpoint(_endpoints[106]);
builder.AddEndpoint(_endpoints[107]);
builder.AddEndpoint(_endpoints[108]);
builder.AddEndpoint(_endpoints[109]);
builder.AddEndpoint(_endpoints[110]);
builder.AddEndpoint(_endpoints[111]);
builder.AddEndpoint(_endpoints[112]);
builder.AddEndpoint(_endpoints[113]);
builder.AddEndpoint(_endpoints[114]);
builder.AddEndpoint(_endpoints[115]);
builder.AddEndpoint(_endpoints[116]);
builder.AddEndpoint(_endpoints[117]);
builder.AddEndpoint(_endpoints[118]);
builder.AddEndpoint(_endpoints[119]);
builder.AddEndpoint(_endpoints[120]);
builder.AddEndpoint(_endpoints[121]);
builder.AddEndpoint(_endpoints[122]);
builder.AddEndpoint(_endpoints[123]);
builder.AddEndpoint(_endpoints[124]);
builder.AddEndpoint(_endpoints[125]);
builder.AddEndpoint(_endpoints[126]);
builder.AddEndpoint(_endpoints[127]);
builder.AddEndpoint(_endpoints[128]);
builder.AddEndpoint(_endpoints[129]);
builder.AddEndpoint(_endpoints[130]);
builder.AddEndpoint(_endpoints[131]);
builder.AddEndpoint(_endpoints[132]);
builder.AddEndpoint(_endpoints[133]);
builder.AddEndpoint(_endpoints[134]);
builder.AddEndpoint(_endpoints[135]);
builder.AddEndpoint(_endpoints[136]);
builder.AddEndpoint(_endpoints[137]);
builder.AddEndpoint(_endpoints[138]);
builder.AddEndpoint(_endpoints[139]);
builder.AddEndpoint(_endpoints[140]);
builder.AddEndpoint(_endpoints[141]);
builder.AddEndpoint(_endpoints[142]);
builder.AddEndpoint(_endpoints[143]);
builder.AddEndpoint(_endpoints[144]);
builder.AddEndpoint(_endpoints[145]);
builder.AddEndpoint(_endpoints[146]);
builder.AddEndpoint(_endpoints[147]);
builder.AddEndpoint(_endpoints[148]);
builder.AddEndpoint(_endpoints[149]);
builder.AddEndpoint(_endpoints[150]);
builder.AddEndpoint(_endpoints[151]);
builder.AddEndpoint(_endpoints[152]);
builder.AddEndpoint(_endpoints[153]);
builder.AddEndpoint(_endpoints[154]);
return builder.Build();
}
}

View File

@ -1,479 +0,0 @@
// 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;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Template;
namespace Microsoft.AspNetCore.Routing.Matchers
{
internal class InstructionMatcher : Matcher
{
public static MatcherBuilder CreateBuilder() => new Builder();
private State _state;
private InstructionMatcher(Instruction[] instructions, Endpoint[] endpoints, JumpTable[] tables)
{
_state = new State()
{
Instructions = instructions,
Endpoints = endpoints,
Tables = tables,
};
}
public unsafe override Task MatchAsync(HttpContext httpContext, IEndpointFeature feature)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
if (feature == null)
{
throw new ArgumentNullException(nameof(feature));
}
var state = _state;
var path = httpContext.Request.Path.Value;
var buffer = stackalloc int[32];
var segment = 0;
var index = -1;
while ((index = path.IndexOf('/', index + 1)) >= 0)
{
buffer[segment++] = index + 1;
}
buffer[segment] = path.Length;
var i = 0;
Endpoint result = null;
while (i < state.Instructions.Length)
{
var instruction = state.Instructions[i];
switch (instruction.Code)
{
case InstructionCode.Accept:
{
result = state.Endpoints[instruction.Payload];
i++;
break;
}
case InstructionCode.Branch:
{
var table = state.Tables[instruction.Payload];
i = table.GetDestination(buffer, path);
break;
}
case InstructionCode.Jump:
{
i = instruction.Payload;
break;
}
}
}
feature.Endpoint = result;
return Task.CompletedTask;
}
private class State
{
public Endpoint[] Endpoints;
public Instruction[] Instructions;
public JumpTable[] Tables;
}
[DebuggerDisplay("{ToDebugString(),nq}")]
[StructLayout(LayoutKind.Explicit)]
private struct Instruction
{
[FieldOffset(3)]
public InstructionCode Code;
[FieldOffset(4)]
public int Payload;
private string ToDebugString()
{
return $"{Code}: {Payload}";
}
}
private enum InstructionCode : byte
{
Accept,
Branch,
Jump,
Pop, // Only used during the instruction builder phase
}
private abstract class JumpTable
{
public unsafe abstract int GetDestination(int* segments, string text);
}
private class JumpTableBuilder
{
private readonly List<(string text, int destination)> _entries = new List<(string text, int destination)>();
public int Depth { get; set; }
public int Exit { get; set; }
public void AddEntry(string text, int destination)
{
_entries.Add((text, destination));
}
public JumpTable Build()
{
return new SimpleJumpTable(Depth, Exit, _entries.ToArray());
}
}
private class SimpleJumpTable : JumpTable
{
private readonly (string text, int destination)[] _entries;
private readonly int _depth;
private readonly int _exit;
public SimpleJumpTable(int depth, int exit, (string text, int destination)[] entries)
{
_depth = depth;
_exit = exit;
_entries = entries;
}
public unsafe override int GetDestination(int* segments, string text)
{
var start = segments[_depth];
var length = segments[_depth + 1] - start;
for (var i = 0; i < _entries.Length; i++)
{
if (length == _entries[i].text.Length &&
string.Compare(
text,
start,
_entries[i].text,
0,
length,
StringComparison.OrdinalIgnoreCase) == 0)
{
return _entries[i].destination;
}
}
return _exit;
}
}
private class Entry
{
public int Order;
public decimal Precedence;
public RouteTemplate Pattern;
public Endpoint Endpoint;
}
private class InstructionBuilder
{
private readonly List<Instruction> _instructions = new List<Instruction>();
private readonly List<Endpoint> _endpoints = new List<Endpoint>();
private readonly List<JumpTableBuilder> _tables = new List<JumpTableBuilder>();
private readonly List<int> _blocks = new List<int>();
public int Next => _instructions.Count;
public void BeginBlock()
{
_blocks.Add(Next);
}
public void EndBlock()
{
var start = _blocks[_blocks.Count - 1];
var end = Next;
for (var i = start; i < end; i++)
{
if (_instructions[i].Code == InstructionCode.Pop)
{
_instructions[i] = new Instruction() { Code = InstructionCode.Jump, Payload = end };
}
}
_blocks.RemoveAt(_blocks.Count - 1);
}
public int AddInstruction(Instruction instruction)
{
_instructions.Add(instruction);
return _instructions.Count - 1;
}
public int AddEndpoint(Endpoint endpoint)
{
_endpoints.Add(endpoint);
return _endpoints.Count - 1;
}
public int AddJumpTable(JumpTableBuilder table)
{
_tables.Add(table);
return _tables.Count - 1;
}
public void Deconstruct(out Instruction[] instructions, out Endpoint[] endpoints, out JumpTable[] tables)
{
instructions = _instructions.ToArray();
endpoints = _endpoints.ToArray();
tables = new JumpTable[_tables.Count];
for (var i = 0; i < _tables.Count; i++)
{
tables[i] = _tables[i].Build();
}
}
}
private abstract class Node
{
public int Depth { get; protected set; }
public List<Node> Children { get; } = new List<Node>();
public abstract void Lower(InstructionBuilder builder);
public TNode GetNode<TNode>() where TNode : Node
{
for (var i = 0; i < Children.Count; i++)
{
if (Children[i] is TNode match)
{
return match;
}
}
return null;
}
public TNode AddNode<TNode>(TNode node) where TNode : Node
{
// We already ordered the routes into precedence order
Children.Add(node);
return node;
}
}
private class SequenceNode : Node
{
public SequenceNode(int depth)
{
Depth = depth;
}
public override void Lower(InstructionBuilder builder)
{
for (var i = 0; i < Children.Count; i++)
{
Children[i].Lower(builder);
}
}
}
private class OrderNode : SequenceNode
{
public OrderNode(int order)
: base(0)
{
Order = order;
}
public int Order { get; }
}
private class BranchNode : Node
{
public BranchNode(int depth)
{
Depth = depth;
}
public List<string> Literals { get; } = new List<string>();
public override void Lower(InstructionBuilder builder)
{
var table = new JumpTableBuilder() { Depth = Depth, };
var index = builder.AddJumpTable(table);
builder.AddInstruction(new Instruction() { Code = InstructionCode.Branch, Payload = index });
builder.BeginBlock();
for (var i = 0; i < Children.Count; i++)
{
table.AddEntry(Literals[i], builder.Next);
Children[i].Lower(builder);
builder.AddInstruction(new Instruction() { Code = InstructionCode.Pop, });
}
builder.EndBlock();
table.Exit = builder.Next;
}
}
private class ParameterNode : Node
{
public ParameterNode(int depth)
{
Depth = depth;
}
public override void Lower(InstructionBuilder builder)
{
for (var i = 0; i < Children.Count; i++)
{
Children[i].Lower(builder);
}
}
}
private class AcceptNode : Node
{
public AcceptNode(Endpoint endpoint)
{
Endpoint = endpoint;
}
public Endpoint Endpoint { get; }
public override void Lower(InstructionBuilder builder)
{
builder.AddInstruction(new Instruction()
{
Code = InstructionCode.Accept,
Payload = builder.AddEndpoint(Endpoint),
});
}
}
private class Builder : MatcherBuilder
{
private List<Entry> _entries = new List<Entry>();
public override void AddEntry(string pattern, MatcherEndpoint endpoint)
{
var parsed = TemplateParser.Parse(pattern);
_entries.Add(new Entry()
{
Order = 0,
Pattern = parsed,
Precedence = RoutePrecedence.ComputeInbound(parsed),
Endpoint = endpoint,
});
}
public override Matcher Build()
{
_entries.Sort((x, y) =>
{
var comparison = x.Order.CompareTo(y.Order);
if (comparison != 0)
{
return comparison;
}
comparison = y.Precedence.CompareTo(x.Precedence);
if (comparison != 0)
{
return comparison;
}
return x.Pattern.TemplateText.CompareTo(y.Pattern.TemplateText);
});
var roots = new List<OrderNode>();
for (var i = 0; i < _entries.Count; i++)
{
var entry = _entries[i];
var parent = (SequenceNode)GetOrCreateRootNode(roots, entry.Order);
for (var depth = 0; depth < entry.Pattern.Segments.Count; depth++)
{
var segment = entry.Pattern.Segments[depth];
if (segment.IsSimple && segment.Parts[0].IsLiteral)
{
var branch = parent.GetNode<BranchNode>() ?? parent.AddNode(new BranchNode(depth));
var index = branch.Literals.IndexOf(segment.Parts[0].Text);
if (index == -1)
{
branch.Literals.Add(segment.Parts[0].Text);
branch.AddNode(new SequenceNode(depth + 1));
index = branch.Children.Count - 1;
}
parent = (SequenceNode)branch.Children[index];
}
else if (segment.IsSimple && segment.Parts[0].IsParameter)
{
var parameter = parent.GetNode<ParameterNode>() ?? parent.AddNode(new ParameterNode(depth));
if (parameter.Children.Count == 0)
{
parameter.AddNode(new SequenceNode(depth + 1));
}
parent = (SequenceNode)parameter.Children[0];
}
else
{
throw new InvalidOperationException("Not implemented!");
}
}
parent.AddNode(new AcceptNode(entry.Endpoint));
}
var builder = new InstructionBuilder();
for (var i = 0; i < roots.Count; i++)
{
roots[i].Lower(builder);
}
var (instructions, endpoints, tables) = builder;
return new InstructionMatcher(instructions, endpoints, tables);
}
private OrderNode GetOrCreateRootNode(List<OrderNode> roots, int order)
{
OrderNode root = null;
for (var j = 0; j < roots.Count; j++)
{
if (roots[j].Order == order)
{
root = roots[j];
break;
}
}
if (root == null)
{
// Nodes are guaranteed to be in order because the entries are in order.
root = new OrderNode(order);
roots.Add(root);
}
return root;
}
}
}
}

View File

@ -1,62 +0,0 @@
// 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;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing.Matchers
{
internal class MinimalMatcher : Matcher
{
public static MatcherBuilder CreateBuilder() => new Builder();
private readonly (string pattern, Endpoint endpoint)[] _entries;
private MinimalMatcher((string pattern, Endpoint endpoint)[] entries)
{
_entries = entries;
}
public override Task MatchAsync(HttpContext httpContext, IEndpointFeature feature)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
if (feature == null)
{
throw new ArgumentNullException(nameof(feature));
}
var path = httpContext.Request.Path.Value;
for (var i = 0; i < _entries.Length; i++)
{
if (string.Equals(_entries[i].pattern, path, StringComparison.OrdinalIgnoreCase))
{
feature.Endpoint = _entries[i].endpoint;
return Task.CompletedTask;
}
}
return Task.CompletedTask;
}
private class Builder : MatcherBuilder
{
private List<(string pattern, Endpoint endpoint)> _entries = new List<(string pattern, Endpoint endpoint)>();
public override void AddEntry(string pattern, MatcherEndpoint endpoint)
{
_entries.Add((pattern, endpoint));
}
public override Matcher Build()
{
return new MinimalMatcher(_entries.ToArray());
}
}
}
}

View File

@ -1,70 +0,0 @@
// 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;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Routing.Matchers
{
internal class RouteMatcher : Matcher
{
public static MatcherBuilder CreateBuilder() => new Builder();
private IRouter _inner;
private RouteMatcher(IRouter inner)
{
_inner = inner;
}
public async override Task MatchAsync(HttpContext httpContext, IEndpointFeature feature)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
if (feature == null)
{
throw new ArgumentNullException(nameof(feature));
}
var context = new RouteContext(httpContext);
await _inner.RouteAsync(context);
if (context.Handler != null)
{
httpContext.Features.Set<IEndpointFeature>(feature);
await context.Handler(httpContext);
}
}
private class Builder : MatcherBuilder
{
private readonly RouteCollection _routes = new RouteCollection();
private readonly IInlineConstraintResolver _constraintResolver;
public Builder()
{
_constraintResolver = new DefaultInlineConstraintResolver(Options.Create(new RouteOptions()));
}
public override void AddEntry(string pattern, MatcherEndpoint endpoint)
{
var handler = new RouteHandler(c =>
{
c.Features.Get<IEndpointFeature>().Endpoint = endpoint;
return Task.CompletedTask;
});
_routes.Add(new Route(handler, pattern, _constraintResolver));
}
public override Matcher Build()
{
return new RouteMatcher(_routes);
}
}
}
}

View File

@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
{
public class SingleEntryMatcherBenchmark : MatcherBenchmarkBase
{
private Matcher _minimal;
private Matcher _baseline;
private Matcher _dfa;
private Matcher _instruction;
private Matcher _route;
@ -28,26 +28,26 @@ namespace Microsoft.AspNetCore.Routing.Matchers
_requests[0].RequestServices = CreateServices();
_requests[0].Request.Path = "/plaintext";
_minimal = SetupMatcher(MinimalMatcher.CreateBuilder());
_dfa = SetupMatcher(DfaMatcher.CreateBuilder());
_instruction = SetupMatcher(InstructionMatcher.CreateBuilder());
_route = SetupMatcher(RouteMatcher.CreateBuilder());
_tree = SetupMatcher(TreeRouterMatcher.CreateBuilder());
_baseline = SetupMatcher(new TrivialMatcherBuilder());
_dfa = SetupMatcher(new DfaMatcherBuilder());
_instruction = SetupMatcher(new InstructionMatcherBuilder());
_route = SetupMatcher(new RouteMatcherBuilder());
_tree = SetupMatcher(new TreeRouterMatcherBuilder());
_feature = new EndpointFeature();
}
private Matcher SetupMatcher(MatcherBuilder builder)
{
builder.AddEntry("/plaintext", _endpoints[0]);
builder.AddEndpoint(_endpoints[0]);
return builder.Build();
}
[Benchmark(Baseline = true)]
public async Task Minimal()
public async Task Baseline()
{
var feature = _feature;
await _minimal.MatchAsync(_requests[0], feature);
await _baseline.MatchAsync(_requests[0], feature);
Validate(_requests[0], _endpoints[0], feature.Endpoint);
}
@ -71,6 +71,10 @@ namespace Microsoft.AspNetCore.Routing.Matchers
public async Task LegacyRoute()
{
var feature = _feature;
// This is required to make the legacy router implementation work with dispatcher.
_requests[0].Features.Set<IEndpointFeature>(feature);
await _route.MatchAsync(_requests[0], feature);
Validate(_requests[0], _endpoints[0], feature.Endpoint);
}
@ -79,6 +83,10 @@ namespace Microsoft.AspNetCore.Routing.Matchers
public async Task LegacyTreeRouter()
{
var feature = _feature;
// This is required to make the legacy router implementation work with dispatcher.
_requests[0].Features.Set<IEndpointFeature>(feature);
await _tree.MatchAsync(_requests[0], feature);
Validate(_requests[0], _endpoints[0], feature.Endpoint);
}

View File

@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
{
public class SmallEntryCountLiteralMatcherBenchark : MatcherBenchmarkBase
{
private Matcher _minimal;
private Matcher _baseline;
private Matcher _dfa;
private Matcher _instruction;
private Matcher _route;
@ -20,46 +20,65 @@ namespace Microsoft.AspNetCore.Routing.Matchers
[GlobalSetup]
public void Setup()
{
_endpoints = new MatcherEndpoint[1];
_endpoints[0] = CreateEndpoint("/plaintext");
SetupEndpoints();
SetupRequests();
_baseline = SetupMatcher(new TrivialMatcherBuilder());
_dfa = SetupMatcher(new DfaMatcherBuilder());
_instruction = SetupMatcher(new InstructionMatcherBuilder());
_route = SetupMatcher(new RouteMatcherBuilder());
_tree = SetupMatcher(new TreeRouterMatcherBuilder());
_feature = new EndpointFeature();
}
private void SetupEndpoints()
{
_endpoints = new MatcherEndpoint[10];
_endpoints[0] = CreateEndpoint("/another-really-cool-entry");
_endpoints[1] = CreateEndpoint("/Some-Entry");
_endpoints[2] = CreateEndpoint("/a/path/with/more/segments");
_endpoints[3] = CreateEndpoint("/random/name");
_endpoints[4] = CreateEndpoint("/random/name2");
_endpoints[5] = CreateEndpoint("/random/name3");
_endpoints[6] = CreateEndpoint("/random/name4");
_endpoints[7] = CreateEndpoint("/plaintext1");
_endpoints[8] = CreateEndpoint("/plaintext2");
_endpoints[9] = CreateEndpoint("/plaintext");
}
private void SetupRequests()
{
_requests = new HttpContext[1];
_requests[0] = new DefaultHttpContext();
_requests[0].RequestServices = CreateServices();
_requests[0].Request.Path = "/plaintext";
_minimal = SetupMatcher(MinimalMatcher.CreateBuilder());
_dfa = SetupMatcher(DfaMatcher.CreateBuilder());
_instruction = SetupMatcher(InstructionMatcher.CreateBuilder());
_route = SetupMatcher(RouteMatcher.CreateBuilder());
_tree = SetupMatcher(TreeRouterMatcher.CreateBuilder());
_feature = new EndpointFeature();
}
// For this case we're specifically targeting the last entry to hit 'worst case'
// performance for the matchers that scale linearly.
private Matcher SetupMatcher(MatcherBuilder builder)
{
builder.AddEntry("/another-really-cool-entry", null);
builder.AddEntry("/Some-Entry", null);
builder.AddEntry("/a/path/with/more/segments", null);
builder.AddEntry("/random/name", null);
builder.AddEntry("/random/name2", null);
builder.AddEntry("/random/name3", null);
builder.AddEntry("/random/name4", null);
builder.AddEntry("/plaintext1", null);
builder.AddEntry("/plaintext2", null);
builder.AddEntry("/plaintext", _endpoints[0]);
builder.AddEndpoint(_endpoints[0]);
builder.AddEndpoint(_endpoints[1]);
builder.AddEndpoint(_endpoints[2]);
builder.AddEndpoint(_endpoints[3]);
builder.AddEndpoint(_endpoints[4]);
builder.AddEndpoint(_endpoints[5]);
builder.AddEndpoint(_endpoints[6]);
builder.AddEndpoint(_endpoints[7]);
builder.AddEndpoint(_endpoints[8]);
builder.AddEndpoint(_endpoints[9]);
return builder.Build();
}
[Benchmark(Baseline = true)]
public async Task Minimal()
public async Task Baseline()
{
var feature = _feature;
await _minimal.MatchAsync(_requests[0], feature);
Validate(_requests[0], _endpoints[0], feature.Endpoint);
await _baseline.MatchAsync(_requests[0], feature);
Validate(_requests[0], _endpoints[9], feature.Endpoint);
}
[Benchmark]
@ -67,7 +86,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
{
var feature = _feature;
await _dfa.MatchAsync(_requests[0], feature);
Validate(_requests[0], _endpoints[0], feature.Endpoint);
Validate(_requests[0], _endpoints[9], feature.Endpoint);
}
[Benchmark]
@ -75,23 +94,31 @@ namespace Microsoft.AspNetCore.Routing.Matchers
{
var feature = _feature;
await _instruction.MatchAsync(_requests[0], feature);
Validate(_requests[0], _endpoints[0], feature.Endpoint);
Validate(_requests[0], _endpoints[9], feature.Endpoint);
}
[Benchmark]
public async Task LegacyRoute()
{
var feature = _feature;
// This is required to make the legacy router implementation work with dispatcher.
_requests[0].Features.Set<IEndpointFeature>(feature);
await _route.MatchAsync(_requests[0], feature);
Validate(_requests[0], _endpoints[0], feature.Endpoint);
Validate(_requests[0], _endpoints[9], feature.Endpoint);
}
[Benchmark]
public async Task LegacyTreeRouter()
{
var feature = _feature;
// This is required to make the legacy router implementation work with dispatcher.
_requests[0].Features.Set<IEndpointFeature>(feature);
await _tree.MatchAsync(_requests[0], feature);
Validate(_requests[0], _endpoints[0], feature.Endpoint);
Validate(_requests[0], _endpoints[9], feature.Endpoint);
}
}
}

View File

@ -1,76 +0,0 @@
// 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;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Internal;
using Microsoft.AspNetCore.Routing.Template;
using Microsoft.AspNetCore.Routing.Tree;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Routing.Matchers
{
internal class TreeRouterMatcher : Matcher
{
public static MatcherBuilder CreateBuilder() => new Builder();
private TreeRouter _inner;
private TreeRouterMatcher(TreeRouter inner)
{
_inner = inner;
}
public async override Task MatchAsync(HttpContext httpContext, IEndpointFeature feature)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
if (feature == null)
{
throw new ArgumentNullException(nameof(feature));
}
var context = new RouteContext(httpContext);
await _inner.RouteAsync(context);
if (context.Handler != null)
{
await context.Handler(httpContext);
}
}
private class Builder : MatcherBuilder
{
private readonly TreeRouteBuilder _inner;
public Builder()
{
_inner = new TreeRouteBuilder(
NullLoggerFactory.Instance,
new DefaultObjectPool<UriBuildingContext>(new UriBuilderContextPooledObjectPolicy()),
new DefaultInlineConstraintResolver(Options.Create(new RouteOptions())));
}
public override void AddEntry(string template, MatcherEndpoint endpoint)
{
var handler = new RouteHandler(c =>
{
c.Features.Get<IEndpointFeature>().Endpoint = endpoint;
return Task.CompletedTask;
});
_inner.MapInbound(handler, TemplateParser.Parse(template), "default", 0);
}
public override Matcher Build()
{
return new TreeRouterMatcher(_inner.Build());
}
}
}
}

View File

@ -0,0 +1,44 @@
// 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;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing.Matchers
{
// A test-only matcher implementation - used as a baseline for simpler
// perf tests. The idea with this matcher is that we can cheat on the requirements
// to establish a lower bound for perf comparisons.
internal class TrivialMatcher : Matcher
{
private readonly MatcherEndpoint _endpoint;
public TrivialMatcher(MatcherEndpoint endpoint)
{
_endpoint = endpoint;
}
public override Task MatchAsync(HttpContext httpContext, IEndpointFeature feature)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
if (feature == null)
{
throw new ArgumentNullException(nameof(feature));
}
var path = httpContext.Request.Path.Value;
if (string.Equals(_endpoint.Template, path, StringComparison.OrdinalIgnoreCase))
{
feature.Endpoint = _endpoint;
feature.Values = new RouteValueDictionary();
}
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,23 @@
// 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 System.Linq;
namespace Microsoft.AspNetCore.Routing.Matchers
{
internal class TrivialMatcherBuilder : MatcherBuilder
{
private readonly List<MatcherEndpoint> _endpoints = new List<MatcherEndpoint>();
public override void AddEndpoint(MatcherEndpoint endpoint)
{
_endpoints.Add(endpoint);
}
public override Matcher Build()
{
return new TrivialMatcher(_endpoints.Last());
}
}
}

View File

@ -14,6 +14,46 @@
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.Routing\Microsoft.AspNetCore.Routing.csproj" />
</ItemGroup>
<!--
Some sources are shared with the unit test so we can benchmark some 'test only' implementations
for perf comparisons.
-->
<ItemGroup>
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matchers\MatcherBuilder.cs">
<Link>Matchers\MatcherBuilder.cs</Link>
</Compile>
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matchers\BarebonesMatcher.cs">
<Link>Matchers\BarebonesMatcher.cs</Link>
</Compile>
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matchers\BarebonesMatcherBuilder.cs">
<Link>Matchers\BarebonesMatcherBuilder.cs</Link>
</Compile>
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matchers\DfaMatcher.cs">
<Link>Matchers\DfaMatcher.cs</Link>
</Compile>
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matchers\DfaMatcherBuilder.cs">
<Link>Matchers\DfaMatcherBuilder.cs</Link>
</Compile>
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matchers\InstructionMatcher.cs">
<Link>Matchers\InstructionMatcher.cs</Link>
</Compile>
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matchers\InstructionMatcherBuilder.cs">
<Link>Matchers\InstructionMatcherBuilder.cs</Link>
</Compile>
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matchers\RouteMatcher.cs">
<Link>Matchers\RouteMatcher.cs</Link>
</Compile>
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matchers\RouteMatcherBuilder.cs">
<Link>Matchers\RouteMatcherBuilder.cs</Link>
</Compile>
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matchers\TreeRouterMatcher.cs">
<Link>Matchers\TreeRouterMatcher.cs</Link>
</Compile>
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matchers\TreeRouterMatcherBuilder.cs">
<Link>Matchers\TreeRouterMatcherBuilder.cs</Link>
</Compile>
</ItemGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="$(BenchmarkDotNetPackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.BenchmarkRunner.Sources" PrivateAssets="All" Version="$(MicrosoftAspNetCoreBenchmarkRunnerSourcesPackageVersion)" />

View File

@ -28,7 +28,7 @@ namespace Swaggatherer
var setupMatcherLines = new List<string>();
for (var i = 0; i < entries.Count; i++)
{
setupMatcherLines.Add($" builder.AddEntry(\"{entries[i].Template.TemplateText}\", _endpoints[{i}]);");
setupMatcherLines.Add($" builder.AddEndpoint(_endpoints[{i}]);");
}
return string.Format(@"

View File

@ -0,0 +1,122 @@
// 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;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing.Matchers
{
// A test-only matcher implementation - used as a baseline for more compilated
// perf tests. The idea with this matcher is that we can cheat on the requirements
// to establish a lower bound for perf comparisons.
internal class BarebonesMatcher : Matcher
{
public readonly InnerMatcher[] _matchers;
public BarebonesMatcher(InnerMatcher[] matchers)
{
_matchers = matchers;
}
public override Task MatchAsync(HttpContext httpContext, IEndpointFeature feature)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
if (feature == null)
{
throw new ArgumentNullException(nameof(feature));
}
for (var i = 0; i < _matchers.Length; i++)
{
if (_matchers[i].TryMatch(httpContext, feature))
{
feature.Endpoint = _matchers[i]._endpoint;
feature.Values = new RouteValueDictionary();
}
}
return Task.CompletedTask;
}
public sealed class InnerMatcher : Matcher
{
private readonly string[] _segments;
public readonly MatcherEndpoint _endpoint;
public InnerMatcher(string[] segments, MatcherEndpoint endpoint)
{
_segments = segments;
_endpoint = endpoint;
}
public bool TryMatch(HttpContext httpContext, IEndpointFeature feature)
{
var segment = 0;
var path = httpContext.Request.Path.Value;
var start = 1; // PathString always has a leading slash
var end = 0;
while ((end = path.IndexOf('/', start)) >= 0)
{
var comparand = _segments.Length > segment ? _segments[segment] : null;
if ((comparand == null && end - start == 0) ||
(comparand != null &&
(comparand.Length != end - start ||
string.Compare(
path,
start,
comparand,
0,
comparand.Length,
StringComparison.OrdinalIgnoreCase) != 0)))
{
return false;
}
start = end + 1;
segment++;
}
// residue
var length = path.Length - start;
if (length > 0)
{
var comparand = _segments.Length > segment ? _segments[segment] : null;
if (comparand != null &&
(comparand.Length != length ||
string.Compare(
path,
start,
comparand,
0,
comparand.Length,
StringComparison.OrdinalIgnoreCase) != 0))
{
return false;
}
segment++;
}
return segment == _segments.Length;
}
public override Task MatchAsync(HttpContext httpContext, IEndpointFeature feature)
{
if (TryMatch(httpContext, feature))
{
feature.Endpoint = _endpoint;
feature.Values = new RouteValueDictionary();
}
return Task.CompletedTask;
}
}
}
}

View File

@ -0,0 +1,35 @@
// 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 System.Linq;
using Microsoft.AspNetCore.Routing.Template;
using static Microsoft.AspNetCore.Routing.Matchers.BarebonesMatcher;
namespace Microsoft.AspNetCore.Routing.Matchers
{
internal class BarebonesMatcherBuilder : MatcherBuilder
{
private List<MatcherEndpoint> _endpoints = new List<MatcherEndpoint>();
public override void AddEndpoint(MatcherEndpoint endpoint)
{
_endpoints.Add(endpoint);
}
public override Matcher Build()
{
var matchers = new InnerMatcher[_endpoints.Count];
for (var i = 0; i < _endpoints.Count; i++)
{
var parsed = TemplateParser.Parse(_endpoints[i].Template);
var segments = parsed.Segments
.Select(s => s.IsSimple && s.Parts[0].IsLiteral ? s.Parts[0].Text : null)
.ToArray();
matchers[i] = new InnerMatcher(segments, _endpoints[i]);
}
return new BarebonesMatcher(matchers);
}
}
}

View File

@ -0,0 +1,42 @@
// 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;
using System.Threading.Tasks;
using Xunit;
namespace Microsoft.AspNetCore.Routing.Matchers
{
public class BarebonesMatcherConformanceTest : MatcherConformanceTest
{
// Route values not supported
[Fact]
public override Task Match_SingleParameter()
{
return Task.CompletedTask;
}
// Route values not supported
[Fact]
public override Task Match_SingleParameter_TrailingSlash()
{
return Task.CompletedTask;
}
// Route values not supported
[Theory]
[InlineData(null, null, null, null)]
public override Task Match_MultipleParameters(string template, string path, string[] keys, string[] values)
{
GC.KeepAlive(new object [] { template, path, keys, values });
return Task.CompletedTask;
}
internal override Matcher CreateMatcher(MatcherEndpoint endpoint)
{
var builder = new BarebonesMatcherBuilder();
builder.AddEndpoint(endpoint);
return builder.Build();
}
}
}

View File

@ -0,0 +1,178 @@
// 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;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing.Matchers
{
internal class DfaMatcher : Matcher
{
private readonly State[] _states;
public DfaMatcher(State[] states)
{
_states = states;
}
public unsafe override Task MatchAsync(HttpContext httpContext, IEndpointFeature feature)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
if (feature == null)
{
throw new ArgumentNullException(nameof(feature));
}
var states = _states;
var current = 0;
var path = httpContext.Request.Path.Value;
// This section tokenizes the path by marking the sequence of slashes, and their
// position in the string. The consuming code uses the sequence and the count of
// slashes to deduce the length of each segment.
//
// If there is residue (text after last slash) then the length of the segment will
// computed based on the string length.
var buffer = stackalloc Segment[32];
var segment = 0;
var start = 1; // PathString guarantees a leading /
var end = 0;
var length = 0;
while ((end = path.IndexOf('/', start)) >= 0 && segment < 32)
{
length = end - start;
buffer[segment++] = new Segment() { Start = start, Length = length, };
current = states[current].Transitions.GetDestination(path, start, length);
start = end + 1; // resume search after the current character
}
// Residue
length = path.Length - start;
if (length > 0)
{
buffer[segment++] = new Segment() { Start = start, Length = length, };
current = states[current].Transitions.GetDestination(path, start, length);
}
var matches = new List<(Endpoint, RouteValueDictionary)>();
var candidates = states[current].Matches;
for (var i = 0; i < candidates.Length; i++)
{
var values = new RouteValueDictionary();
var parameters = candidates[i].Parameters;
if (parameters != null)
{
for (var j = 0; j < parameters.Length; j++)
{
var parameter = parameters[j];
if (parameter != null && buffer[j].Length == 0)
{
goto notmatch;
}
else if (parameter != null)
{
var value = path.Substring(buffer[j].Start, buffer[j].Length);
values.Add(parameter, value);
}
}
}
matches.Add((candidates[i].Endpoint, values));
notmatch: ;
}
feature.Endpoint = matches.Count == 0 ? null : matches[0].Item1;
feature.Values = matches.Count == 0 ? null : matches[0].Item2;
return Task.CompletedTask;
}
public struct Segment
{
public int Start;
public int Length;
}
public struct State
{
public bool IsAccepting;
public Candidate[] Matches;
public JumpTable Transitions;
}
public struct Candidate
{
public Endpoint Endpoint;
public string[] Parameters;
}
public abstract class JumpTable
{
public abstract int GetDestination(string text, int start, int length);
}
public class JumpTableBuilder
{
private readonly List<(string text, int destination)> _entries = new List<(string text, int destination)>();
public int Depth { get; set; }
public int Exit { get; set; }
public void AddEntry(string text, int destination)
{
_entries.Add((text, destination));
}
public JumpTable Build()
{
return new SimpleJumpTable(Depth, Exit, _entries.ToArray());
}
}
private class SimpleJumpTable : JumpTable
{
private readonly (string text, int destination)[] _entries;
private readonly int _depth;
private readonly int _exit;
public SimpleJumpTable(int depth, int exit, (string text, int destination)[] entries)
{
_depth = depth;
_exit = exit;
_entries = entries;
}
public override int GetDestination(string text, int start, int length)
{
for (var i = 0; i < _entries.Length; i++)
{
if (length == _entries[i].text.Length &&
string.Compare(
text,
start,
_entries[i].text,
0,
length,
StringComparison.OrdinalIgnoreCase) == 0)
{
return _entries[i].destination;
}
}
return _exit;
}
}
}
}

View File

@ -0,0 +1,257 @@
// 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;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using Microsoft.AspNetCore.Routing.Template;
using static Microsoft.AspNetCore.Routing.Matchers.DfaMatcher;
namespace Microsoft.AspNetCore.Routing.Matchers
{
internal class DfaMatcherBuilder : MatcherBuilder
{
private List<Entry> _entries = new List<Entry>();
public override void AddEndpoint(MatcherEndpoint endpoint)
{
var parsed = TemplateParser.Parse(endpoint.Template);
_entries.Add(new Entry()
{
Order = 0,
Pattern = parsed,
Precedence = RoutePrecedence.ComputeInbound(parsed),
Endpoint = endpoint,
});
}
public override Matcher Build()
{
Sort(_entries);
var root = new Node() { Depth = -1 };
// We build the tree by doing a BFS over the list of entries. This is important
// because a 'parameter' node can also traverse the same paths that literal nodes traverse.
var maxDepth = 0;
for (var i = 0; i < _entries.Count; i++)
{
maxDepth = Math.Max(maxDepth, _entries[i].Pattern.Segments.Count);
}
for (var depth = 0; depth <= maxDepth; depth++)
{
for (var i = 0; i < _entries.Count; i++)
{
var entry = _entries[i];
if (entry.Pattern.Segments.Count < depth)
{
continue;
}
// Find the parents of this edge at the current depth
var parents = new List<Node>() { root };
for (var j = 0; j < depth; j++)
{
var next = new List<Node>();
for (var k = 0; k < parents.Count; k++)
{
next.Add(Traverse(parents[k], entry.Pattern.Segments[j]));
}
parents = next;
}
if (entry.Pattern.Segments.Count == depth)
{
for (var j = 0; j < parents.Count; j++)
{
var parent = parents[j];
parent.Matches.Add(entry);
}
continue;
}
var segment = entry.Pattern.Segments[depth];
for (var j = 0; j < parents.Count; j++)
{
var parent = parents[j];
if (segment.IsSimple && segment.Parts[0].IsLiteral)
{
if (!parent.Literals.TryGetValue(segment.Parts[0].Text, out var next))
{
next = new Node() { Depth = depth, };
parent.Literals.Add(segment.Parts[0].Text, next);
}
}
else if (segment.IsSimple && segment.Parts[0].IsParameter)
{
if (!parent.Literals.TryGetValue("*", out var next))
{
next = new Node() { Depth = depth, };
parent.Literals.Add("*", next);
}
}
else
{
throw new InvalidOperationException("We only support simple segments.");
}
}
}
}
var states = new List<State>();
var tables = new List<JumpTableBuilder>();
AddNode(root, states, tables);
var exit = states.Count;
states.Add(new State() { IsAccepting = false, Matches = Array.Empty<Candidate>(), });
tables.Add(new JumpTableBuilder() { Exit = exit, });
for (var i = 0; i < tables.Count; i++)
{
if (tables[i].Exit == -1)
{
tables[i].Exit = exit;
}
}
for (var i = 0; i < states.Count; i++)
{
states[i] = new State()
{
IsAccepting = states[i].IsAccepting,
Matches = states[i].Matches,
Transitions = tables[i].Build(),
};
}
return new DfaMatcher(states.ToArray());
}
private Node Traverse(Node node, TemplateSegment segment)
{
if (!segment.IsSimple)
{
throw new InvalidOperationException("We only support simple segments.");
}
if (segment.Parts[0].IsLiteral)
{
return node.Literals[segment.Parts[0].Text];
}
return node.Literals["*"];
}
private static int AddNode(Node node, List<State> states, List<JumpTableBuilder> tables)
{
Sort(node.Matches);
var index = states.Count;
states.Add(new State()
{
Matches = node.Matches.Select(CreateCandidate).ToArray(),
IsAccepting = node.Matches.Count > 0,
});
var table = new JumpTableBuilder() { Depth = node.Depth, };
tables.Add(table);
foreach (var kvp in node.Literals)
{
if (kvp.Key == "*")
{
continue;
}
var transition = AddNode(kvp.Value, states, tables);
table.AddEntry(kvp.Key, transition);
}
var exitIndex = -1;
if (node.Literals.TryGetValue("*", out var exit))
{
exitIndex = AddNode(exit, states, tables);
}
table.Exit = exitIndex;
return index;
}
private static Candidate CreateCandidate(Entry entry)
{
return new Candidate()
{
Endpoint = entry.Endpoint,
Parameters = entry.Pattern.Segments.Select(s => s.IsSimple && s.Parts[0].IsParameter ? s.Parts[0].Name : null).ToArray(),
};
}
private static void Sort(List<Entry> entries)
{
entries.Sort((x, y) =>
{
var comparison = x.Order.CompareTo(y.Order);
if (comparison != 0)
{
return comparison;
}
comparison = x.Precedence.CompareTo(y.Precedence);
if (comparison != 0)
{
return comparison;
}
return x.Pattern.TemplateText.CompareTo(y.Pattern.TemplateText);
});
}
private static Node DeepCopy(Node node)
{
var copy = new Node() { Depth = node.Depth, };
copy.Matches.AddRange(node.Matches);
foreach (var kvp in node.Literals)
{
copy.Literals.Add(kvp.Key, DeepCopy(kvp.Value));
}
return node;
}
private class Entry
{
public int Order;
public decimal Precedence;
public RouteTemplate Pattern;
public Endpoint Endpoint;
}
[DebuggerDisplay("{DebuggerToString(),nq}")]
private class Node
{
public int Depth { get; set; }
public List<Entry> Matches { get; } = new List<Entry>();
public Dictionary<string, Node> Literals { get; } = new Dictionary<string, Node>(StringComparer.OrdinalIgnoreCase);
private string DebuggerToString()
{
var builder = new StringBuilder();
builder.Append("d:");
builder.Append(Depth);
builder.Append(" m:");
builder.Append(Matches.Count);
builder.Append(" c: ");
builder.Append(string.Join(", ", Literals.Select(kvp => $"{kvp.Key}->({kvp.Value.DebuggerToString()})")));
return builder.ToString();
}
}
}
}

View File

@ -0,0 +1,35 @@
// 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;
using System.Threading.Tasks;
using Xunit;
namespace Microsoft.AspNetCore.Routing.Matchers
{
public class DfaMatcherConformanceTest : MatcherConformanceTest
{
// Route values not supported
[Fact]
public override Task Match_SingleParameter_TrailingSlash()
{
return Task.CompletedTask;
}
// Route values not supported
[Theory]
[InlineData(null, null, null, null)]
public override Task Match_MultipleParameters(string template, string path, string[] keys, string[] values)
{
GC.KeepAlive(new object[] { template, path, keys, values });
return Task.CompletedTask;
}
internal override Matcher CreateMatcher(MatcherEndpoint endpoint)
{
var builder = new DfaMatcherBuilder();
builder.AddEndpoint(endpoint);
return builder.Build();
}
}
}

View File

@ -1,6 +1,7 @@
// 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 System.Linq;
using Xunit.Sdk;
@ -13,6 +14,17 @@ namespace Microsoft.AspNetCore.Routing.Matchers
AssertMatch(feature, expected, new RouteValueDictionary());
}
public static void AssertMatch(IEndpointFeature feature, Endpoint expected, string[] keys, string[] values)
{
if (keys.Length != values.Length)
{
throw new XunitException($"Keys and Values must be the same length.");
}
var zipped = keys.Zip(values, (k, v) => new KeyValuePair<string, object>(k, v));
AssertMatch(feature, expected, new RouteValueDictionary(zipped));
}
public static void AssertMatch(IEndpointFeature feature, Endpoint expected, RouteValueDictionary values)
{
if (feature.Endpoint == null)
@ -20,6 +32,11 @@ namespace Microsoft.AspNetCore.Routing.Matchers
throw new XunitException($"Was expected to match '{expected.DisplayName}' but did not match.");
}
if (feature.Values == null)
{
throw new XunitException("Values is null.");
}
if (!object.ReferenceEquals(expected, feature.Endpoint))
{
throw new XunitException(
@ -27,6 +44,8 @@ namespace Microsoft.AspNetCore.Routing.Matchers
$"'{feature.Endpoint.DisplayName}' with values: {FormatRouteValues(feature.Values)}.");
}
// Note: this comparison is intended for unit testing, and is stricter than necessary to make tests
// more precise. Route value comparisons in product code are more flexible than a simple .Equals.
if (values.Count != feature.Values.Count ||
!values.OrderBy(kvp => kvp.Key).SequenceEqual(feature.Values.OrderBy(kvp => kvp.Key)))
{

View File

@ -0,0 +1,239 @@
// 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;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing.Matchers
{
internal class InstructionMatcher : Matcher
{
private State _state;
public InstructionMatcher(Instruction[] instructions, Candidate[] candidates, JumpTable[] tables)
{
_state = new State()
{
Instructions = instructions,
Candidates = candidates,
Tables = tables,
};
}
public unsafe override Task MatchAsync(HttpContext httpContext, IEndpointFeature feature)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
if (feature == null)
{
throw new ArgumentNullException(nameof(feature));
}
var state = _state;
var path = httpContext.Request.Path.Value;
// This section tokenizes the path by marking the sequence of slashes, and their
// position in the string. The consuming code uses the sequence and the count of
// slashes to deduce the length of each segment.
//
// If there is residue (text after last slash) then the length of the segment will
// computed based on the string length.
var buffer = stackalloc Segment[32];
var count = 0;
var start = 1; // PathString guarantees a leading /
var end = 0;
while ((end = path.IndexOf('/', start)) >= 0 && count < 32)
{
buffer[count++] = new Segment() { Start = start, Length = end - start, };
start = end + 1; // resume search after the current character
}
// Residue
if (start < path.Length)
{
buffer[count++] = new Segment() { Start = start, Length = path.Length - start, };
}
var i = 0;
Candidate match = default(Candidate);
while (i < state.Instructions.Length)
{
var instruction = state.Instructions[i];
switch (instruction.Code)
{
case InstructionCode.Accept:
{
if (count == instruction.Depth)
{
match = state.Candidates[instruction.Payload];
}
i++;
break;
}
case InstructionCode.Branch:
{
var table = state.Tables[instruction.Payload];
i = table.GetDestination(buffer, count, path);
break;
}
case InstructionCode.Jump:
{
i = instruction.Payload;
break;
}
}
}
if (match.Endpoint != null)
{
var values = new RouteValueDictionary();
var parameters = match.Parameters;
if (parameters != null)
{
for (var j = 0; j < parameters.Length; j++)
{
var parameter = parameters[j];
if (parameter != null && buffer[j].Length == 0)
{
goto notmatch;
}
else if (parameter != null)
{
var value = path.Substring(buffer[j].Start, buffer[j].Length);
values.Add(parameter, value);
}
}
}
feature.Endpoint = match.Endpoint;
feature.Values = values;
notmatch:;
}
return Task.CompletedTask;
}
public struct Segment
{
public int Start;
public int Length;
}
public struct Candidate
{
public Endpoint Endpoint;
public string[] Parameters;
}
public class State
{
public Candidate[] Candidates;
public Instruction[] Instructions;
public JumpTable[] Tables;
}
[DebuggerDisplay("{ToDebugString(),nq}")]
[StructLayout(LayoutKind.Explicit)]
public struct Instruction
{
[FieldOffset(0)]
public byte Depth;
[FieldOffset(3)]
public InstructionCode Code;
[FieldOffset(4)]
public int Payload;
private string ToDebugString()
{
return $"{Code}: {Payload}";
}
}
public enum InstructionCode : byte
{
Accept,
Branch,
Jump,
Pop, // Only used during the instruction builder phase
}
public abstract class JumpTable
{
public unsafe abstract int GetDestination(Segment* segments, int count, string path);
}
public class JumpTableBuilder
{
private readonly List<(string text, int destination)> _entries = new List<(string text, int destination)>();
public int Depth { get; set; }
public int Exit { get; set; }
public void AddEntry(string text, int destination)
{
_entries.Add((text, destination));
}
public JumpTable Build()
{
return new SimpleJumpTable(Depth, Exit, _entries.ToArray());
}
}
public class SimpleJumpTable : JumpTable
{
private readonly (string text, int destination)[] _entries;
private readonly int _depth;
private readonly int _exit;
public SimpleJumpTable(int depth, int exit, (string text, int destination)[] entries)
{
_depth = depth;
_exit = exit;
_entries = entries;
}
public unsafe override int GetDestination(Segment* segments, int count, string path)
{
if (_depth == count)
{
return _exit;
}
var start = segments[_depth].Start;
var length = segments[_depth].Length;
for (var i = 0; i < _entries.Length; i++)
{
if (length == _entries[i].text.Length &&
string.Compare(
path,
start,
_entries[i].text,
0,
length,
StringComparison.OrdinalIgnoreCase) == 0)
{
return _entries[i].destination;
}
}
return _exit;
}
}
}
}

View File

@ -0,0 +1,356 @@
// 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;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Routing.Template;
using static Microsoft.AspNetCore.Routing.Matchers.InstructionMatcher;
namespace Microsoft.AspNetCore.Routing.Matchers
{
internal class InstructionMatcherBuilder : MatcherBuilder
{
private List<Entry> _entries = new List<Entry>();
public override void AddEndpoint(MatcherEndpoint endpoint)
{
var parsed = TemplateParser.Parse(endpoint.Template);
_entries.Add(new Entry()
{
Order = 0,
Pattern = parsed,
Precedence = RoutePrecedence.ComputeInbound(parsed),
Endpoint = endpoint,
});
}
public override Matcher Build()
{
_entries.Sort((x, y) =>
{
var comparison = x.Order.CompareTo(y.Order);
if (comparison != 0)
{
return comparison;
}
comparison = y.Precedence.CompareTo(x.Precedence);
if (comparison != 0)
{
return comparison;
}
return x.Pattern.TemplateText.CompareTo(y.Pattern.TemplateText);
});
var roots = new List<OrderNode>();
for (var i = 0; i < _entries.Count; i++)
{
var entry = _entries[i];
var parent = (SequenceNode)GetOrCreateRootNode(roots, entry.Order);
var depth = 0;
for (; depth < entry.Pattern.Segments.Count; depth++)
{
var segment = entry.Pattern.Segments[depth];
if (segment.IsSimple && segment.Parts[0].IsLiteral)
{
var branch = parent.GetNode<BranchNode>() ?? parent.AddNode(new BranchNode(depth));
var index = -1;
for (var j = 0; j < branch.Literals.Count; j++)
{
if (string.Equals(segment.Parts[0].Text, branch.Literals[j], StringComparison.OrdinalIgnoreCase))
{
index = j;
break;
}
}
if (index == -1)
{
branch.Literals.Add(segment.Parts[0].Text);
branch.AddNode(new SequenceNode(depth + 1));
index = branch.Children.Count - 1;
}
parent = (SequenceNode)branch.Children[index];
}
else if (segment.IsSimple && segment.Parts[0].IsParameter)
{
var parameter = parent.GetNode<ParameterNode>() ?? parent.AddNode(new ParameterNode(depth));
if (parameter.Children.Count == 0)
{
parameter.AddNode(new SequenceNode(depth + 1));
}
parent = (SequenceNode)parameter.Children[0];
}
else
{
throw new InvalidOperationException("Not implemented!");
}
}
parent.AddNode(new AcceptNode(depth, entry.Endpoint));
}
var builder = new InstructionBuilder();
for (var i = 0; i < roots.Count; i++)
{
roots[i].Lower(builder);
}
var (instructions, endpoints, tables) = builder;
var candidates = new Candidate[endpoints.Length];
for (var i = 0; i < endpoints.Length; i++)
{
candidates[i] = CreateCandidate(endpoints[i]);
}
return new InstructionMatcher(instructions, candidates, tables);
}
private OrderNode GetOrCreateRootNode(List<OrderNode> roots, int order)
{
OrderNode root = null;
for (var j = 0; j < roots.Count; j++)
{
if (roots[j].Order == order)
{
root = roots[j];
break;
}
}
if (root == null)
{
// Nodes are guaranteed to be in order because the entries are in order.
root = new OrderNode(order);
roots.Add(root);
}
return root;
}
private static Candidate CreateCandidate(MatcherEndpoint endpoint)
{
var parsed = TemplateParser.Parse(endpoint.Template);
return new Candidate()
{
Endpoint = endpoint,
Parameters = parsed.Segments.Select(s => s.IsSimple && s.Parts[0].IsParameter ? s.Parts[0].Name : null).ToArray(),
};
}
private class Entry
{
public int Order;
public decimal Precedence;
public RouteTemplate Pattern;
public MatcherEndpoint Endpoint;
}
private class InstructionBuilder
{
private readonly List<Instruction> _instructions = new List<Instruction>();
private readonly List<MatcherEndpoint> _endpoints = new List<MatcherEndpoint>();
private readonly List<JumpTableBuilder> _tables = new List<JumpTableBuilder>();
private readonly List<int> _blocks = new List<int>();
public int Next => _instructions.Count;
public void BeginBlock()
{
_blocks.Add(Next);
}
public void EndBlock()
{
var start = _blocks[_blocks.Count - 1];
var end = Next;
for (var i = start; i < end; i++)
{
if (_instructions[i].Code == InstructionCode.Pop)
{
_instructions[i] = new Instruction()
{
Code = InstructionCode.Jump,
Depth = _instructions[i].Depth,
Payload = end,
};
}
}
_blocks.RemoveAt(_blocks.Count - 1);
}
public int AddInstruction(Instruction instruction)
{
_instructions.Add(instruction);
return _instructions.Count - 1;
}
public int AddEndpoint(MatcherEndpoint endpoint)
{
_endpoints.Add(endpoint);
return _endpoints.Count - 1;
}
public int AddJumpTable(JumpTableBuilder table)
{
_tables.Add(table);
return _tables.Count - 1;
}
public void Deconstruct(
out Instruction[] instructions,
out MatcherEndpoint[] endpoints,
out JumpTable[] tables)
{
instructions = _instructions.ToArray();
endpoints = _endpoints.ToArray();
tables = new JumpTable[_tables.Count];
for (var i = 0; i < _tables.Count; i++)
{
tables[i] = _tables[i].Build();
}
}
}
private abstract class Node
{
public int Depth { get; protected set; }
public List<Node> Children { get; } = new List<Node>();
public abstract void Lower(InstructionBuilder builder);
public TNode GetNode<TNode>() where TNode : Node
{
for (var i = 0; i < Children.Count; i++)
{
if (Children[i] is TNode match)
{
return match;
}
}
return null;
}
public TNode AddNode<TNode>(TNode node) where TNode : Node
{
// We already ordered the routes into precedence order
Children.Add(node);
return node;
}
}
private class SequenceNode : Node
{
public SequenceNode(int depth)
{
Depth = depth;
}
public override void Lower(InstructionBuilder builder)
{
for (var i = 0; i < Children.Count; i++)
{
Children[i].Lower(builder);
}
}
}
private class OrderNode : SequenceNode
{
public OrderNode(int order)
: base(0)
{
Order = order;
}
public int Order { get; }
}
private class BranchNode : Node
{
public BranchNode(int depth)
{
Depth = depth;
}
public List<string> Literals { get; } = new List<string>();
public override void Lower(InstructionBuilder builder)
{
var table = new JumpTableBuilder() { Depth = Depth, };
var index = builder.AddJumpTable(table);
builder.AddInstruction(new Instruction()
{
Code = InstructionCode.Branch,
Depth = (byte)Depth,
Payload = index
});
builder.BeginBlock();
for (var i = 0; i < Children.Count; i++)
{
table.AddEntry(Literals[i], builder.Next);
Children[i].Lower(builder);
builder.AddInstruction(new Instruction()
{
Code = InstructionCode.Pop,
Depth = (byte)Depth,
});
}
builder.EndBlock();
table.Exit = builder.Next;
}
}
private class ParameterNode : Node
{
public ParameterNode(int depth)
{
Depth = depth;
}
public override void Lower(InstructionBuilder builder)
{
for (var i = 0; i < Children.Count; i++)
{
Children[i].Lower(builder);
}
}
}
private class AcceptNode : Node
{
public AcceptNode(int depth, MatcherEndpoint endpoint)
{
Depth = depth;
Endpoint = endpoint;
}
public MatcherEndpoint Endpoint { get; }
public override void Lower(InstructionBuilder builder)
{
builder.AddInstruction(new Instruction()
{
Code = InstructionCode.Accept,
Depth = (byte)Depth,
Payload = builder.AddEndpoint(Endpoint),
});
}
}
}
}

View File

@ -0,0 +1,19 @@
// 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;
using System.Threading.Tasks;
using Xunit;
namespace Microsoft.AspNetCore.Routing.Matchers
{
public class InstructionMatcherConformanceTest : MatcherConformanceTest
{
internal override Matcher CreateMatcher(MatcherEndpoint endpoint)
{
var builder = new InstructionMatcherBuilder();
builder.AddEndpoint(endpoint);
return builder.Build();
}
}
}

View File

@ -5,7 +5,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
{
internal abstract class MatcherBuilder
{
public abstract void AddEntry(string template, MatcherEndpoint endpoint);
public abstract void AddEndpoint(MatcherEndpoint endpoint);
public abstract Matcher Build();
}

View File

@ -2,6 +2,8 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
@ -14,7 +16,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
internal abstract Matcher CreateMatcher(MatcherEndpoint endpoint);
[Fact]
public virtual async Task Match_SingleLiteralSegment_Success()
public virtual async Task Match_SingleLiteralSegment()
{
// Arrange
var (matcher, endpoint) = CreateMatcher("/simple");
@ -27,11 +29,98 @@ namespace Microsoft.AspNetCore.Routing.Matchers
DispatcherAssert.AssertMatch(feature, endpoint);
}
[Fact]
public virtual async Task Match_SingleLiteralSegment_TrailingSlash()
{
// Arrange
var (matcher, endpoint) = CreateMatcher("/simple");
var (httpContext, feature) = CreateContext("/simple/");
// Act
await matcher.MatchAsync(httpContext, feature);
// Assert
DispatcherAssert.AssertMatch(feature, endpoint);
}
[Theory]
[InlineData("/simple")]
[InlineData("/sImpLe")]
[InlineData("/SIMPLE")]
public virtual async Task Match_SingleLiteralSegment_CaseInsensitive(string path)
{
// Arrange
var (matcher, endpoint) = CreateMatcher("/Simple");
var (httpContext, feature) = CreateContext(path);
// Act
await matcher.MatchAsync(httpContext, feature);
// Assert
DispatcherAssert.AssertMatch(feature, endpoint);
}
// Some matchers will optimize for the ASCII case
[Theory]
[InlineData("/SÏmple", "/SÏmple")]
[InlineData("/ab\uD834\uDD1Ecd", "/ab\uD834\uDD1Ecd")] // surrogate pair
public virtual async Task Match_SingleLiteralSegment_Unicode(string template, string path)
{
// Arrange
var (matcher, endpoint) = CreateMatcher(template);
var (httpContext, feature) = CreateContext(path);
// Act
await matcher.MatchAsync(httpContext, feature);
// Assert
DispatcherAssert.AssertMatch(feature, endpoint);
}
// Matchers should operate on the decoded representation - a matcher that calls
// `httpContext.Request.Path.ToString()` will break this test.
[Theory]
[InlineData("/S%mple", "/S%mple")]
[InlineData("/S\\imple", "/S\\imple")] // surrogate pair
public virtual async Task Match_SingleLiteralSegment_PercentEncoded(string template, string path)
{
// Arrange
var (matcher, endpoint) = CreateMatcher(template);
var (httpContext, feature) = CreateContext(path);
// Act
await matcher.MatchAsync(httpContext, feature);
// Assert
DispatcherAssert.AssertMatch(feature, endpoint);
}
[Theory]
[InlineData("/")]
[InlineData("/imple")]
[InlineData("/siple")]
[InlineData("/simple1")]
[InlineData("/simple/not-simple")]
[InlineData("/simple/a/b/c")]
public virtual async Task NotMatch_SingleLiteralSegment(string path)
{
// Arrange
var (matcher, endpoint) = CreateMatcher("/simple");
var (httpContext, feature) = CreateContext(path);
// Act
await matcher.MatchAsync(httpContext, feature);
// Assert
DispatcherAssert.AssertNotMatch(feature);
}
[Theory]
[InlineData("simple")]
[InlineData("/simple")]
[InlineData("~/simple")]
public virtual async Task Match_Sanitizies_TemplatePrefix(string template)
public virtual async Task Match_Sanitizies_Template(string template)
{
// Arrange
var (matcher, endpoint) = CreateMatcher(template);
@ -44,6 +133,159 @@ namespace Microsoft.AspNetCore.Routing.Matchers
DispatcherAssert.AssertMatch(feature, endpoint);
}
// Matchers do their own 'splitting' of the path into segments, so including
// some extra variation here
[Theory]
[InlineData("/a/b", "/a/b")]
[InlineData("/a/b", "/A/B")]
[InlineData("/a/b", "/a/b/")]
[InlineData("/a/b/c", "/a/b/c")]
[InlineData("/a/b/c", "/a/b/c/")]
[InlineData("/a/b/c/d", "/a/b/c/d")]
[InlineData("/a/b/c/d", "/a/b/c/d/")]
public virtual async Task Match_MultipleLiteralSegments(string template, string path)
{
// Arrange
var (matcher, endpoint) = CreateMatcher(template);
var (httpContext, feature) = CreateContext(path);
// Act
await matcher.MatchAsync(httpContext, feature);
// Assert
DispatcherAssert.AssertMatch(feature, endpoint);
}
// Matchers do their own 'splitting' of the path into segments, so including
// some extra variation here
[Theory]
[InlineData("/a/b", "/")]
[InlineData("/a/b", "/a")]
[InlineData("/a/b", "/a/")]
[InlineData("/a/b", "/a//")]
[InlineData("/a/b", "/aa/")]
[InlineData("/a/b", "/a/bb")]
[InlineData("/a/b", "/a/bb/")]
[InlineData("/a/b/c", "/aa/b/c")]
[InlineData("/a/b/c", "/a/bb/c/")]
[InlineData("/a/b/c", "/a/b/cab")]
[InlineData("/a/b/c", "/d/b/c/")]
[InlineData("/a/b/c", "//b/c")]
[InlineData("/a/b/c", "/a/b//")]
[InlineData("/a/b/c", "/a/b/c/d")]
[InlineData("/a/b/c", "/a/b/c/d/e")]
public virtual async Task NotMatch_MultipleLiteralSegments(string template, string path)
{
// Arrange
var (matcher, endpoint) = CreateMatcher(template);
var (httpContext, feature) = CreateContext(path);
// Act
await matcher.MatchAsync(httpContext, feature);
// Assert
DispatcherAssert.AssertNotMatch(feature);
}
[Fact]
public virtual async Task Match_SingleParameter()
{
// Arrange
var (matcher, endpoint) = CreateMatcher("/{p}");
var (httpContext, feature) = CreateContext("/14");
var values = new RouteValueDictionary(new { p = "14", });
// Act
await matcher.MatchAsync(httpContext, feature);
// Assert
DispatcherAssert.AssertMatch(feature, endpoint, values);
}
[Fact]
public virtual async Task Match_SingleParameter_TrailingSlash()
{
// Arrange
var (matcher, endpoint) = CreateMatcher("/{p}");
var (httpContext, feature) = CreateContext("/14/");
var values = new RouteValueDictionary(new { p = "14", });
// Act
await matcher.MatchAsync(httpContext, feature);
// Assert
DispatcherAssert.AssertMatch(feature, endpoint, values);
}
[Theory]
[InlineData("/")]
[InlineData("/a/b")]
[InlineData("/a/b/c")]
[InlineData("//")]
public virtual async Task NotMatch_SingleParameter(string path)
{
// Arrange
var (matcher, endpoint) = CreateMatcher("/{p}");
var (httpContext, feature) = CreateContext(path);
// Act
await matcher.MatchAsync(httpContext, feature);
// Assert
DispatcherAssert.AssertNotMatch(feature);
}
[Theory]
[InlineData("/subscriptions/{subscriptionId}/providers/Microsoft.Insights/metricAlerts", "/subscriptions/foo/providers/Microsoft.Insights/metricAlerts", new string[] { "subscriptionId", }, new string[] { "foo", })]
[InlineData("/{a}/b", "/54/b", new string[] { "a", }, new string[] {"54", })]
[InlineData("/{a}/b", "/54/b/", new string[] { "a", }, new string[] { "54", })]
[InlineData("/{a}/{b}", "/54/73", new string[] { "a", "b" }, new string[] { "54", "73", })]
[InlineData("/a/{b}/c", "/a/b/c", new string[] { "b", }, new string[] { "b", })]
[InlineData("/a/{b}/c/", "/a/b/c", new string[] { "b", }, new string[] { "b", })]
[InlineData("/{a}/b/{c}", "/54/b/c", new string[] { "a", "c", }, new string[] { "54", "c", })]
[InlineData("/{a}/{b}/{c}", "/54/b/c", new string[] { "a", "b", "c", }, new string[] { "54", "b", "c", })]
public virtual async Task Match_MultipleParameters(string template, string path, string[] keys, string[] values)
{
// Arrange
var (matcher, endpoint) = CreateMatcher(template);
var (httpContext, feature) = CreateContext(path);
// Act
await matcher.MatchAsync(httpContext, feature);
// Assert
DispatcherAssert.AssertMatch(feature, endpoint, keys, values);
}
[Theory]
[InlineData("/{a}/b", "/54/bb")]
[InlineData("/{a}/b", "/54/b/17")]
[InlineData("/{a}/b", "/54/b//")]
[InlineData("/{a}/{b}", "//73")]
[InlineData("/{a}/{b}", "/54//")]
[InlineData("/{a}/{b}", "/54/73/18")]
[InlineData("/a/{b}/c", "/aa/b/c")]
[InlineData("/a/{b}/c", "/a/b/cc")]
[InlineData("/a/{b}/c", "/a/b/c/d")]
[InlineData("/{a}/b/{c}", "/54/bb/c")]
[InlineData("/{a}/{b}/{c}", "/54/b/c/d")]
[InlineData("/{a}/{b}/{c}", "/54/b/c//")]
[InlineData("/{a}/{b}/{c}", "//b/c/")]
[InlineData("/{a}/{b}/{c}", "/54//c/")]
[InlineData("/{a}/{b}/{c}", "/54/b//")]
public virtual async Task NotMatch_MultipleParameters(string template, string path)
{
// Arrange
var (matcher, endpoint) = CreateMatcher(template);
var (httpContext, feature) = CreateContext(path);
// Act
await matcher.MatchAsync(httpContext, feature);
// Assert
DispatcherAssert.AssertNotMatch(feature);
}
internal static (HttpContext httpContext, IEndpointFeature feature) CreateContext(string path)
{
var httpContext = new DefaultHttpContext();

View File

@ -0,0 +1,43 @@
// 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;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing.Matchers
{
// This is an adapter to use Route in the conformance tests
internal class RouteMatcher : Matcher
{
private readonly RouteCollection _inner;
internal RouteMatcher(RouteCollection inner)
{
_inner = inner;
}
public async override Task MatchAsync(HttpContext httpContext, IEndpointFeature feature)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
if (feature == null)
{
throw new ArgumentNullException(nameof(feature));
}
var context = new RouteContext(httpContext);
await _inner.RouteAsync(context);
if (context.Handler != null)
{
feature.Values = context.RouteData.Values;
await context.Handler(httpContext);
}
}
}
}

View File

@ -0,0 +1,34 @@
// 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.Threading.Tasks;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Routing.Matchers
{
internal class RouteMatcherBuilder : MatcherBuilder
{
private readonly RouteCollection _routes = new RouteCollection();
private readonly IInlineConstraintResolver _constraintResolver;
public RouteMatcherBuilder()
{
_constraintResolver = new DefaultInlineConstraintResolver(Options.Create(new RouteOptions()));
}
public override void AddEndpoint(MatcherEndpoint endpoint)
{
var handler = new RouteHandler(c =>
{
c.Features.Get<IEndpointFeature>().Endpoint = endpoint;
return Task.CompletedTask;
});
_routes.Add(new Route(handler, endpoint.Template, _constraintResolver));
}
public override Matcher Build()
{
return new RouteMatcher(_routes);
}
}
}

View File

@ -0,0 +1,15 @@
// 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.
namespace Microsoft.AspNetCore.Routing.Matchers
{
public class RouteMatcherConformanceTest : MatcherConformanceTest
{
internal override Matcher CreateMatcher(MatcherEndpoint endpoint)
{
var builder = new RouteMatcherBuilder();
builder.AddEndpoint(endpoint);
return builder.Build();
}
}
}

View File

@ -35,7 +35,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
if (context.Handler != null)
{
httpContext.Features.Get<IEndpointFeature>().Values = context.RouteData.Values;
feature.Values = context.RouteData.Values;
await context.Handler(httpContext);
}
}

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 System.Threading.Tasks;
using Microsoft.AspNetCore.Routing.Internal;
using Microsoft.AspNetCore.Routing.Template;
using Microsoft.AspNetCore.Routing.Tree;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Routing.Matchers
{
internal class TreeRouterMatcherBuilder : MatcherBuilder
{
private readonly TreeRouteBuilder _inner;
public TreeRouterMatcherBuilder()
{
_inner = new TreeRouteBuilder(
NullLoggerFactory.Instance,
new DefaultObjectPool<UriBuildingContext>(new UriBuilderContextPooledObjectPolicy()),
new DefaultInlineConstraintResolver(Options.Create(new RouteOptions())));
}
public override void AddEndpoint(MatcherEndpoint endpoint)
{
var handler = new RouteHandler(c =>
{
var feature = c.Features.Get<IEndpointFeature>();
feature.Endpoint = endpoint;
feature.Invoker = MatcherEndpoint.EmptyInvoker;
return Task.CompletedTask;
});
_inner.MapInbound(handler, TemplateParser.Parse(endpoint.Template), "default", 0);
}
public override Matcher Build()
{
return new TreeRouterMatcher(_inner.Build());
}
}
}

View File

@ -1,38 +1,15 @@
// 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.Threading.Tasks;
using Microsoft.AspNetCore.Routing.Internal;
using Microsoft.AspNetCore.Routing.Template;
using Microsoft.AspNetCore.Routing.Tree;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.Options;
using Xunit;
namespace Microsoft.AspNetCore.Routing.Matchers
{
public class TreeRouterMatcherConformanceTest : MatcherConformanceTest
{
internal override Matcher CreateMatcher(MatcherEndpoint endpoint)
{
var builder = new TreeRouteBuilder(
NullLoggerFactory.Instance,
new DefaultObjectPool<UriBuildingContext>(new UriBuilderContextPooledObjectPolicy()),
new DefaultInlineConstraintResolver(Options.Create(new RouteOptions())));
var handler = new RouteHandler(c =>
{
var feature = c.Features.Get<IEndpointFeature>();
feature.Endpoint = endpoint;
feature.Invoker = MatcherEndpoint.EmptyInvoker;
return Task.CompletedTask;
});
builder.MapInbound(handler, TemplateParser.Parse(endpoint.Template), "default", 0);
return new TreeRouterMatcher(builder.Build());
var builder = new TreeRouterMatcherBuilder();
builder.AddEndpoint(endpoint);
return builder.Build();
}
}
}

View File

@ -5,6 +5,10 @@
<RootNamespace>Microsoft.AspNetCore.Routing</RootNamespace>
</PropertyGroup>
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.Routing\Microsoft.AspNetCore.Routing.csproj" />
</ItemGroup>