diff --git a/samples/DispatcherSample/Startup.cs b/samples/DispatcherSample/Startup.cs index 5d4edc7f69..da026af1a2 100644 --- a/samples/DispatcherSample/Startup.cs +++ b/samples/DispatcherSample/Startup.cs @@ -22,11 +22,10 @@ namespace DispatcherSample // This is a temporary layering issue, don't worry about :) services.AddRouting(); services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); // Imagine this was done by MVC or another framework. services.AddSingleton(ConfigureDispatcher()); - services.AddSingleton(); services.AddSingleton(); } @@ -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")), }, }; } diff --git a/src/Microsoft.AspNetCore.Dispatcher.Abstractions/IDispatcherFeature.cs b/src/Microsoft.AspNetCore.Dispatcher.Abstractions/IDispatcherFeature.cs index bbfb0975ef..92cc6e6a95 100644 --- a/src/Microsoft.AspNetCore.Dispatcher.Abstractions/IDispatcherFeature.cs +++ b/src/Microsoft.AspNetCore.Dispatcher.Abstractions/IDispatcherFeature.cs @@ -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 Handler { get; set; } DispatcherValueCollection Values { get; set; } } diff --git a/src/Microsoft.AspNetCore.Dispatcher/DefaultAddressTable.cs b/src/Microsoft.AspNetCore.Dispatcher/DefaultAddressTable.cs index d67f698c64..4e8d47667b 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/DefaultAddressTable.cs +++ b/src/Microsoft.AspNetCore.Dispatcher/DefaultAddressTable.cs @@ -21,10 +21,10 @@ namespace Microsoft.AspNetCore.Dispatcher _options = options.Value; - _groups = new List
[options.Value.Dispatchers.Count]; - for (var i = 0; i < options.Value.Dispatchers.Count; i++) + _groups = new List
[options.Value.Matchers.Count]; + for (var i = 0; i < options.Value.Matchers.Count; i++) { - _groups[i] = new List
(options.Value.Dispatchers[i].AddressProvider?.Addresses ?? Array.Empty
()); + _groups[i] = new List
(options.Value.Matchers[i].AddressProvider?.Addresses ?? Array.Empty
()); } } diff --git a/src/Microsoft.AspNetCore.Dispatcher/DefaultDispatcherConfigureOptions.cs b/src/Microsoft.AspNetCore.Dispatcher/DefaultDispatcherConfigureOptions.cs index 16351dc924..f372a2812f 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/DefaultDispatcherConfigureOptions.cs +++ b/src/Microsoft.AspNetCore.Dispatcher/DefaultDispatcherConfigureOptions.cs @@ -10,12 +10,12 @@ namespace Microsoft.AspNetCore.Dispatcher internal class DefaultDispatcherConfigureOptions : IConfigureOptions { private readonly IEnumerable _dataSources; - private readonly IDefaultDispatcherFactory _dispatcherFactory; + private readonly IDefaultMatcherFactory _dispatcherFactory; private readonly IEnumerable _endpointSelectors; private readonly IEnumerable _handlerFactories; public DefaultDispatcherConfigureOptions( - IDefaultDispatcherFactory dispatcherFactory, + IDefaultMatcherFactory dispatcherFactory, IEnumerable dataSources, IEnumerable endpointSelectors, IEnumerable 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) { diff --git a/src/Microsoft.AspNetCore.Dispatcher/DispatcherCollection.cs b/src/Microsoft.AspNetCore.Dispatcher/DispatcherCollection.cs deleted file mode 100644 index 6c5f7e0b80..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/DispatcherCollection.cs +++ /dev/null @@ -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 - { - 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, - }); - } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/DispatcherFeature.cs b/src/Microsoft.AspNetCore.Dispatcher/DispatcherFeature.cs index bfece9b91d..6506f6efcb 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/DispatcherFeature.cs +++ b/src/Microsoft.AspNetCore.Dispatcher/DispatcherFeature.cs @@ -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 Handler { get; set; } public DispatcherValueCollection Values { get; set; } } diff --git a/src/Microsoft.AspNetCore.Dispatcher/DispatcherLoggerExtensions.cs b/src/Microsoft.AspNetCore.Dispatcher/DispatcherLoggerExtensions.cs index 7381430f4c..4f8dd9f631 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/DispatcherLoggerExtensions.cs +++ b/src/Microsoft.AspNetCore.Dispatcher/DispatcherLoggerExtensions.cs @@ -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) diff --git a/src/Microsoft.AspNetCore.Dispatcher/DispatcherMiddleware.cs b/src/Microsoft.AspNetCore.Dispatcher/DispatcherMiddleware.cs index 4f32b16551..211061e088 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/DispatcherMiddleware.cs +++ b/src/Microsoft.AspNetCore.Dispatcher/DispatcherMiddleware.cs @@ -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(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; } } diff --git a/src/Microsoft.AspNetCore.Dispatcher/DispatcherOptions.cs b/src/Microsoft.AspNetCore.Dispatcher/DispatcherOptions.cs index ad5d71a1a2..cffea2f891 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/DispatcherOptions.cs +++ b/src/Microsoft.AspNetCore.Dispatcher/DispatcherOptions.cs @@ -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 HandlerFactories { get; } = new List(); } diff --git a/src/Microsoft.AspNetCore.Dispatcher/EndpointMiddleware.cs b/src/Microsoft.AspNetCore.Dispatcher/EndpointMiddleware.cs index 2c41fa0d7a..48b5731475 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/EndpointMiddleware.cs +++ b/src/Microsoft.AspNetCore.Dispatcher/EndpointMiddleware.cs @@ -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(); - 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(); + 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); } } } diff --git a/src/Microsoft.AspNetCore.Dispatcher/EndpointSelectorContext.cs b/src/Microsoft.AspNetCore.Dispatcher/EndpointSelectorContext.cs index f6a5ae602c..a1ea827aa3 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/EndpointSelectorContext.cs +++ b/src/Microsoft.AspNetCore.Dispatcher/EndpointSelectorContext.cs @@ -13,13 +13,18 @@ namespace Microsoft.AspNetCore.Dispatcher { private int _index; - public EndpointSelectorContext(HttpContext httpContext, IList endpoints, IList selectors) + public EndpointSelectorContext(HttpContext httpContext, DispatcherValueCollection values, IList endpoints, IList 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 Selectors { get; } + public RequestDelegate ShortCircuit { get; set; } + + public DispatcherValueCollection Values { get; } + public Task InvokeNextAsync() { if (_index >= Selectors.Count) diff --git a/src/Microsoft.AspNetCore.Dispatcher/IDefaultDispatcherFactory.cs b/src/Microsoft.AspNetCore.Dispatcher/IDefaultMatcherFactory.cs similarity index 60% rename from src/Microsoft.AspNetCore.Dispatcher/IDefaultDispatcherFactory.cs rename to src/Microsoft.AspNetCore.Dispatcher/IDefaultMatcherFactory.cs index b8d1901b67..a98b281af8 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/IDefaultDispatcherFactory.cs +++ b/src/Microsoft.AspNetCore.Dispatcher/IDefaultMatcherFactory.cs @@ -5,8 +5,8 @@ using System.Collections.Generic; namespace Microsoft.AspNetCore.Dispatcher { - public interface IDefaultDispatcherFactory + public interface IDefaultMatcherFactory { - DispatcherEntry CreateDispatcher(DispatcherDataSource dataSource, IEnumerable endpointSelectors); + MatcherEntry CreateDispatcher(DispatcherDataSource dataSource, IEnumerable endpointSelectors); } } diff --git a/src/Microsoft.AspNetCore.Dispatcher/IMatcher.cs b/src/Microsoft.AspNetCore.Dispatcher/IMatcher.cs new file mode 100644 index 0000000000..a9762a3c38 --- /dev/null +++ b/src/Microsoft.AspNetCore.Dispatcher/IMatcher.cs @@ -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 +{ + /// + /// An interface for components that can select an given the current request, as part + /// of the execution of . + /// + /// + /// + /// implementations can also optionally implement the + /// and interfaces to provide addition information. + /// + /// + /// Use to register instances of that will be used by the + /// . + /// + /// + public interface IMatcher + { + /// + /// Attempts to asynchronously select an for the current request. + /// + /// The associated with the current request. + /// A which represents the asynchronous completion of the operation. + /// + /// + /// An implementation should use data from the current request () to select the + /// and set and optionally + /// to indicate a successful result. + /// + /// + /// If the matcher encounters an immediate failure condition, the implementation should set + /// to a that will respond to the current request. + /// + /// + Task MatchAsync(MatcherContext context); + } +} diff --git a/src/Microsoft.AspNetCore.Dispatcher/DispatcherBase.cs b/src/Microsoft.AspNetCore.Dispatcher/MatcherBase.cs similarity index 63% rename from src/Microsoft.AspNetCore.Dispatcher/DispatcherBase.cs rename to src/Microsoft.AspNetCore.Dispatcher/MatcherBase.cs index 87617e04fe..5f9edd8cfb 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/DispatcherBase.cs +++ b/src/Microsoft.AspNetCore.Dispatcher/MatcherBase.cs @@ -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
_addresses; private List _endpoints; private List _endpointSelectors; - private object _initialize; - private bool _selectorsInitialized; - private readonly Func _initializer; private object _lock; - private bool _servicesInitialized; + private bool _selectorsInitialized; + private readonly Func _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)Array.Empty(); } - 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(); - 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 TryMatchAsync(HttpContext httpContext) + protected virtual Task 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 SelectEndpointAsync(HttpContext httpContext, IEnumerable endpoints, IEnumerable selectors) + protected virtual void InitializeServices(IServiceProvider services) { - if (httpContext == null) + } + + protected virtual async Task SelectEndpointAsync(MatcherContext context, IEnumerable 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().CreateLogger(GetType()); + var services = context.HttpContext.RequestServices; + Logger = services.GetRequiredService().CreateLogger(GetType()); + InitializeServices(services); } } } diff --git a/src/Microsoft.AspNetCore.Dispatcher/MatcherCollection.cs b/src/Microsoft.AspNetCore.Dispatcher/MatcherCollection.cs new file mode 100644 index 0000000000..586caafeb9 --- /dev/null +++ b/src/Microsoft.AspNetCore.Dispatcher/MatcherCollection.cs @@ -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 + { + 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, + }); + } + } +} diff --git a/src/Microsoft.AspNetCore.Dispatcher/MatcherContext.cs b/src/Microsoft.AspNetCore.Dispatcher/MatcherContext.cs new file mode 100644 index 0000000000..6f49fc9885 --- /dev/null +++ b/src/Microsoft.AspNetCore.Dispatcher/MatcherContext.cs @@ -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 +{ + /// + /// A context object for . + /// + public class MatcherContext + { + /// + /// Creates a new for the current request. + /// + /// The associated with the current request. + public MatcherContext(HttpContext httpContext) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + HttpContext = httpContext; + } + + /// + /// Gets the associated with the current request. + /// + public HttpContext HttpContext { get; } + + /// + /// Gets or sets the selected by the matcher. + /// + public Endpoint Endpoint { get; set; } + + /// + /// Gets or sets a short-circuit delegate provided by the matcher. + /// + public RequestDelegate ShortCircuit { get; set; } + + /// + /// Gets or sets a provided by the matcher. + /// + public DispatcherValueCollection Values { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.Dispatcher/DispatcherEntry.cs b/src/Microsoft.AspNetCore.Dispatcher/MatcherEntry.cs similarity index 75% rename from src/Microsoft.AspNetCore.Dispatcher/DispatcherEntry.cs rename to src/Microsoft.AspNetCore.Dispatcher/MatcherEntry.cs index eeac61bf96..0f32c5eaea 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/DispatcherEntry.cs +++ b/src/Microsoft.AspNetCore.Dispatcher/MatcherEntry.cs @@ -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; } diff --git a/src/Microsoft.AspNetCore.Dispatcher/TemplateEndpointSelector.cs b/src/Microsoft.AspNetCore.Dispatcher/TemplateEndpointSelector.cs deleted file mode 100644 index 3e2a2d6bb9..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/TemplateEndpointSelector.cs +++ /dev/null @@ -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(); - - 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; - } - } -} diff --git a/src/Microsoft.AspNetCore.Routing/Dispatcher/RouteTemplateDispatcher.cs b/src/Microsoft.AspNetCore.Routing/Dispatcher/RouteTemplateDispatcher.cs deleted file mode 100644 index 551540883e..0000000000 --- a/src/Microsoft.AspNetCore.Routing/Dispatcher/RouteTemplateDispatcher.cs +++ /dev/null @@ -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 _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 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 TryMatchAsync(HttpContext httpContext) - { - if (httpContext == null) - { - throw new ArgumentNullException(nameof(httpContext)); - } - - var feature = httpContext.Features.Get(); - 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 GetConstraints( - IInlineConstraintResolver inlineConstraintResolver, - RouteTemplate parsedTemplate, - IDictionary 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; - } - } -} diff --git a/src/Microsoft.AspNetCore.Routing/Dispatcher/RouterDispatcher.cs b/src/Microsoft.AspNetCore.Routing/Dispatcher/RouterDispatcher.cs deleted file mode 100644 index a96d9ac9fb..0000000000 --- a/src/Microsoft.AspNetCore.Routing/Dispatcher/RouterDispatcher.cs +++ /dev/null @@ -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 -{ - /// - /// An adapter to plug an into a dispatcher. - /// - 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 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(); - 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 Metadata => Array.Empty(); - } - } -} - diff --git a/src/Microsoft.AspNetCore.Routing/Dispatcher/TreeDispatcher.cs b/src/Microsoft.AspNetCore.Routing/Dispatcher/TreeMatcher.cs similarity index 88% rename from src/Microsoft.AspNetCore.Routing/Dispatcher/TreeDispatcher.cs rename to src/Microsoft.AspNetCore.Routing/Dispatcher/TreeMatcher.cs index c11260b9e1..68e0c44515 100644 --- a/src/Microsoft.AspNetCore.Routing/Dispatcher/TreeDispatcher.cs +++ b/src/Microsoft.AspNetCore.Routing/Dispatcher/TreeMatcher.cs @@ -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 _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(); - 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; + } } } } diff --git a/src/Microsoft.AspNetCore.Routing/Dispatcher/TreeDispatcherFactory.cs b/src/Microsoft.AspNetCore.Routing/Dispatcher/TreeMatcherFactory.cs similarity index 57% rename from src/Microsoft.AspNetCore.Routing/Dispatcher/TreeDispatcherFactory.cs rename to src/Microsoft.AspNetCore.Routing/Dispatcher/TreeMatcherFactory.cs index a86183332b..213a0791a4 100644 --- a/src/Microsoft.AspNetCore.Routing/Dispatcher/TreeDispatcherFactory.cs +++ b/src/Microsoft.AspNetCore.Routing/Dispatcher/TreeMatcherFactory.cs @@ -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 endpointSelectors) + public MatcherEntry CreateDispatcher(DispatcherDataSource dataSource, IEnumerable 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, }; } } diff --git a/test/Microsoft.AspNetCore.Dispatcher.FunctionalTest/ApiAppStartup.cs b/test/Microsoft.AspNetCore.Dispatcher.FunctionalTest/ApiAppStartup.cs index 89a34c8e86..bc3e8017d1 100644 --- a/test/Microsoft.AspNetCore.Dispatcher.FunctionalTest/ApiAppStartup.cs +++ b/test/Microsoft.AspNetCore.Dispatcher.FunctionalTest/ApiAppStartup.cs @@ -21,7 +21,7 @@ namespace Microsoft.AspNetCore.Dispatcher.FunctionalTest // This is a temporary layering issue, don't worry about it :) services.AddRouting(); services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); services.Configure(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 = { diff --git a/test/Microsoft.AspNetCore.Dispatcher.Test/HttpMethodEndpointSelectorTest.cs b/test/Microsoft.AspNetCore.Dispatcher.Test/HttpMethodEndpointSelectorTest.cs index 898960d4cf..76e779d24c 100644 --- a/test/Microsoft.AspNetCore.Dispatcher.Test/HttpMethodEndpointSelectorTest.cs +++ b/test/Microsoft.AspNetCore.Dispatcher.Test/HttpMethodEndpointSelectorTest.cs @@ -84,13 +84,14 @@ namespace Microsoft.AspNetCore.Dispatcher { var httpContext = new DefaultHttpContext(); httpContext.Request.Method = httpMethod; + var selector = new HttpMethodEndpointSelector(); var selectors = new List() { selector }; - var selectorContext = new EndpointSelectorContext(httpContext, endpoints, selectors); + var selectorContext = new EndpointSelectorContext(httpContext, new DispatcherValueCollection(), endpoints, selectors); return (selectorContext, selector); }