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 :) // This is a temporary layering issue, don't worry about :)
services.AddRouting(); services.AddRouting();
services.AddSingleton<RouteTemplateUrlGenerator>(); services.AddSingleton<RouteTemplateUrlGenerator>();
services.AddSingleton<IDefaultDispatcherFactory, TreeDispatcherFactory>(); services.AddSingleton<IDefaultMatcherFactory, TreeMatcherFactory>();
// Imagine this was done by MVC or another framework. // Imagine this was done by MVC or another framework.
services.AddSingleton<DispatcherDataSource>(ConfigureDispatcher()); services.AddSingleton<DispatcherDataSource>(ConfigureDispatcher());
services.AddSingleton<EndpointSelector, TemplateEndpointSelector>();
services.AddSingleton<EndpointSelector, HttpMethodEndpointSelector>(); services.AddSingleton<EndpointSelector, HttpMethodEndpointSelector>();
} }
@ -37,18 +36,20 @@ namespace DispatcherSample
{ {
Addresses = Addresses =
{ {
new TemplateAddress("{controller=Home}/{action=Index}/{id?}", new { controller = "Home", action = "Index", }, "Home:Index()"), new TemplateAddress("{id?}", new { controller = "Home", action = "Index", }, "Home:Index()"),
new TemplateAddress("{controller=Home}/{action=Index}/{id?}", new { controller = "Home", action = "About", }, "Home:About()"), new TemplateAddress("Home/About/{id?}", new { controller = "Home", action = "About", }, "Home:About()"),
new TemplateAddress("{controller=Home}/{action=Index}/{id?}", new { controller = "Admin", action = "Index", }, "Admin:Index()"), new TemplateAddress("Admin/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("Admin/Users/{id?}", new { controller = "Admin", action = "Users", }, "Admin:GetUsers()/Admin:EditUsers()"),
}, },
Endpoints = Endpoints =
{ {
new TemplateEndpoint("{controller=Home}/{action=Index}/{id?}", new { controller = "Home", action = "Index", }, Home_Index, "Home:Index()"), new TemplateEndpoint("{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("Home/{id?}", new { controller = "Home", action = "Index", }, Home_Index, "Home:Index()"),
new TemplateEndpoint("{controller=Home}/{action=Index}/{id?}", new { controller = "Admin", action = "Index", }, Admin_Index, "Admin:Index()"), new TemplateEndpoint("Home/Index/{id?}", new { controller = "Home", action = "Index", }, Home_Index, "Home:Index()"),
new TemplateEndpoint("{controller=Home}/{action=Index}/{id?}", new { controller = "Admin", action = "Users", }, "GET", Admin_GetUsers, "Admin:GetUsers()", new AuthorizationPolicyMetadata("Admin")), new TemplateEndpoint("Home/About/{id?}", new { controller = "Home", action = "About", }, Home_About, "Home:About()"),
new TemplateEndpoint("{controller=Home}/{action=Index}/{id?}", new { controller = "Admin", action = "Users", }, "POST", Admin_EditUsers, "Admin:EditUsers()", new AuthorizationPolicyMetadata("Admin")), 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. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Dispatcher namespace Microsoft.AspNetCore.Dispatcher
@ -9,7 +10,7 @@ namespace Microsoft.AspNetCore.Dispatcher
{ {
Endpoint Endpoint { get; set; } Endpoint Endpoint { get; set; }
RequestDelegate RequestDelegate { get; set; } Func<RequestDelegate, RequestDelegate> Handler { get; set; }
DispatcherValueCollection Values { get; set; } DispatcherValueCollection Values { get; set; }
} }

View File

@ -21,10 +21,10 @@ namespace Microsoft.AspNetCore.Dispatcher
_options = options.Value; _options = options.Value;
_groups = new List<Address>[options.Value.Dispatchers.Count]; _groups = new List<Address>[options.Value.Matchers.Count];
for (var i = 0; i < options.Value.Dispatchers.Count; i++) 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> internal class DefaultDispatcherConfigureOptions : IConfigureOptions<DispatcherOptions>
{ {
private readonly IEnumerable<DispatcherDataSource> _dataSources; private readonly IEnumerable<DispatcherDataSource> _dataSources;
private readonly IDefaultDispatcherFactory _dispatcherFactory; private readonly IDefaultMatcherFactory _dispatcherFactory;
private readonly IEnumerable<EndpointSelector> _endpointSelectors; private readonly IEnumerable<EndpointSelector> _endpointSelectors;
private readonly IEnumerable<EndpointHandlerFactoryBase> _handlerFactories; private readonly IEnumerable<EndpointHandlerFactoryBase> _handlerFactories;
public DefaultDispatcherConfigureOptions( public DefaultDispatcherConfigureOptions(
IDefaultDispatcherFactory dispatcherFactory, IDefaultMatcherFactory dispatcherFactory,
IEnumerable<DispatcherDataSource> dataSources, IEnumerable<DispatcherDataSource> dataSources,
IEnumerable<EndpointSelector> endpointSelectors, IEnumerable<EndpointSelector> endpointSelectors,
IEnumerable<EndpointHandlerFactoryBase> handlerFactories) IEnumerable<EndpointHandlerFactoryBase> handlerFactories)
@ -33,7 +33,7 @@ namespace Microsoft.AspNetCore.Dispatcher
throw new ArgumentNullException(nameof(options)); 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) 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. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Dispatcher namespace Microsoft.AspNetCore.Dispatcher
@ -9,7 +10,7 @@ namespace Microsoft.AspNetCore.Dispatcher
{ {
public Endpoint Endpoint { get; set; } public Endpoint Endpoint { get; set; }
public RequestDelegate RequestDelegate { get; set; } public Func<RequestDelegate, RequestDelegate> Handler { get; set; }
public DispatcherValueCollection Values { get; set; } public DispatcherValueCollection Values { get; set; }
} }

View File

@ -31,9 +31,9 @@ namespace Microsoft.AspNetCore.Dispatcher
_ambiguousEndpoints(logger, ambiguousEndpoints, null); _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) public static void NoEndpointsMatched(this ILogger logger, PathString pathString)

View File

@ -42,12 +42,38 @@ namespace Microsoft.AspNetCore.Dispatcher
var feature = new DispatcherFeature(); var feature = new DispatcherFeature();
httpContext.Features.Set<IDispatcherFeature>(feature); 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); await entry.Matcher.MatchAsync(context);
if (feature.Endpoint != null || feature.RequestDelegate != null)
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; break;
} }
} }

View File

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

View File

@ -37,28 +37,15 @@ namespace Microsoft.AspNetCore.Dispatcher
_next = next; _next = next;
} }
public async Task Invoke(HttpContext context) public async Task Invoke(HttpContext httpContext)
{ {
var feature = context.Features.Get<IDispatcherFeature>(); var feature = httpContext.Features.Get<IDispatcherFeature>();
if (feature.Endpoint != null && feature.RequestDelegate == null) if (feature.Handler != 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)
{ {
_logger.LogInformation("Executing endpoint {Endpoint}", feature.Endpoint.DisplayName); _logger.LogInformation("Executing endpoint {Endpoint}", feature.Endpoint.DisplayName);
try try
{ {
await feature.RequestDelegate(context); await feature.Handler(_next)(httpContext);
} }
finally finally
{ {
@ -68,7 +55,7 @@ namespace Microsoft.AspNetCore.Dispatcher
return; return;
} }
await _next(context); await _next(httpContext);
} }
} }
} }

View File

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

View File

@ -5,8 +5,8 @@ using System.Collections.Generic;
namespace Microsoft.AspNetCore.Dispatcher 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 namespace Microsoft.AspNetCore.Dispatcher
{ {
public abstract class DispatcherBase : IAddressCollectionProvider, IEndpointCollectionProvider public abstract class MatcherBase : IMatcher, IAddressCollectionProvider, IEndpointCollectionProvider
{ {
private List<Address> _addresses; private List<Address> _addresses;
private List<Endpoint> _endpoints; private List<Endpoint> _endpoints;
private List<EndpointSelector> _endpointSelectors; private List<EndpointSelector> _endpointSelectors;
private object _initialize;
private bool _selectorsInitialized;
private readonly Func<object> _initializer;
private object _lock; private object _lock;
private bool _servicesInitialized; private bool _servicesInitialized;
private bool _selectorsInitialized;
private readonly Func<object> _selectorInitializer;
public DispatcherBase()
public MatcherBase()
{ {
_lock = new object(); _lock = new object();
_initializer = InitializeSelectors; _selectorInitializer = InitializeSelectors;
} }
protected ILogger Logger { get; private set; } protected ILogger Logger { get; private set; }
@ -92,39 +91,37 @@ namespace Microsoft.AspNetCore.Dispatcher
return ((IEndpointCollectionProvider)DataSource)?.Endpoints ?? _endpoints ?? (IReadOnlyList<Endpoint>)Array.Empty<Endpoint>(); 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>(); context.Values = await MatchRequestAsync(context.HttpContext);
if (await TryMatchAsync(httpContext)) if (context.Values != null)
{ {
if (feature.RequestDelegate != null) await SelectEndpointAsync(context, GetEndpoints());
{
// Short circuit, no need to select an endpoint.
return;
}
feature.Endpoint = await SelectEndpointAsync(httpContext, GetEndpoints(), Selectors);
} }
} }
protected virtual Task<bool> TryMatchAsync(HttpContext httpContext) protected virtual Task<DispatcherValueCollection> MatchRequestAsync(HttpContext httpContext)
{ {
// By default don't apply any criteria. // By default don't apply any criteria or provide any values.
return Task.FromResult(true); 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) if (endpoints == null)
@ -132,25 +129,28 @@ namespace Microsoft.AspNetCore.Dispatcher
throw new ArgumentNullException(nameof(endpoints)); throw new ArgumentNullException(nameof(endpoints));
} }
if (selectors == null) EnsureSelectorsInitialized();
{
throw new ArgumentNullException(nameof(selectors));
}
LazyInitializer.EnsureInitialized(ref _initialize, ref _selectorsInitialized, ref _lock, _initializer); var selectorContext = new EndpointSelectorContext(context.HttpContext, context.Values, endpoints.ToList(), Selectors.ToList());
var selectorContext = new EndpointSelectorContext(httpContext, endpoints.ToList(), selectors.ToList());
await selectorContext.InvokeNextAsync(); await selectorContext.InvokeNextAsync();
if (selectorContext.ShortCircuit != null)
{
context.ShortCircuit = selectorContext.ShortCircuit;
return;
}
switch (selectorContext.Endpoints.Count) switch (selectorContext.Endpoints.Count)
{ {
case 0: case 0:
Logger.NoEndpointsMatched(httpContext.Request.Path); Logger.NoEndpointsMatched(context.HttpContext.Request.Path);
return null; return;
case 1: case 1:
Logger.EndpointMatched(selectorContext.Endpoints[0].DisplayName); context.Endpoint = selectorContext.Endpoints[0];
return selectorContext.Endpoints[0];
Logger.EndpointMatched(context.Endpoint);
return;
default: default:
var endpointNames = string.Join( 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() private object InitializeSelectors()
{ {
foreach (var selector in Selectors) foreach (var selector in Selectors)
@ -177,23 +183,25 @@ namespace Microsoft.AspNetCore.Dispatcher
return null; return null;
} }
protected void EnsureServicesInitialized(HttpContext httpContext) protected void EnsureServicesInitialized(MatcherContext context)
{ {
if (Volatile.Read(ref _servicesInitialized)) if (Volatile.Read(ref _servicesInitialized))
{ {
return; return;
} }
EnsureServicesInitializedSlow(httpContext); EnsureServicesInitializedSlow(context);
} }
private void EnsureServicesInitializedSlow(HttpContext httpContext) private void EnsureServicesInitializedSlow(MatcherContext context)
{ {
lock (_lock) lock (_lock)
{ {
if (!Volatile.Read(ref _servicesInitialized)) 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. // 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. // 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 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; } 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Dispatcher; using Microsoft.AspNetCore.Dispatcher;
@ -15,11 +14,10 @@ using Microsoft.AspNetCore.Routing.Logging;
using Microsoft.AspNetCore.Routing.Template; using Microsoft.AspNetCore.Routing.Template;
using Microsoft.AspNetCore.Routing.Tree; using Microsoft.AspNetCore.Routing.Tree;
using Microsoft.Extensions.Internal; using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Routing.Dispatcher namespace Microsoft.AspNetCore.Routing.Dispatcher
{ {
public class TreeDispatcher : DispatcherBase public class TreeMatcher : MatcherBase
{ {
private bool _dataInitialized; private bool _dataInitialized;
private object _lock; private object _lock;
@ -27,31 +25,30 @@ namespace Microsoft.AspNetCore.Routing.Dispatcher
private readonly Func<Cache> _initializer; private readonly Func<Cache> _initializer;
public TreeDispatcher() public TreeMatcher()
{ {
_lock = new object(); _lock = new object();
_initializer = CreateCache; _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 cache = LazyInitializer.EnsureInitialized(ref _cache, ref _dataInitialized, ref _lock, _initializer);
var feature = httpContext.Features.Get<IDispatcherFeature>(); var values = new RouteValueDictionary();
var values = feature.Values?.AsRouteValueDictionary() ?? new RouteValueDictionary(); context.Values = values;
feature.Values = values;
for (var i = 0; i < cache.Trees.Length; i++) for (var i = 0; i < cache.Trees.Length; i++)
{ {
var tree = cache.Trees[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); var treenumerator = new Treenumerator(tree.Root, tokenizer);
@ -64,23 +61,36 @@ namespace Microsoft.AspNetCore.Routing.Dispatcher
var matcher = item.TemplateMatcher; var matcher = item.TemplateMatcher;
values.Clear(); values.Clear();
if (!matcher.TryMatch(httpContext.Request.Path, values)) if (!matcher.TryMatch(context.HttpContext.Request.Path, values))
{ {
continue; continue;
} }
Logger.MatchedRoute(entry.RouteName, entry.RouteTemplate.TemplateText); Logger.MatchedRoute(entry.RouteName, entry.RouteTemplate.TemplateText);
if (!MatchConstraints(httpContext, values, entry.Constraints)) if (!MatchConstraints(context.HttpContext, values, entry.Constraints))
{ {
continue; continue;
} }
feature.Endpoint = await SelectEndpointAsync(httpContext, (Endpoint[])entry.Tag, Selectors); await SelectEndpointAsync(context, (Endpoint[])entry.Tag);
if (feature.Endpoint != null || feature.RequestDelegate != null) if (context.ShortCircuit != null)
{ {
return; 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 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) if (dataSource == null)
{ {
throw new ArgumentNullException(nameof(dataSource)); throw new ArgumentNullException(nameof(dataSource));
} }
var dispatcher = new TreeDispatcher() var matcher = new TreeMatcher()
{ {
DataSource = dataSource, DataSource = dataSource,
}; };
foreach (var endpointSelector in endpointSelectors) foreach (var endpointSelector in endpointSelectors)
{ {
dispatcher.Selectors.Add(endpointSelector); matcher.Selectors.Add(endpointSelector);
} }
return new DispatcherEntry() return new MatcherEntry()
{ {
AddressProvider = dispatcher, AddressProvider = matcher,
Dispatcher = dispatcher.InvokeAsync, Matcher = matcher,
EndpointProvider = dispatcher, EndpointProvider = matcher,
}; };
} }
} }

View File

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

View File

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