Add tree dispatcher

This commit is contained in:
Ryan Nowak 2017-09-26 10:17:48 -07:00
parent eeebefee1b
commit 5fed462123
7 changed files with 424 additions and 28 deletions

View File

@ -28,7 +28,7 @@ namespace DispatcherSample
{
services.Configure<DispatcherOptions>(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<object>(), new { controller = "Home", action = "Index", }, "Home:Index()"),
new SimpleEndpoint(Home_About, Array.Empty<object>(), new { controller = "Home", action = "About", }, "Home:About()"),
new SimpleEndpoint(Admin_Index, Array.Empty<object>(), 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 =
{

View File

@ -60,9 +60,19 @@ namespace Microsoft.AspNetCore.Dispatcher
public IChangeToken ChangeToken => DataSource?.ChangeToken ?? NullChangeToken.Singleton;
IReadOnlyList<Address> IAddressCollectionProvider.Addresses => ((IAddressCollectionProvider)DataSource)?.Addresses ?? _addresses ?? (IReadOnlyList<Address>)Array.Empty<Address>();
IReadOnlyList<Address> IAddressCollectionProvider.Addresses => GetAddresses();
IReadOnlyList<Endpoint> IEndpointCollectionProvider.Endpoints => ((IEndpointCollectionProvider)DataSource)?.Endpoints ?? _endpoints ?? (IReadOnlyList<Endpoint>)Array.Empty<Endpoint>();
IReadOnlyList<Endpoint> IEndpointCollectionProvider.Endpoints => GetEndpoints();
protected virtual IReadOnlyList<Address> GetAddresses()
{
return ((IAddressCollectionProvider)DataSource)?.Addresses ?? _addresses ?? (IReadOnlyList<Address>)Array.Empty<Address>();
}
protected virtual IReadOnlyList<Endpoint> GetEndpoints()
{
return ((IEndpointCollectionProvider)DataSource)?.Endpoints ?? _endpoints ?? (IReadOnlyList<Endpoint>)Array.Empty<Endpoint>();
}
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<bool> TryMatchAsync(HttpContext httpContext);
protected virtual Task<bool> TryMatchAsync(HttpContext httpContext)
{
// By default don't apply any criteria.
return Task.FromResult(true);
}
protected virtual async Task<Endpoint> SelectEndpointAsync(HttpContext httpContext, IEnumerable<Endpoint> endpoints, IEnumerable<EndpointSelector> 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!");
}
}
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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<Cache> _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<IDispatcherFeature>();
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<string, IRouteConstraint> 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<ILogger<TreeDispatcher>>();
}
}
}
private Cache CreateCache()
{
var endpoints = GetEndpoints();
var groups = new Dictionary<Key, List<Endpoint>>();
for (var i = 0; i < endpoints.Count; i++)
{
var endpoint = endpoints[i];
var metadata = endpoint.Metadata.OfType<ITreeDispatcherMetadata>().LastOrDefault();
if (metadata == null)
{
continue;
}
if (!groups.TryGetValue(new Key(metadata.Order, metadata.RouteTemplate), out var group))
{
group = new List<Endpoint>();
groups.Add(new Key(metadata.Order, metadata.RouteTemplate), group);
}
group.Add(endpoint);
}
var entries = new List<InboundRouteEntry>();
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<UrlMatchingTree>();
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<Key>
{
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<UrlMatchingNode>
{
private readonly Stack<UrlMatchingNode> _stack;
private readonly PathTokenizer _tokenizer;
public Treenumerator(UrlMatchingNode root, PathTokenizer tokenizer)
{
_stack = new Stack<UrlMatchingNode>();
_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;
}
}
}
}

View File

@ -52,5 +52,10 @@ namespace Microsoft.AspNetCore.Routing.Tree
/// Gets or sets the <see cref="RouteTemplate"/>.
/// </summary>
public RouteTemplate RouteTemplate { get; set; }
/// <summary>
/// Gets or sets an arbitrary value associated with the entry.
/// </summary>
public object Tag { get; set; }
}
}

View File

@ -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