diff --git a/samples/DispatcherSample/Startup.cs b/samples/DispatcherSample/Startup.cs index 448ab9c4d1..0f561a3147 100644 --- a/samples/DispatcherSample/Startup.cs +++ b/samples/DispatcherSample/Startup.cs @@ -28,7 +28,7 @@ namespace DispatcherSample { services.Configure(options => { - options.Dispatchers.Add(new RouteTemplateDispatcher("{controller=Home}/{action=Index}/{id?}", ConstraintResolver) + options.Dispatchers.Add(new TreeDispatcher() { Addresses = { @@ -39,11 +39,11 @@ namespace DispatcherSample }, Endpoints = { - new SimpleEndpoint(Home_Index, Array.Empty(), new { controller = "Home", action = "Index", }, "Home:Index()"), - new SimpleEndpoint(Home_About, Array.Empty(), new { controller = "Home", action = "About", }, "Home:About()"), - new SimpleEndpoint(Admin_Index, Array.Empty(), new { controller = "Admin", action = "Index", }, "Admin:Index()"), - new SimpleEndpoint(Admin_GetUsers, new object[] { new HttpMethodMetadata("GET"), new AuthorizationPolicyMetadata("Admin"), }, new { controller = "Admin", action = "Users", }, "Admin:GetUsers()"), - new SimpleEndpoint(Admin_EditUsers, new object[] { new HttpMethodMetadata("POST"), new AuthorizationPolicyMetadata("Admin"), }, new { controller = "Admin", action = "Users", }, "Admin:EditUsers()"), + new SimpleEndpoint(Home_Index, new object[] { new RouteTemplateMetadata("{controller=Home}/{action=Index}/{id?}"), }, new { controller = "Home", action = "Index", }, "Home:Index()"), + new SimpleEndpoint(Home_About, new object[] { new RouteTemplateMetadata("{controller=Home}/{action=Index}/{id?}"), }, new { controller = "Home", action = "About", }, "Home:About()"), + new SimpleEndpoint(Admin_Index, new object[] { new RouteTemplateMetadata("{controller=Home}/{action=Index}/{id?}"), }, new { controller = "Admin", action = "Index", }, "Admin:Index()"), + new SimpleEndpoint(Admin_GetUsers, new object[] { new RouteTemplateMetadata("{controller=Home}/{action=Index}/{id?}"), new HttpMethodMetadata("GET"), new AuthorizationPolicyMetadata("Admin"), }, new { controller = "Admin", action = "Users", }, "Admin:GetUsers()"), + new SimpleEndpoint(Admin_EditUsers, new object[] { new RouteTemplateMetadata("{controller=Home}/{action=Index}/{id?}"), new HttpMethodMetadata("POST"), new AuthorizationPolicyMetadata("Admin"), }, new { controller = "Admin", action = "Users", }, "Admin:EditUsers()"), }, Selectors = { diff --git a/src/Microsoft.AspNetCore.Dispatcher/DispatcherBase.cs b/src/Microsoft.AspNetCore.Dispatcher/DispatcherBase.cs index e7cedc2abf..5d3ecb2343 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/DispatcherBase.cs +++ b/src/Microsoft.AspNetCore.Dispatcher/DispatcherBase.cs @@ -60,9 +60,19 @@ namespace Microsoft.AspNetCore.Dispatcher public IChangeToken ChangeToken => DataSource?.ChangeToken ?? NullChangeToken.Singleton; - IReadOnlyList
IAddressCollectionProvider.Addresses => ((IAddressCollectionProvider)DataSource)?.Addresses ?? _addresses ?? (IReadOnlyList
)Array.Empty
(); + IReadOnlyList
IAddressCollectionProvider.Addresses => GetAddresses(); - IReadOnlyList IEndpointCollectionProvider.Endpoints => ((IEndpointCollectionProvider)DataSource)?.Endpoints ?? _endpoints ?? (IReadOnlyList)Array.Empty(); + IReadOnlyList IEndpointCollectionProvider.Endpoints => GetEndpoints(); + + protected virtual IReadOnlyList
GetAddresses() + { + return ((IAddressCollectionProvider)DataSource)?.Addresses ?? _addresses ?? (IReadOnlyList
)Array.Empty
(); + } + + protected virtual IReadOnlyList GetEndpoints() + { + return ((IEndpointCollectionProvider)DataSource)?.Endpoints ?? _endpoints ?? (IReadOnlyList)Array.Empty(); + } public virtual async Task InvokeAsync(HttpContext httpContext) { @@ -80,26 +90,48 @@ namespace Microsoft.AspNetCore.Dispatcher return; } - var selectorContext = new EndpointSelectorContext(httpContext, Endpoints.ToList(), Selectors); - await selectorContext.InvokeNextAsync(); - - switch (selectorContext.Endpoints.Count) - { - case 0: - break; - - case 1: - - feature.Endpoint = selectorContext.Endpoints[0]; - break; - - default: - throw new InvalidOperationException("Ambiguous bro!"); - - } + feature.Endpoint = await SelectEndpointAsync(httpContext, GetEndpoints(), Selectors); } } - protected abstract Task TryMatchAsync(HttpContext httpContext); + protected virtual Task TryMatchAsync(HttpContext httpContext) + { + // By default don't apply any criteria. + return Task.FromResult(true); + } + + protected virtual async Task SelectEndpointAsync(HttpContext httpContext, IEnumerable endpoints, IEnumerable selectors) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + if (endpoints == null) + { + throw new ArgumentNullException(nameof(endpoints)); + } + + if (selectors == null) + { + throw new ArgumentNullException(nameof(selectors)); + } + + var selectorContext = new EndpointSelectorContext(httpContext, endpoints.ToList(), selectors.ToList()); + await selectorContext.InvokeNextAsync(); + + switch (selectorContext.Endpoints.Count) + { + case 0: + return null; + + case 1: + return selectorContext.Endpoints[0]; + + default: + throw new InvalidOperationException("Ambiguous bro!"); + + } + } } } diff --git a/src/Microsoft.AspNetCore.Routing/Dispatcher/ITreeDispatcherMetadata.cs b/src/Microsoft.AspNetCore.Routing/Dispatcher/ITreeDispatcherMetadata.cs new file mode 100644 index 0000000000..27ad992905 --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/Dispatcher/ITreeDispatcherMetadata.cs @@ -0,0 +1,12 @@ +// 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.Dispatcher +{ + public interface ITreeDispatcherMetadata + { + int Order { get; } + + string RouteTemplate { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Routing/Dispatcher/RouteTemplateMetadata.cs b/src/Microsoft.AspNetCore.Routing/Dispatcher/RouteTemplateMetadata.cs index d476cb556f..8416c422a7 100644 --- a/src/Microsoft.AspNetCore.Routing/Dispatcher/RouteTemplateMetadata.cs +++ b/src/Microsoft.AspNetCore.Routing/Dispatcher/RouteTemplateMetadata.cs @@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Dispatcher; namespace Microsoft.AspNetCore.Routing.Dispatcher { - public class RouteTemplateMetadata : IRouteTemplateMetadata + public class RouteTemplateMetadata : IRouteTemplateMetadata, ITreeDispatcherMetadata { public RouteTemplateMetadata(string routeTemplate) : this(routeTemplate, null) @@ -27,5 +27,7 @@ namespace Microsoft.AspNetCore.Routing.Dispatcher public string RouteTemplate { get; } public DispatcherValueCollection Defaults { get; } + + public int Order { get; set; } } } diff --git a/src/Microsoft.AspNetCore.Routing/Dispatcher/TreeDispatcher.cs b/src/Microsoft.AspNetCore.Routing/Dispatcher/TreeDispatcher.cs new file mode 100644 index 0000000000..223b7e271e --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/Dispatcher/TreeDispatcher.cs @@ -0,0 +1,345 @@ +// 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; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Dispatcher; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing.Internal; +using Microsoft.AspNetCore.Routing.Logging; +using Microsoft.AspNetCore.Routing.Template; +using Microsoft.AspNetCore.Routing.Tree; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Internal; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Routing.Dispatcher +{ + public class TreeDispatcher : DispatcherBase + { + private bool _dataInitialized; + private bool _servicesInitialized; + private object _lock; + private Cache _cache; + + private readonly Func _initializer; + + private ILogger _logger; + + public TreeDispatcher() + { + _lock = new object(); + _initializer = CreateCache; + } + + public override async Task InvokeAsync(HttpContext httpContext) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + EnsureServicesInitialized(httpContext); + + var cache = LazyInitializer.EnsureInitialized(ref _cache, ref _dataInitialized, ref _lock, _initializer); + + var feature = httpContext.Features.Get(); + var values = feature.Values?.AsRouteValueDictionary() ?? new RouteValueDictionary(); + feature.Values = values; + + for (var i = 0; i < cache.Trees.Length; i++) + { + var tree = cache.Trees[i]; + var tokenizer = new PathTokenizer(httpContext.Request.Path); + + var treenumerator = new Treenumerator(tree.Root, tokenizer); + + while (treenumerator.MoveNext()) + { + var node = treenumerator.Current; + foreach (var item in node.Matches) + { + var entry = item.Entry; + var matcher = item.TemplateMatcher; + + values.Clear(); + if (!matcher.TryMatch(httpContext.Request.Path, values)) + { + continue; + } + + _logger.MatchedRoute(entry.RouteName, entry.RouteTemplate.TemplateText); + + if (!MatchConstraints(httpContext, values, entry.Constraints)) + { + continue; + } + + feature.Endpoint = await SelectEndpointAsync(httpContext, (Endpoint[])entry.Tag, Selectors); + if (feature.Endpoint != null || feature.RequestDelegate != null) + { + return; + } + } + } + } + } + + private bool MatchConstraints(HttpContext httpContext, RouteValueDictionary values, IDictionary constraints) + { + if (constraints != null) + { + foreach (var kvp in constraints) + { + var constraint = kvp.Value; + if (!constraint.Match(httpContext, null, kvp.Key, values, RouteDirection.IncomingRequest)) + { + object value; + values.TryGetValue(kvp.Key, out value); + + _logger.RouteValueDoesNotMatchConstraint(value, kvp.Key, kvp.Value); + return false; + } + } + } + + return true; + } + + private void EnsureServicesInitialized(HttpContext httpContext) + { + if (Volatile.Read(ref _servicesInitialized)) + { + return; + } + + EnsureServicesInitializedSlow(httpContext); + } + + private void EnsureServicesInitializedSlow(HttpContext httpContext) + { + lock (_lock) + { + if (!Volatile.Read(ref _servicesInitialized)) + { + _logger = httpContext.RequestServices.GetRequiredService>(); + } + } + } + + private Cache CreateCache() + { + var endpoints = GetEndpoints(); + + var groups = new Dictionary>(); + + for (var i = 0; i < endpoints.Count; i++) + { + var endpoint = endpoints[i]; + + var metadata = endpoint.Metadata.OfType().LastOrDefault(); + if (metadata == null) + { + continue; + } + + if (!groups.TryGetValue(new Key(metadata.Order, metadata.RouteTemplate), out var group)) + { + group = new List(); + groups.Add(new Key(metadata.Order, metadata.RouteTemplate), group); + } + + group.Add(endpoint); + } + + var entries = new List(); + foreach (var group in groups) + { + var template = TemplateParser.Parse(group.Key.RouteTemplate); + + var defaults = new RouteValueDictionary(); + for (var i = 0; i < template.Parameters.Count; i++) + { + var parameter = template.Parameters[i]; + if (parameter.DefaultValue != null) + { + defaults.Add(parameter.Name, parameter.DefaultValue); + } + } + + entries.Add(new InboundRouteEntry() + { + Defaults = defaults, + Order = group.Key.Order, + Precedence = RoutePrecedence.ComputeInbound(template), + RouteTemplate = template, + Tag = group.Value.ToArray(), + }); + } + + var trees = new List(); + for (var i = 0; i < entries.Count; i++) + { + var entry = entries[i]; + + while (trees.Count <= entry.Order) + { + trees.Add(new UrlMatchingTree(trees.Count)); + } + + var tree = trees[i]; + + TreeRouteBuilder.AddEntryToTree(tree, entry); + } + + return new Cache(trees.ToArray()); + } + + private struct Key : IEquatable + { + public readonly int Order; + public readonly string RouteTemplate; + + public Key(int order, string routeTemplate) + { + Order = order; + RouteTemplate = routeTemplate; + } + + public bool Equals(Key other) + { + return Order == other.Order && string.Equals(RouteTemplate, other.RouteTemplate, StringComparison.OrdinalIgnoreCase); + } + + public override bool Equals(object obj) + { + return obj is Key ? Equals((Key)obj) : false; + } + + public override int GetHashCode() + { + var hash = new HashCodeCombiner(); + hash.Add(Order); + hash.Add(RouteTemplate, StringComparer.OrdinalIgnoreCase); + return hash; + } + } + + private class Cache + { + public readonly UrlMatchingTree[] Trees; + + public Cache(UrlMatchingTree[] trees) + { + Trees = trees; + } + } + + private struct Treenumerator : IEnumerator + { + private readonly Stack _stack; + private readonly PathTokenizer _tokenizer; + + public Treenumerator(UrlMatchingNode root, PathTokenizer tokenizer) + { + _stack = new Stack(); + _tokenizer = tokenizer; + Current = null; + + _stack.Push(root); + } + + public UrlMatchingNode Current { get; private set; } + + object IEnumerator.Current => Current; + + public void Dispose() + { + } + + public bool MoveNext() + { + if (_stack == null) + { + return false; + } + + while (_stack.Count > 0) + { + var next = _stack.Pop(); + + // In case of wild card segment, the request path segment length can be greater + // Example: + // Template: a/{*path} + // Request Url: a/b/c/d + if (next.IsCatchAll && next.Matches.Count > 0) + { + Current = next; + return true; + } + // Next template has the same length as the url we are trying to match + // The only possible matching segments are either our current matches or + // any catch-all segment after this segment in which the catch all is empty. + else if (next.Depth == _tokenizer.Count) + { + if (next.Matches.Count > 0) + { + Current = next; + return true; + } + else + { + // We can stop looking as any other child node from this node will be + // either a literal, a constrained parameter or a parameter. + // (Catch alls and constrained catch alls will show up as candidate matches). + continue; + } + } + + if (next.CatchAlls != null) + { + _stack.Push(next.CatchAlls); + } + + if (next.ConstrainedCatchAlls != null) + { + _stack.Push(next.ConstrainedCatchAlls); + } + + if (next.Parameters != null) + { + _stack.Push(next.Parameters); + } + + if (next.ConstrainedParameters != null) + { + _stack.Push(next.ConstrainedParameters); + } + + if (next.Literals.Count > 0) + { + UrlMatchingNode node; + Debug.Assert(next.Depth < _tokenizer.Count); + if (next.Literals.TryGetValue(_tokenizer[next.Depth].Value, out node)) + { + _stack.Push(node); + } + } + } + + return false; + } + + public void Reset() + { + _stack.Clear(); + Current = null; + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Routing/Tree/InboundRouteEntry.cs b/src/Microsoft.AspNetCore.Routing/Tree/InboundRouteEntry.cs index 7c4a5f0abc..67db0d5ad5 100644 --- a/src/Microsoft.AspNetCore.Routing/Tree/InboundRouteEntry.cs +++ b/src/Microsoft.AspNetCore.Routing/Tree/InboundRouteEntry.cs @@ -52,5 +52,10 @@ namespace Microsoft.AspNetCore.Routing.Tree /// Gets or sets the . /// public RouteTemplate RouteTemplate { get; set; } + + /// + /// Gets or sets an arbitrary value associated with the entry. + /// + public object Tag { get; set; } } } diff --git a/src/Microsoft.AspNetCore.Routing/Tree/TreeRouteBuilder.cs b/src/Microsoft.AspNetCore.Routing/Tree/TreeRouteBuilder.cs index e5f7d80823..a2521c02e4 100644 --- a/src/Microsoft.AspNetCore.Routing/Tree/TreeRouteBuilder.cs +++ b/src/Microsoft.AspNetCore.Routing/Tree/TreeRouteBuilder.cs @@ -268,7 +268,7 @@ namespace Microsoft.AspNetCore.Routing.Tree OutboundEntries.Clear(); } - private void AddEntryToTree(UrlMatchingTree tree, InboundRouteEntry entry) + internal static void AddEntryToTree(UrlMatchingTree tree, InboundRouteEntry entry) { // The url matching tree represents all the routes asociated with a given // order. Each node in the tree represents all the different categories