Changes due to davidfowl feedback

Changed dispatchers to IMatcher, added a context.

Removed a bunch of outdated cruft and updated the sample to use
attribute-routing like entries.
This commit is contained in:
Ryan Nowak 2017-10-04 21:28:39 -07:00
parent 6b3d42f6bd
commit 63d2cc4637
24 changed files with 293 additions and 444 deletions

View File

@ -22,11 +22,10 @@ namespace DispatcherSample
// This is a temporary layering issue, don't worry about :)
services.AddRouting();
services.AddSingleton<RouteTemplateUrlGenerator>();
services.AddSingleton<IDefaultDispatcherFactory, TreeDispatcherFactory>();
services.AddSingleton<IDefaultMatcherFactory, TreeMatcherFactory>();
// Imagine this was done by MVC or another framework.
services.AddSingleton<DispatcherDataSource>(ConfigureDispatcher());
services.AddSingleton<EndpointSelector, TemplateEndpointSelector>();
services.AddSingleton<EndpointSelector, HttpMethodEndpointSelector>();
}
@ -37,18 +36,20 @@ namespace DispatcherSample
{
Addresses =
{
new TemplateAddress("{controller=Home}/{action=Index}/{id?}", new { controller = "Home", action = "Index", }, "Home:Index()"),
new TemplateAddress("{controller=Home}/{action=Index}/{id?}", new { controller = "Home", action = "About", }, "Home:About()"),
new TemplateAddress("{controller=Home}/{action=Index}/{id?}", new { controller = "Admin", action = "Index", }, "Admin:Index()"),
new TemplateAddress("{controller=Home}/{action=Index}/{id?}", new { controller = "Admin", action = "Users", }, "Admin:GetUsers()/Admin:EditUsers()"),
new TemplateAddress("{id?}", new { controller = "Home", action = "Index", }, "Home:Index()"),
new TemplateAddress("Home/About/{id?}", new { controller = "Home", action = "About", }, "Home:About()"),
new TemplateAddress("Admin/Index/{id?}", new { controller = "Admin", action = "Index", }, "Admin:Index()"),
new TemplateAddress("Admin/Users/{id?}", new { controller = "Admin", action = "Users", }, "Admin:GetUsers()/Admin:EditUsers()"),
},
Endpoints =
{
new TemplateEndpoint("{controller=Home}/{action=Index}/{id?}", new { controller = "Home", action = "Index", }, Home_Index, "Home:Index()"),
new TemplateEndpoint("{controller=Home}/{action=Index}/{id?}", new { controller = "Home", action = "About", }, Home_About, "Home:About()"),
new TemplateEndpoint("{controller=Home}/{action=Index}/{id?}", new { controller = "Admin", action = "Index", }, Admin_Index, "Admin:Index()"),
new TemplateEndpoint("{controller=Home}/{action=Index}/{id?}", new { controller = "Admin", action = "Users", }, "GET", Admin_GetUsers, "Admin:GetUsers()", new AuthorizationPolicyMetadata("Admin")),
new TemplateEndpoint("{controller=Home}/{action=Index}/{id?}", new { controller = "Admin", action = "Users", }, "POST", Admin_EditUsers, "Admin:EditUsers()", new AuthorizationPolicyMetadata("Admin")),
new TemplateEndpoint("{id?}", new { controller = "Home", action = "Index", }, Home_Index, "Home:Index()"),
new TemplateEndpoint("Home/{id?}", new { controller = "Home", action = "Index", }, Home_Index, "Home:Index()"),
new TemplateEndpoint("Home/Index/{id?}", new { controller = "Home", action = "Index", }, Home_Index, "Home:Index()"),
new TemplateEndpoint("Home/About/{id?}", new { controller = "Home", action = "About", }, Home_About, "Home:About()"),
new TemplateEndpoint("Admin/Index/{id?}", new { controller = "Admin", action = "Index", }, Admin_Index, "Admin:Index()"),
new TemplateEndpoint("Admin/Users/{id?}", new { controller = "Admin", action = "Users", }, "GET", Admin_GetUsers, "Admin:GetUsers()", new AuthorizationPolicyMetadata("Admin")),
new TemplateEndpoint("Admin/Users/{id?}", new { controller = "Admin", action = "Users", }, "POST", Admin_EditUsers, "Admin:EditUsers()", new AuthorizationPolicyMetadata("Admin")),
},
};
}

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 Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Dispatcher
@ -9,7 +10,7 @@ namespace Microsoft.AspNetCore.Dispatcher
{
Endpoint Endpoint { get; set; }
RequestDelegate RequestDelegate { get; set; }
Func<RequestDelegate, RequestDelegate> Handler { get; set; }
DispatcherValueCollection Values { get; set; }
}

View File

@ -21,10 +21,10 @@ namespace Microsoft.AspNetCore.Dispatcher
_options = options.Value;
_groups = new List<Address>[options.Value.Dispatchers.Count];
for (var i = 0; i < options.Value.Dispatchers.Count; i++)
_groups = new List<Address>[options.Value.Matchers.Count];
for (var i = 0; i < options.Value.Matchers.Count; i++)
{
_groups[i] = new List<Address>(options.Value.Dispatchers[i].AddressProvider?.Addresses ?? Array.Empty<Address>());
_groups[i] = new List<Address>(options.Value.Matchers[i].AddressProvider?.Addresses ?? Array.Empty<Address>());
}
}

View File

@ -10,12 +10,12 @@ namespace Microsoft.AspNetCore.Dispatcher
internal class DefaultDispatcherConfigureOptions : IConfigureOptions<DispatcherOptions>
{
private readonly IEnumerable<DispatcherDataSource> _dataSources;
private readonly IDefaultDispatcherFactory _dispatcherFactory;
private readonly IDefaultMatcherFactory _dispatcherFactory;
private readonly IEnumerable<EndpointSelector> _endpointSelectors;
private readonly IEnumerable<EndpointHandlerFactoryBase> _handlerFactories;
public DefaultDispatcherConfigureOptions(
IDefaultDispatcherFactory dispatcherFactory,
IDefaultMatcherFactory dispatcherFactory,
IEnumerable<DispatcherDataSource> dataSources,
IEnumerable<EndpointSelector> endpointSelectors,
IEnumerable<EndpointHandlerFactoryBase> handlerFactories)
@ -33,7 +33,7 @@ namespace Microsoft.AspNetCore.Dispatcher
throw new ArgumentNullException(nameof(options));
}
options.Dispatchers.Add(_dispatcherFactory.CreateDispatcher(new CompositeDispatcherDataSource(_dataSources), _endpointSelectors));
options.Matchers.Add(_dispatcherFactory.CreateDispatcher(new CompositeDispatcherDataSource(_dataSources), _endpointSelectors));
foreach (var handlerFactory in _handlerFactories)
{

View File

@ -1,40 +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.ObjectModel;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Dispatcher
{
public class DispatcherCollection : Collection<DispatcherEntry>
{
public void Add(DispatcherBase dispatcher)
{
if (dispatcher == null)
{
throw new ArgumentNullException(nameof(dispatcher));
}
Add(new DispatcherEntry()
{
Dispatcher = dispatcher.InvokeAsync,
AddressProvider = dispatcher,
EndpointProvider = dispatcher,
});
}
public void Add(RequestDelegate dispatcher)
{
if (dispatcher == null)
{
throw new ArgumentNullException(nameof(dispatcher));
}
Add(new DispatcherEntry()
{
Dispatcher = dispatcher,
});
}
}
}

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 Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Dispatcher
@ -9,7 +10,7 @@ namespace Microsoft.AspNetCore.Dispatcher
{
public Endpoint Endpoint { get; set; }
public RequestDelegate RequestDelegate { get; set; }
public Func<RequestDelegate, RequestDelegate> Handler { get; set; }
public DispatcherValueCollection Values { get; set; }
}

View File

@ -31,9 +31,9 @@ namespace Microsoft.AspNetCore.Dispatcher
_ambiguousEndpoints(logger, ambiguousEndpoints, null);
}
public static void EndpointMatched(this ILogger logger, string endpointName)
public static void EndpointMatched(this ILogger logger, Endpoint endpoint)
{
_endpointMatched(logger, endpointName ?? "Unnamed endpoint", null);
_endpointMatched(logger, endpoint.DisplayName ?? "Unnamed endpoint", null);
}
public static void NoEndpointsMatched(this ILogger logger, PathString pathString)

View File

@ -31,7 +31,7 @@ namespace Microsoft.AspNetCore.Dispatcher
{
throw new ArgumentNullException(nameof(next));
}
_options = options.Value;
_logger = logger;
_next = next;
@ -42,12 +42,38 @@ namespace Microsoft.AspNetCore.Dispatcher
var feature = new DispatcherFeature();
httpContext.Features.Set<IDispatcherFeature>(feature);
foreach (var entry in _options.Dispatchers)
var context = new MatcherContext(httpContext);
foreach (var entry in _options.Matchers)
{
await entry.Dispatcher(httpContext);
if (feature.Endpoint != null || feature.RequestDelegate != null)
await entry.Matcher.MatchAsync(context);
if (context.ShortCircuit != null)
{
_logger.LogInformation("Matched endpoint {Endpoint}", feature.Endpoint.DisplayName);
feature.Endpoint = context.Endpoint;
feature.Values = context.Values;
await context.ShortCircuit(httpContext);
return;
}
if (context.Endpoint != null)
{
_logger.LogInformation("Matched endpoint {Endpoint}", context.Endpoint.DisplayName);
feature.Endpoint = context.Endpoint;
feature.Values = context.Values;
// Associate this with the DispatcherEntry, not global
for (var i = 0; i < _options.HandlerFactories.Count; i++)
{
var middleware = _options.HandlerFactories[i](feature.Endpoint);
if (middleware != null)
{
feature.Handler = middleware;
break;
}
}
break;
}
}

View File

@ -7,7 +7,7 @@ namespace Microsoft.AspNetCore.Dispatcher
{
public class DispatcherOptions
{
public DispatcherCollection Dispatchers { get; } = new DispatcherCollection();
public MatcherCollection Matchers { get; } = new MatcherCollection();
public IList<EndpointHandlerFactory> HandlerFactories { get; } = new List<EndpointHandlerFactory>();
}

View File

@ -37,28 +37,15 @@ namespace Microsoft.AspNetCore.Dispatcher
_next = next;
}
public async Task Invoke(HttpContext context)
public async Task Invoke(HttpContext httpContext)
{
var feature = context.Features.Get<IDispatcherFeature>();
if (feature.Endpoint != null && feature.RequestDelegate == null)
{
for (var i = 0; i < _options.HandlerFactories.Count; i++)
{
var handler = _options.HandlerFactories[i](feature.Endpoint);
if (handler != null)
{
feature.RequestDelegate = handler(_next);
break;
}
}
}
if (feature.RequestDelegate != null)
var feature = httpContext.Features.Get<IDispatcherFeature>();
if (feature.Handler != null)
{
_logger.LogInformation("Executing endpoint {Endpoint}", feature.Endpoint.DisplayName);
try
{
await feature.RequestDelegate(context);
await feature.Handler(_next)(httpContext);
}
finally
{
@ -68,7 +55,7 @@ namespace Microsoft.AspNetCore.Dispatcher
return;
}
await _next(context);
await _next(httpContext);
}
}
}

View File

@ -13,13 +13,18 @@ namespace Microsoft.AspNetCore.Dispatcher
{
private int _index;
public EndpointSelectorContext(HttpContext httpContext, IList<Endpoint> endpoints, IList<EndpointSelector> selectors)
public EndpointSelectorContext(HttpContext httpContext, DispatcherValueCollection values, IList<Endpoint> endpoints, IList<EndpointSelector> selectors)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
if (values == null)
{
throw new ArgumentNullException(nameof(values));
}
if (endpoints == null)
{
throw new ArgumentNullException(nameof(endpoints));
@ -31,6 +36,7 @@ namespace Microsoft.AspNetCore.Dispatcher
}
HttpContext = httpContext;
Values = values;
Endpoints = endpoints;
Selectors = selectors;
}
@ -41,6 +47,10 @@ namespace Microsoft.AspNetCore.Dispatcher
public IList<EndpointSelector> Selectors { get; }
public RequestDelegate ShortCircuit { get; set; }
public DispatcherValueCollection Values { get; }
public Task InvokeNextAsync()
{
if (_index >= Selectors.Count)

View File

@ -5,8 +5,8 @@ using System.Collections.Generic;
namespace Microsoft.AspNetCore.Dispatcher
{
public interface IDefaultDispatcherFactory
public interface IDefaultMatcherFactory
{
DispatcherEntry CreateDispatcher(DispatcherDataSource dataSource, IEnumerable<EndpointSelector> endpointSelectors);
MatcherEntry CreateDispatcher(DispatcherDataSource dataSource, IEnumerable<EndpointSelector> endpointSelectors);
}
}

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.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Dispatcher
{
/// <summary>
/// An interface for components that can select an <see cref="Endpoint"/> given the current request, as part
/// of the execution of <see cref="DispatcherMiddleware"/>.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="IMatcher"/> implementations can also optionally implement the <see cref="IEndpointCollectionProvider"/>
/// and <see cref="IAddressCollectionProvider"/> interfaces to provide addition information.
/// </para>
/// <para>
/// Use <see cref="DispatcherOptions"/> to register instances of <see cref="IMatcher"/> that will be used by the
/// <see cref="DispatcherMiddleware"/>.
/// </para>
/// </remarks>
public interface IMatcher
{
/// <summary>
/// Attempts to asynchronously select an <see cref="Endpoint"/> for the current request.
/// </summary>
/// <param name="context">The <see cref="MatcherContext"/> associated with the current request.</param>
/// <returns>A <see cref="Task"/> which represents the asynchronous completion of the operation.</returns>
/// <remarks>
/// <para>
/// An implementation should use data from the current request (<see cref="MatcherContext.HttpContext"/>) to select the
/// <see cref="Endpoint"/> and set <see cref="MatcherContext.Endpoint"/> and optionally <see cref="MatcherContext.Values"/>
/// to indicate a successful result.
/// </para>
/// <para>
/// If the matcher encounters an immediate failure condition, the implementation should set
/// <see cref="MatcherContext.ShortCircuit"/> to a <see cref="RequestDelegate"/> that will respond to the current request.
/// </para>
/// </remarks>
Task MatchAsync(MatcherContext context);
}
}

View File

@ -14,23 +14,22 @@ using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Dispatcher
{
public abstract class DispatcherBase : IAddressCollectionProvider, IEndpointCollectionProvider
public abstract class MatcherBase : IMatcher, IAddressCollectionProvider, IEndpointCollectionProvider
{
private List<Address> _addresses;
private List<Endpoint> _endpoints;
private List<EndpointSelector> _endpointSelectors;
private object _initialize;
private bool _selectorsInitialized;
private readonly Func<object> _initializer;
private object _lock;
private bool _servicesInitialized;
private bool _selectorsInitialized;
private readonly Func<object> _selectorInitializer;
public DispatcherBase()
public MatcherBase()
{
_lock = new object();
_initializer = InitializeSelectors;
_selectorInitializer = InitializeSelectors;
}
protected ILogger Logger { get; private set; }
@ -92,39 +91,37 @@ namespace Microsoft.AspNetCore.Dispatcher
return ((IEndpointCollectionProvider)DataSource)?.Endpoints ?? _endpoints ?? (IReadOnlyList<Endpoint>)Array.Empty<Endpoint>();
}
public virtual async Task InvokeAsync(HttpContext httpContext)
public virtual async Task MatchAsync(MatcherContext context)
{
if (httpContext == null)
if (context == null)
{
throw new ArgumentNullException(nameof(httpContext));
throw new ArgumentNullException(nameof(context));
}
EnsureServicesInitialized(httpContext);
EnsureServicesInitialized(context);
var feature = httpContext.Features.Get<IDispatcherFeature>();
if (await TryMatchAsync(httpContext))
context.Values = await MatchRequestAsync(context.HttpContext);
if (context.Values != null)
{
if (feature.RequestDelegate != null)
{
// Short circuit, no need to select an endpoint.
return;
}
feature.Endpoint = await SelectEndpointAsync(httpContext, GetEndpoints(), Selectors);
await SelectEndpointAsync(context, GetEndpoints());
}
}
protected virtual Task<bool> TryMatchAsync(HttpContext httpContext)
protected virtual Task<DispatcherValueCollection> MatchRequestAsync(HttpContext httpContext)
{
// By default don't apply any criteria.
return Task.FromResult(true);
// By default don't apply any criteria or provide any values.
return Task.FromResult(new DispatcherValueCollection());
}
protected virtual async Task<Endpoint> SelectEndpointAsync(HttpContext httpContext, IEnumerable<Endpoint> endpoints, IEnumerable<EndpointSelector> selectors)
protected virtual void InitializeServices(IServiceProvider services)
{
if (httpContext == null)
}
protected virtual async Task SelectEndpointAsync(MatcherContext context, IEnumerable<Endpoint> endpoints)
{
if (context == null)
{
throw new ArgumentNullException(nameof(httpContext));
throw new ArgumentNullException(nameof(context));
}
if (endpoints == null)
@ -132,25 +129,28 @@ namespace Microsoft.AspNetCore.Dispatcher
throw new ArgumentNullException(nameof(endpoints));
}
if (selectors == null)
{
throw new ArgumentNullException(nameof(selectors));
}
EnsureSelectorsInitialized();
LazyInitializer.EnsureInitialized(ref _initialize, ref _selectorsInitialized, ref _lock, _initializer);
var selectorContext = new EndpointSelectorContext(httpContext, endpoints.ToList(), selectors.ToList());
var selectorContext = new EndpointSelectorContext(context.HttpContext, context.Values, endpoints.ToList(), Selectors.ToList());
await selectorContext.InvokeNextAsync();
if (selectorContext.ShortCircuit != null)
{
context.ShortCircuit = selectorContext.ShortCircuit;
return;
}
switch (selectorContext.Endpoints.Count)
{
case 0:
Logger.NoEndpointsMatched(httpContext.Request.Path);
return null;
Logger.NoEndpointsMatched(context.HttpContext.Request.Path);
return;
case 1:
Logger.EndpointMatched(selectorContext.Endpoints[0].DisplayName);
return selectorContext.Endpoints[0];
context.Endpoint = selectorContext.Endpoints[0];
Logger.EndpointMatched(context.Endpoint);
return;
default:
var endpointNames = string.Join(
@ -167,6 +167,12 @@ namespace Microsoft.AspNetCore.Dispatcher
}
}
protected void EnsureSelectorsInitialized()
{
object _ = null;
LazyInitializer.EnsureInitialized(ref _, ref _selectorsInitialized, ref _lock, _selectorInitializer);
}
private object InitializeSelectors()
{
foreach (var selector in Selectors)
@ -177,23 +183,25 @@ namespace Microsoft.AspNetCore.Dispatcher
return null;
}
protected void EnsureServicesInitialized(HttpContext httpContext)
protected void EnsureServicesInitialized(MatcherContext context)
{
if (Volatile.Read(ref _servicesInitialized))
{
return;
}
EnsureServicesInitializedSlow(httpContext);
EnsureServicesInitializedSlow(context);
}
private void EnsureServicesInitializedSlow(HttpContext httpContext)
private void EnsureServicesInitializedSlow(MatcherContext context)
{
lock (_lock)
{
if (!Volatile.Read(ref _servicesInitialized))
{
Logger = httpContext.RequestServices.GetRequiredService<ILoggerFactory>().CreateLogger(GetType());
var services = context.HttpContext.RequestServices;
Logger = services.GetRequiredService<ILoggerFactory>().CreateLogger(GetType());
InitializeServices(services);
}
}
}

View File

@ -0,0 +1,39 @@
// 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.ObjectModel;
namespace Microsoft.AspNetCore.Dispatcher
{
public class MatcherCollection : Collection<MatcherEntry>
{
public void Add(MatcherBase matcher)
{
if (matcher == null)
{
throw new ArgumentNullException(nameof(matcher));
}
Add(new MatcherEntry()
{
Matcher = matcher,
AddressProvider = matcher,
EndpointProvider = matcher,
});
}
public void Add(IMatcher matcher)
{
if (matcher == null)
{
throw new ArgumentNullException(nameof(matcher));
}
Add(new MatcherEntry()
{
Matcher = matcher,
});
}
}
}

View File

@ -0,0 +1,48 @@
// 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 Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Dispatcher
{
/// <summary>
/// A context object for <see cref="IMatcher.MatchAsync(MatcherContext)"/>.
/// </summary>
public class MatcherContext
{
/// <summary>
/// Creates a new <see cref="MatcherContext"/> for the current request.
/// </summary>
/// <param name="httpContext">The <see cref="Http.HttpContext"/> associated with the current request.</param>
public MatcherContext(HttpContext httpContext)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
HttpContext = httpContext;
}
/// <summary>
/// Gets the <see cref="Http.HttpContext"/> associated with the current request.
/// </summary>
public HttpContext HttpContext { get; }
/// <summary>
/// Gets or sets the <see cref="Endpoint"/> selected by the matcher.
/// </summary>
public Endpoint Endpoint { get; set; }
/// <summary>
/// Gets or sets a short-circuit delegate provided by the matcher.
/// </summary>
public RequestDelegate ShortCircuit { get; set; }
/// <summary>
/// Gets or sets a <see cref="DispatcherValueCollection"/> provided by the matcher.
/// </summary>
public DispatcherValueCollection Values { get; set; }
}
}

View File

@ -1,13 +1,11 @@
// 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 Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Dispatcher
{
public class DispatcherEntry
public class MatcherEntry
{
public RequestDelegate Dispatcher { get; set; }
public IMatcher Matcher { get; set; }
public IAddressCollectionProvider AddressProvider { get; set; }

View File

@ -1,55 +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;
namespace Microsoft.AspNetCore.Dispatcher
{
public class TemplateEndpointSelector : EndpointSelector
{
public override Task SelectAsync(EndpointSelectorContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var dispatcherFeature = context.HttpContext.Features.Get<IDispatcherFeature>();
for (var i = context.Endpoints.Count - 1; i >= 0; i--)
{
var endpoint = context.Endpoints[i] as ITemplateEndpoint;
if (!CompareRouteValues(dispatcherFeature.Values, endpoint.Values))
{
context.Endpoints.RemoveAt(i);
}
}
return context.InvokeNextAsync();
}
private bool CompareRouteValues(DispatcherValueCollection values, DispatcherValueCollection requiredValues)
{
foreach (var kvp in requiredValues)
{
if (string.IsNullOrEmpty(kvp.Value.ToString()))
{
if (values.TryGetValue(kvp.Key, out var routeValue) && !string.IsNullOrEmpty(routeValue.ToString()))
{
return false;
}
}
else
{
if (!values.TryGetValue(kvp.Key, out var routeValue) || !string.Equals(kvp.Value.ToString(), routeValue.ToString(), StringComparison.OrdinalIgnoreCase))
{
return false;
}
}
}
return true;
}
}
}

View File

@ -1,157 +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.Dispatcher;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Template;
namespace Microsoft.AspNetCore.Routing.Dispatcher
{
public class RouteTemplateDispatcher : DispatcherBase
{
private readonly IDictionary<string, IRouteConstraint> _constraints;
private readonly RouteValueDictionary _defaults;
private readonly TemplateMatcher _matcher;
private readonly RouteTemplate _parsedTemplate;
public RouteTemplateDispatcher(
string routeTemplate,
IInlineConstraintResolver constraintResolver)
: this(routeTemplate, constraintResolver, null, null)
{
}
public RouteTemplateDispatcher(
string routeTemplate,
IInlineConstraintResolver constraintResolver,
RouteValueDictionary defaults)
: this(routeTemplate, constraintResolver, defaults, null)
{
}
public RouteTemplateDispatcher(
string routeTemplate,
IInlineConstraintResolver constraintResolver,
RouteValueDictionary defaults,
IDictionary<string, object> constraints)
{
if (routeTemplate == null)
{
throw new ArgumentNullException(nameof(routeTemplate));
}
if (constraintResolver == null)
{
throw new ArgumentNullException(nameof(constraintResolver));
}
RouteTemplate = routeTemplate;
try
{
// Data we parse from the template will be used to fill in the rest of the constraints or
// defaults. The parser will throw for invalid routes.
_parsedTemplate = TemplateParser.Parse(routeTemplate);
_constraints = GetConstraints(constraintResolver, _parsedTemplate, constraints);
_defaults = GetDefaults(_parsedTemplate, defaults);
}
catch (Exception exception)
{
throw new RouteCreationException(Resources.FormatTemplateRoute_Exception(string.Empty, routeTemplate), exception);
}
_matcher = new TemplateMatcher(_parsedTemplate, _defaults);
}
public string RouteTemplate { get; }
protected override Task<bool> TryMatchAsync(HttpContext httpContext)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
var feature = httpContext.Features.Get<IDispatcherFeature>();
feature.Values = feature.Values ?? new RouteValueDictionary();
if (!_matcher.TryMatch(httpContext.Request.Path, (RouteValueDictionary)feature.Values))
{
// If we got back a null value set, that means the URI did not match
return Task.FromResult(false);
}
foreach (var kvp in _constraints)
{
var constraint = kvp.Value;
if (!constraint.Match(httpContext, null, kvp.Key, (RouteValueDictionary)feature.Values, RouteDirection.IncomingRequest))
{
return Task.FromResult(false);
}
}
return Task.FromResult(true);
}
private static IDictionary<string, IRouteConstraint> GetConstraints(
IInlineConstraintResolver inlineConstraintResolver,
RouteTemplate parsedTemplate,
IDictionary<string, object> constraints)
{
var constraintBuilder = new RouteConstraintBuilder(inlineConstraintResolver, parsedTemplate.TemplateText);
if (constraints != null)
{
foreach (var kvp in constraints)
{
constraintBuilder.AddConstraint(kvp.Key, kvp.Value);
}
}
foreach (var parameter in parsedTemplate.Parameters)
{
if (parameter.IsOptional)
{
constraintBuilder.SetOptional(parameter.Name);
}
foreach (var inlineConstraint in parameter.InlineConstraints)
{
constraintBuilder.AddResolvedConstraint(parameter.Name, inlineConstraint.Constraint);
}
}
return constraintBuilder.Build();
}
private static RouteValueDictionary GetDefaults(
RouteTemplate parsedTemplate,
RouteValueDictionary defaults)
{
var result = defaults == null ? new RouteValueDictionary() : new RouteValueDictionary(defaults);
foreach (var parameter in parsedTemplate.Parameters)
{
if (parameter.DefaultValue != null)
{
if (result.ContainsKey(parameter.Name))
{
throw new InvalidOperationException(
Resources.FormatTemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly(
parameter.Name));
}
else
{
result.Add(parameter.Name, parameter.DefaultValue);
}
}
}
return result;
}
}
}

View File

@ -1,72 +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.Dispatcher;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing.Dispatcher
{
/// <summary>
/// An adapter to plug an <see cref="IRouter"/> into a dispatcher.
/// </summary>
public class RouterDispatcher : DispatcherBase
{
private readonly Endpoint _fallbackEndpoint;
private readonly IRouter _router;
public RouterDispatcher(IRouter router)
{
if (router == null)
{
throw new ArgumentNullException(nameof(router));
}
_router = router;
_fallbackEndpoint = new UnknownEndpoint(_router);
}
protected override async Task<bool> TryMatchAsync(HttpContext httpContext)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
var routeContext = new RouteContext(httpContext);
await _router.RouteAsync(routeContext);
var feature = httpContext.Features.Get<IDispatcherFeature>();
if (routeContext.Handler == null)
{
// The route did not match, clear everything as it may have been set by the route.
feature.Endpoint = null;
feature.RequestDelegate = null;
feature.Values = null;
return false;
}
else
{
feature.Endpoint = feature.Endpoint ?? _fallbackEndpoint;
feature.RequestDelegate = routeContext.Handler;
feature.Values = routeContext.RouteData.Values;
return true;
}
}
private class UnknownEndpoint : Endpoint
{
public UnknownEndpoint(IRouter router)
{
DisplayName = $"Endpoint for '{router}";
}
public override string DisplayName { get; }
public override IReadOnlyList<object> Metadata => Array.Empty<object>();
}
}
}

View File

@ -5,7 +5,6 @@ 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;
@ -15,11 +14,10 @@ using Microsoft.AspNetCore.Routing.Logging;
using Microsoft.AspNetCore.Routing.Template;
using Microsoft.AspNetCore.Routing.Tree;
using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Routing.Dispatcher
{
public class TreeDispatcher : DispatcherBase
public class TreeMatcher : MatcherBase
{
private bool _dataInitialized;
private object _lock;
@ -27,31 +25,30 @@ namespace Microsoft.AspNetCore.Routing.Dispatcher
private readonly Func<Cache> _initializer;
public TreeDispatcher()
public TreeMatcher()
{
_lock = new object();
_initializer = CreateCache;
}
public override async Task InvokeAsync(HttpContext httpContext)
public override async Task MatchAsync(MatcherContext context)
{
if (httpContext == null)
if (context == null)
{
throw new ArgumentNullException(nameof(httpContext));
throw new ArgumentNullException(nameof(context));
}
EnsureServicesInitialized(httpContext);
EnsureServicesInitialized(context);
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;
var values = new RouteValueDictionary();
context.Values = values;
for (var i = 0; i < cache.Trees.Length; i++)
{
var tree = cache.Trees[i];
var tokenizer = new PathTokenizer(httpContext.Request.Path);
var tokenizer = new PathTokenizer(context.HttpContext.Request.Path);
var treenumerator = new Treenumerator(tree.Root, tokenizer);
@ -64,23 +61,36 @@ namespace Microsoft.AspNetCore.Routing.Dispatcher
var matcher = item.TemplateMatcher;
values.Clear();
if (!matcher.TryMatch(httpContext.Request.Path, values))
if (!matcher.TryMatch(context.HttpContext.Request.Path, values))
{
continue;
}
Logger.MatchedRoute(entry.RouteName, entry.RouteTemplate.TemplateText);
if (!MatchConstraints(httpContext, values, entry.Constraints))
if (!MatchConstraints(context.HttpContext, values, entry.Constraints))
{
continue;
}
feature.Endpoint = await SelectEndpointAsync(httpContext, (Endpoint[])entry.Tag, Selectors);
if (feature.Endpoint != null || feature.RequestDelegate != null)
await SelectEndpointAsync(context, (Endpoint[])entry.Tag);
if (context.ShortCircuit != null)
{
return;
}
if (context.Endpoint != null)
{
if (context.Endpoint is ITemplateEndpoint templateEndpoint)
{
foreach (var kvp in templateEndpoint.Values)
{
context.Values[kvp.Key] = kvp.Value;
}
}
return;
}
}
}
}

View File

@ -7,30 +7,30 @@ using Microsoft.AspNetCore.Dispatcher;
namespace Microsoft.AspNetCore.Routing.Dispatcher
{
public class TreeDispatcherFactory : IDefaultDispatcherFactory
public class TreeMatcherFactory : IDefaultMatcherFactory
{
public DispatcherEntry CreateDispatcher(DispatcherDataSource dataSource, IEnumerable<EndpointSelector> endpointSelectors)
public MatcherEntry CreateDispatcher(DispatcherDataSource dataSource, IEnumerable<EndpointSelector> endpointSelectors)
{
if (dataSource == null)
{
throw new ArgumentNullException(nameof(dataSource));
}
var dispatcher = new TreeDispatcher()
var matcher = new TreeMatcher()
{
DataSource = dataSource,
};
foreach (var endpointSelector in endpointSelectors)
{
dispatcher.Selectors.Add(endpointSelector);
matcher.Selectors.Add(endpointSelector);
}
return new DispatcherEntry()
return new MatcherEntry()
{
AddressProvider = dispatcher,
Dispatcher = dispatcher.InvokeAsync,
EndpointProvider = dispatcher,
AddressProvider = matcher,
Matcher = matcher,
EndpointProvider = matcher,
};
}
}

View File

@ -21,7 +21,7 @@ namespace Microsoft.AspNetCore.Dispatcher.FunctionalTest
// This is a temporary layering issue, don't worry about it :)
services.AddRouting();
services.AddSingleton<RouteTemplateUrlGenerator>();
services.AddSingleton<IDefaultDispatcherFactory, TreeDispatcherFactory>();
services.AddSingleton<IDefaultMatcherFactory, TreeMatcherFactory>();
services.Configure<DispatcherOptions>(ConfigureDispatcher);
}
@ -58,7 +58,7 @@ namespace Microsoft.AspNetCore.Dispatcher.FunctionalTest
public void ConfigureDispatcher(DispatcherOptions options)
{
options.Dispatchers.Add(new TreeDispatcher()
options.Matchers.Add(new TreeMatcher()
{
Endpoints =
{

View File

@ -84,13 +84,14 @@ namespace Microsoft.AspNetCore.Dispatcher
{
var httpContext = new DefaultHttpContext();
httpContext.Request.Method = httpMethod;
var selector = new HttpMethodEndpointSelector();
var selectors = new List<EndpointSelector>()
{
selector
};
var selectorContext = new EndpointSelectorContext(httpContext, endpoints, selectors);
var selectorContext = new EndpointSelectorContext(httpContext, new DispatcherValueCollection(), endpoints, selectors);
return (selectorContext, selector);
}