Add tree dispatcher
This commit is contained in:
parent
eeebefee1b
commit
5fed462123
|
|
@ -28,7 +28,7 @@ namespace DispatcherSample
|
||||||
{
|
{
|
||||||
services.Configure<DispatcherOptions>(options =>
|
services.Configure<DispatcherOptions>(options =>
|
||||||
{
|
{
|
||||||
options.Dispatchers.Add(new RouteTemplateDispatcher("{controller=Home}/{action=Index}/{id?}", ConstraintResolver)
|
options.Dispatchers.Add(new TreeDispatcher()
|
||||||
{
|
{
|
||||||
Addresses =
|
Addresses =
|
||||||
{
|
{
|
||||||
|
|
@ -39,11 +39,11 @@ namespace DispatcherSample
|
||||||
},
|
},
|
||||||
Endpoints =
|
Endpoints =
|
||||||
{
|
{
|
||||||
new SimpleEndpoint(Home_Index, Array.Empty<object>(), new { controller = "Home", action = "Index", }, "Home:Index()"),
|
new SimpleEndpoint(Home_Index, new object[] { new RouteTemplateMetadata("{controller=Home}/{action=Index}/{id?}"), }, new { controller = "Home", action = "Index", }, "Home:Index()"),
|
||||||
new SimpleEndpoint(Home_About, Array.Empty<object>(), new { controller = "Home", action = "About", }, "Home:About()"),
|
new SimpleEndpoint(Home_About, new object[] { new RouteTemplateMetadata("{controller=Home}/{action=Index}/{id?}"), }, new { controller = "Home", action = "About", }, "Home:About()"),
|
||||||
new SimpleEndpoint(Admin_Index, Array.Empty<object>(), new { controller = "Admin", action = "Index", }, "Admin:Index()"),
|
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 HttpMethodMetadata("GET"), new AuthorizationPolicyMetadata("Admin"), }, new { controller = "Admin", action = "Users", }, "Admin:GetUsers()"),
|
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 HttpMethodMetadata("POST"), new AuthorizationPolicyMetadata("Admin"), }, new { controller = "Admin", action = "Users", }, "Admin:EditUsers()"),
|
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 =
|
Selectors =
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -60,9 +60,19 @@ namespace Microsoft.AspNetCore.Dispatcher
|
||||||
|
|
||||||
public IChangeToken ChangeToken => DataSource?.ChangeToken ?? NullChangeToken.Singleton;
|
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)
|
public virtual async Task InvokeAsync(HttpContext httpContext)
|
||||||
{
|
{
|
||||||
|
|
@ -80,26 +90,48 @@ namespace Microsoft.AspNetCore.Dispatcher
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var selectorContext = new EndpointSelectorContext(httpContext, Endpoints.ToList(), Selectors);
|
feature.Endpoint = await SelectEndpointAsync(httpContext, GetEndpoints(), 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!");
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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!");
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Dispatcher;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing.Dispatcher
|
namespace Microsoft.AspNetCore.Routing.Dispatcher
|
||||||
{
|
{
|
||||||
public class RouteTemplateMetadata : IRouteTemplateMetadata
|
public class RouteTemplateMetadata : IRouteTemplateMetadata, ITreeDispatcherMetadata
|
||||||
{
|
{
|
||||||
public RouteTemplateMetadata(string routeTemplate)
|
public RouteTemplateMetadata(string routeTemplate)
|
||||||
: this(routeTemplate, null)
|
: this(routeTemplate, null)
|
||||||
|
|
@ -27,5 +27,7 @@ namespace Microsoft.AspNetCore.Routing.Dispatcher
|
||||||
public string RouteTemplate { get; }
|
public string RouteTemplate { get; }
|
||||||
|
|
||||||
public DispatcherValueCollection Defaults { get; }
|
public DispatcherValueCollection Defaults { get; }
|
||||||
|
|
||||||
|
public int Order { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -52,5 +52,10 @@ namespace Microsoft.AspNetCore.Routing.Tree
|
||||||
/// Gets or sets the <see cref="RouteTemplate"/>.
|
/// Gets or sets the <see cref="RouteTemplate"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public RouteTemplate RouteTemplate { get; set; }
|
public RouteTemplate RouteTemplate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets an arbitrary value associated with the entry.
|
||||||
|
/// </summary>
|
||||||
|
public object Tag { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -268,7 +268,7 @@ namespace Microsoft.AspNetCore.Routing.Tree
|
||||||
OutboundEntries.Clear();
|
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
|
// The url matching tree represents all the routes asociated with a given
|
||||||
// order. Each node in the tree represents all the different categories
|
// order. Each node in the tree represents all the different categories
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue