Skip to content

Commit fd83b30

Browse files
authored
Port TreeMatcher (#488)
Addresses #472
1 parent 3fadca6 commit fd83b30

25 files changed

+1805
-374
lines changed

benchmarks/Microsoft.AspNetCore.Dispatcher.Performance/DispatcherBenchmark.cs

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,11 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5-
using System.Text.Encodings.Web;
5+
using System.Collections.Generic;
66
using System.Threading.Tasks;
77
using BenchmarkDotNet.Attributes;
88
using Microsoft.AspNetCore.Http;
99
using Microsoft.AspNetCore.Routing;
10-
using Microsoft.AspNetCore.Routing.Tree;
11-
using Microsoft.Extensions.Logging.Abstractions;
12-
using Microsoft.Extensions.ObjectPool;
13-
using Microsoft.Extensions.Options;
1410

1511
namespace Microsoft.AspNetCore.Dispatcher.Performance
1612
{
@@ -19,25 +15,25 @@ public class DispatcherBenchmark
1915
private const int NumberOfRequestTypes = 3;
2016
private const int Iterations = 100;
2117

22-
private readonly IRouter _treeRouter;
18+
private readonly IMatcher _treeMatcher;
2319
private readonly RequestEntry[] _requests;
2420

2521
public DispatcherBenchmark()
2622
{
27-
var handler = new RouteHandler((next) => Task.FromResult<object>(null));
28-
29-
var treeBuilder = new TreeRouteBuilder(
30-
NullLoggerFactory.Instance,
31-
new RoutePatternBinderFactory(UrlEncoder.Default, new DefaultObjectPoolProvider()),
32-
new DefaultInlineConstraintResolver(Options.Create(new RouteOptions())));
33-
34-
treeBuilder.MapInbound(handler, Routing.Template.TemplateParser.Parse("api/Widgets"), "default", 0);
35-
treeBuilder.MapInbound(handler, Routing.Template.TemplateParser.Parse("api/Widgets/{id}"), "default", 0);
36-
treeBuilder.MapInbound(handler, Routing.Template.TemplateParser.Parse("api/Widgets/search/{term}"), "default", 0);
37-
treeBuilder.MapInbound(handler, Routing.Template.TemplateParser.Parse("admin/users/{id}"), "default", 0);
38-
treeBuilder.MapInbound(handler, Routing.Template.TemplateParser.Parse("admin/users/{id}/manage"), "default", 0);
23+
var dataSource = new DefaultDispatcherDataSource()
24+
{
25+
Endpoints =
26+
{
27+
new RoutePatternEndpoint("api/Widgets", Benchmark_Delegate),
28+
new RoutePatternEndpoint("api/Widgets/{id}", Benchmark_Delegate),
29+
new RoutePatternEndpoint("api/Widgets/search/{term}", Benchmark_Delegate),
30+
new RoutePatternEndpoint("admin/users/{id}", Benchmark_Delegate),
31+
new RoutePatternEndpoint("admin/users/{id}/manage", Benchmark_Delegate),
32+
},
33+
};
3934

40-
_treeRouter = treeBuilder.Build();
35+
var factory = new TreeMatcherFactory();
36+
_treeMatcher = factory.CreateMatcher(dataSource, new List<EndpointSelector>());
4137

4238
_requests = new RequestEntry[NumberOfRequestTypes];
4339

@@ -64,38 +60,38 @@ public async Task AttributeRouting()
6460
{
6561
for (var j = 0; j < _requests.Length; j++)
6662
{
67-
var context = new RouteContext(_requests[j].HttpContext);
63+
var context = new MatcherContext(_requests[j].HttpContext);
6864

69-
await _treeRouter.RouteAsync(context);
65+
await _treeMatcher.MatchAsync(context);
7066

7167
Verify(context, j);
7268
}
7369
}
7470
}
7571

76-
private void Verify(RouteContext context, int i)
72+
private void Verify(MatcherContext context, int i)
7773
{
7874
if (_requests[i].IsMatch)
7975
{
80-
if (context.Handler == null)
76+
if (context.Endpoint == null)
8177
{
8278
throw new InvalidOperationException($"Failed {i}");
8379
}
8480

8581
var values = _requests[i].Values;
86-
if (values.Count != context.RouteData.Values.Count)
82+
if (values.Count != context.Values.Count)
8783
{
8884
throw new InvalidOperationException($"Failed {i}");
8985
}
9086
}
9187
else
9288
{
93-
if (context.Handler != null)
89+
if (context.Endpoint != null)
9490
{
9591
throw new InvalidOperationException($"Failed {i}");
9692
}
9793

98-
if (context.RouteData.Values.Count != 0)
94+
if (context.Values.Count != 0)
9995
{
10096
throw new InvalidOperationException($"Failed {i}");
10197
}
@@ -108,5 +104,10 @@ private struct RequestEntry
108104
public bool IsMatch;
109105
public RouteValueDictionary Values;
110106
}
107+
108+
private static Task Benchmark_Delegate(HttpContext httpContext)
109+
{
110+
return Task.CompletedTask;
111+
}
111112
}
112113
}

samples/DispatcherSample/Startup.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4-
using System.Linq;
54
using System.Threading.Tasks;
65
using Microsoft.AspNetCore.Builder;
76
using Microsoft.AspNetCore.Dispatcher;
87
using Microsoft.AspNetCore.Hosting;
98
using Microsoft.AspNetCore.Http;
10-
using Microsoft.AspNetCore.Routing.Dispatcher;
119
using Microsoft.Extensions.DependencyInjection;
1210
using Microsoft.Extensions.Logging;
1311

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
namespace Microsoft.AspNetCore.Dispatcher
5+
{
6+
/// <summary>
7+
/// Constrains a dispatcher value parameter to contain only lowercase or uppercase letters A through Z in the English alphabet.
8+
/// </summary>
9+
public class AlphaDispatcherValueConstraint : RegexDispatcherValueConstraint
10+
{
11+
/// <summary>
12+
/// Initializes a new instance of the <see cref="AlphaDispatcherValueConstraint" /> class.
13+
/// </summary>
14+
public AlphaDispatcherValueConstraint() : base(@"^[a-z]*$")
15+
{
16+
}
17+
}
18+
}

src/Microsoft.AspNetCore.Dispatcher/Constraints/DispatcherValueConstraintBuilder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Dispatcher
1010
/// A builder for producing a mapping of keys to <see cref="IDispatcherValueConstraint"/>.
1111
/// </summary>
1212
/// <remarks>
13-
/// <see cref="DispatcherValueConstraintBuilder"/> allows iterative building a set of route constraints, and will
13+
/// <see cref="DispatcherValueConstraintBuilder"/> allows iterative building a set of dispatcher value constraints, and will
1414
/// merge multiple entries for the same key.
1515
/// </remarks>
1616
public class DispatcherValueConstraintBuilder
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Globalization;
6+
7+
namespace Microsoft.AspNetCore.Dispatcher
8+
{
9+
/// <summary>
10+
/// Constrains a dispatcher value parameter to represent only 32-bit integer values.
11+
/// </summary>
12+
public class IntDispatcherValueConstraint : IDispatcherValueConstraint
13+
{
14+
/// <inheritdoc />
15+
public bool Match(DispatcherValueConstraintContext constraintContext)
16+
{
17+
if (constraintContext == null)
18+
{
19+
throw new ArgumentNullException(nameof(constraintContext));
20+
}
21+
22+
if (constraintContext.Values.TryGetValue(constraintContext.Key, out var value) && value != null)
23+
{
24+
if (value is int)
25+
{
26+
return true;
27+
}
28+
29+
var valueString = Convert.ToString(value, CultureInfo.InvariantCulture);
30+
return int.TryParse(valueString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result);
31+
}
32+
33+
return false;
34+
}
35+
}
36+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
namespace Microsoft.AspNetCore.Dispatcher
5+
{
6+
/// <summary>
7+
/// Represents a regex constraint.
8+
/// </summary>
9+
public class RegexStringDispatcherValueConstraint : RegexDispatcherValueConstraint
10+
{
11+
/// <summary>
12+
/// Initializes a new instance of the <see cref="RegexStringDispatcherValueConstraint" /> class.
13+
/// </summary>
14+
/// <param name="regexPattern">The regular expression pattern to match.</param>
15+
public RegexStringDispatcherValueConstraint(string regexPattern)
16+
: base(regexPattern)
17+
{
18+
}
19+
}
20+
}

src/Microsoft.AspNetCore.Dispatcher/DependencyInjection/DispatcherServiceCollectionExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public static IServiceCollection AddDispatcher(this IServiceCollection services)
3636
// Misc Infrastructure
3737
//
3838
services.TryAddSingleton<RoutePatternBinderFactory>();
39+
services.TryAddSingleton<IConstraintFactory, DefaultConstraintFactory>();
3940

4041
services.TryAddEnumerable(ServiceDescriptor.Singleton<IHandlerFactory, RoutePatternEndpointHandlerFactory>());
4142

src/Microsoft.AspNetCore.Dispatcher/DispatcherOptions.cs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,36 @@ public class DispatcherOptions
1010
{
1111
public MatcherCollection Matchers { get; } = new MatcherCollection();
1212

13-
public IDictionary<string, Type> ConstraintMap = new Dictionary<string, Type>();
13+
private IDictionary<string, Type> _constraintTypeMap = GetDefaultConstraintMap();
14+
15+
public IDictionary<string, Type> ConstraintMap
16+
{
17+
get
18+
{
19+
return _constraintTypeMap;
20+
}
21+
set
22+
{
23+
if (value == null)
24+
{
25+
throw new ArgumentNullException(nameof(ConstraintMap));
26+
}
27+
28+
_constraintTypeMap = value;
29+
}
30+
}
31+
32+
private static IDictionary<string, Type> GetDefaultConstraintMap()
33+
{
34+
return new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase)
35+
{
36+
// Type-specific constraints
37+
{ "int", typeof(IntDispatcherValueConstraint) },
38+
39+
//// Regex-based constraints
40+
{ "alpha", typeof(AlphaDispatcherValueConstraint) },
41+
{ "regex", typeof(RegexStringDispatcherValueConstraint) },
42+
};
43+
}
1444
}
1545
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
namespace Microsoft.AspNetCore.Dispatcher
5+
{
6+
public class EndpointOrderMetadata : IEndpointOrderMetadata
7+
{
8+
public EndpointOrderMetadata(int order)
9+
{
10+
Order = order;
11+
}
12+
13+
public int Order { get; }
14+
}
15+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
namespace Microsoft.AspNetCore.Dispatcher
5+
{
6+
public interface IEndpointOrderMetadata
7+
{
8+
int Order { get; }
9+
}
10+
}

src/Microsoft.AspNetCore.Dispatcher/LoggerExtensions.cs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,17 @@ internal static class LoggerExtensions
8383
new EventId(3, "NoEndpointMatchedRequestMethod"),
8484
"No endpoint matched request method '{Method}'.");
8585

86-
// DispatcherValueConstraintMatcher
86+
// TreeMatcher
87+
private static readonly Action<ILogger, string, Exception> _requestShortCircuited = LoggerMessage.Define<string>(
88+
LogLevel.Information,
89+
new EventId(3, "RequestShortCircuited"),
90+
"The current request '{RequestPath}' was short circuited.");
91+
92+
private static readonly Action<ILogger, string, Exception> _matchedRoute = LoggerMessage.Define<string>(
93+
LogLevel.Debug,
94+
1,
95+
"Request successfully matched the route pattern '{RoutePattern}'.");
96+
8797
private static readonly Action<ILogger, object, string, IDispatcherValueConstraint, Exception> _routeValueDoesNotMatchConstraint = LoggerMessage.Define<object, string, IDispatcherValueConstraint>(
8898
LogLevel.Debug,
8999
1,
@@ -98,6 +108,19 @@ public static void RouteValueDoesNotMatchConstraint(
98108
_routeValueDoesNotMatchConstraint(logger, routeValue, routeKey, routeConstraint, null);
99109
}
100110

111+
public static void RequestShortCircuited(this ILogger logger, MatcherContext matcherContext)
112+
{
113+
var requestPath = matcherContext.HttpContext.Request.Path;
114+
_requestShortCircuited(logger, requestPath, null);
115+
}
116+
117+
public static void MatchedRoute(
118+
this ILogger logger,
119+
string routePattern)
120+
{
121+
_matchedRoute(logger, routePattern, null);
122+
}
123+
101124
public static void AmbiguousEndpoints(this ILogger logger, string ambiguousEndpoints)
102125
{
103126
_ambiguousEndpoints(logger, ambiguousEndpoints, null);

src/Microsoft.AspNetCore.Dispatcher/RoutePatternEndpoint.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5-
using System.Collections.Generic;
6-
using System.Linq;
75
using Microsoft.AspNetCore.Http;
86

97
namespace Microsoft.AspNetCore.Dispatcher

0 commit comments

Comments
 (0)