From 134096d9cb833f9c994893d2783378adc5c990be Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Sun, 17 Sep 2017 00:06:58 -0700 Subject: [PATCH] Relayer implementation This refactor introduces two major changes 1. Now creating the 'handler' delegate happens inside the endpoint middleware. This allows you to short circuit even harder, AND to create endpoint funcs that capture and use 'next' to rejoin the middleware pipeline. 2. Relayered the implementation to have routing plug into the dispatcher. It wasn't immediately apparent to me that this was the right thing to do, but I think we will need to do things this way to deliver the kind of back-compat experience we need to do. The idea that I have is that 'attribute routing' will be the 'default' entry in the dispatcher. Adding additional conventional routes or other IRouter-based extensibility will be possible through adapters - but the default experience will be to add items to the 'attribute route'. So. We will need to port the attribute routing infrastructure to the dispatcher library. We may also need to make RVD into a subclass of something in the dispatcher assembly. --- .../DispatcherSample/DispatcherEndpoint.cs | 17 --- .../DispatcherSample/DispatcherSample.csproj | 3 - samples/DispatcherSample/Startup.cs | 101 ++++++++-------- .../Endpoint.cs | 4 + .../EndpointHandlerFactory.cs | 17 +++ .../IDispatcherFeature.cs | 4 +- ....AspNetCore.Dispatcher.Abstractions.csproj | 2 +- ...ispatcherEndpoint.cs => DispatcherBase.cs} | 12 +- .../DispatcherEntry.cs | 15 --- .../DispatcherMiddleware.cs | 83 +++---------- .../DispatcherOptions.cs | 5 +- .../EndpointMiddleware.cs | 33 +++++- .../Microsoft.AspNetCore.Dispatcher.csproj | 5 +- .../RouteValuesEndpoint.cs | 22 ---- .../SimpleEndpoint.cs | 73 ++++++++++++ .../Dispatcher/RouteValuesEndpoint.cs | 59 ++++++++++ .../Dispatcher/RouterDispatcher.cs | 40 +++++++ .../Dispatcher/RouterEndpointSelector.cs | 110 ++++++++++++++++++ .../Microsoft.AspNetCore.Routing.csproj | 2 + .../Properties/Resources.Designer.cs | 14 +++ .../Resources.resx | 57 ++++----- 21 files changed, 462 insertions(+), 216 deletions(-) delete mode 100644 samples/DispatcherSample/DispatcherEndpoint.cs create mode 100644 src/Microsoft.AspNetCore.Dispatcher.Abstractions/EndpointHandlerFactory.cs rename src/Microsoft.AspNetCore.Dispatcher/{DispatcherEndpoint.cs => DispatcherBase.cs} (52%) delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/DispatcherEntry.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/RouteValuesEndpoint.cs create mode 100644 src/Microsoft.AspNetCore.Dispatcher/SimpleEndpoint.cs create mode 100644 src/Microsoft.AspNetCore.Routing/Dispatcher/RouteValuesEndpoint.cs create mode 100644 src/Microsoft.AspNetCore.Routing/Dispatcher/RouterDispatcher.cs create mode 100644 src/Microsoft.AspNetCore.Routing/Dispatcher/RouterEndpointSelector.cs diff --git a/samples/DispatcherSample/DispatcherEndpoint.cs b/samples/DispatcherSample/DispatcherEndpoint.cs deleted file mode 100644 index b58156eae7..0000000000 --- a/samples/DispatcherSample/DispatcherEndpoint.cs +++ /dev/null @@ -1,17 +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 Microsoft.AspNetCore.Dispatcher; - -namespace DispatcherSample -{ - public class DispatcherEndpoint : Endpoint - { - public DispatcherEndpoint(string displayName) - { - DisplayName = displayName; - } - - public override string DisplayName { get; } - } -} diff --git a/samples/DispatcherSample/DispatcherSample.csproj b/samples/DispatcherSample/DispatcherSample.csproj index 7151f7ad15..29bcf6c954 100644 --- a/samples/DispatcherSample/DispatcherSample.csproj +++ b/samples/DispatcherSample/DispatcherSample.csproj @@ -6,9 +6,6 @@ - - - diff --git a/samples/DispatcherSample/Startup.cs b/samples/DispatcherSample/Startup.cs index bd1cba2d01..a07f62da66 100644 --- a/samples/DispatcherSample/Startup.cs +++ b/samples/DispatcherSample/Startup.cs @@ -1,73 +1,70 @@ // 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.Collections.Generic; +using System; +using System.Linq; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Dispatcher; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; -using Microsoft.AspNetCore.Routing.Template; +using Microsoft.AspNetCore.Routing.Dispatcher; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; namespace DispatcherSample { public class Startup { + private readonly static IInlineConstraintResolver ConstraintResolver = new DefaultInlineConstraintResolver( + new OptionsManager( + new OptionsFactory( + Enumerable.Empty>(), + Enumerable.Empty>()))); + public void ConfigureServices(IServiceCollection services) { services.Configure(options => { - options.DispatcherEntryList = new List() - { - new DispatcherEntry - { - RouteTemplate = TemplateParser.Parse("{Endpoint=example}"), - Endpoints = new [] + options.Dispatchers.Add(CreateDispatcher( + "{Endpoint=example}", + new RouteValuesEndpoint( + new RouteValueDictionary(new { Endpoint = "First" }), + async (context) => { - new RouteValuesEndpoint("example") - { - RequiredValues = new RouteValueDictionary(new { Endpoint = "First" }), - RequestDelegate = async (context) => - { - await context.Response.WriteAsync("Hello from the example!"); - } - }, - new RouteValuesEndpoint("example2") - { - RequiredValues = new RouteValueDictionary(new { Endpoint = "Second" }), - RequestDelegate = async (context) => - { - await context.Response.WriteAsync("Hello from the second example!"); - } - }, - } - }, + await context.Response.WriteAsync("Hello from the example!"); + }, + Array.Empty(), + "example"), + new RouteValuesEndpoint( + new RouteValueDictionary(new { Endpoint = "Second" }), + async (context) => + { + await context.Response.WriteAsync("Hello from the second example!"); + }, + Array.Empty(), + "example2"))); - new DispatcherEntry - { - RouteTemplate = TemplateParser.Parse("{Endpoint=example}/{Parameter=foo}"), - Endpoints = new [] + options.Dispatchers.Add(CreateDispatcher( + "{Endpoint=example}/{Parameter=foo}", + new RouteValuesEndpoint( + new RouteValueDictionary(new { Endpoint = "First", Parameter = "param1" }), + async (context) => { - new RouteValuesEndpoint("example") - { - RequiredValues = new RouteValueDictionary(new { Endpoint = "First", Parameter = "param1"}), - RequestDelegate = async (context) => - { - await context.Response.WriteAsync("Hello from the example for foo!"); - } - }, - new RouteValuesEndpoint("example2") - { - RequiredValues = new RouteValueDictionary(new { Endpoint = "Second", Parameter = "param2"}), - RequestDelegate = async (context) => - { - await context.Response.WriteAsync("Hello from the second example for foo!"); - } - }, - } - } - }; + await context.Response.WriteAsync("Hello from the example for foo!"); + }, + Array.Empty(), + "example"), + new RouteValuesEndpoint( + new RouteValueDictionary(new { Endpoint = "Second", Parameter = "param2" }), + async (context) => + { + await context.Response.WriteAsync("Hello from the second example for foo!"); + }, + Array.Empty(), + "example2"))); + + options.HandlerFactories.Add((endpoint) => (endpoint as RouteValuesEndpoint)?.HandlerFactory); }); services.AddSingleton(); @@ -99,5 +96,11 @@ namespace DispatcherSample await next.Invoke(); }); } + + private static RequestDelegate CreateDispatcher(string routeTemplate, RouteValuesEndpoint endpoint, params RouteValuesEndpoint[] endpoints) + { + var dispatcher = new RouterDispatcher(new Route(new RouterEndpointSelector(new[] { endpoint }.Concat(endpoints)), routeTemplate, ConstraintResolver)); + return dispatcher.InvokeAsync; + } } } diff --git a/src/Microsoft.AspNetCore.Dispatcher.Abstractions/Endpoint.cs b/src/Microsoft.AspNetCore.Dispatcher.Abstractions/Endpoint.cs index a2f9092721..2577d16e62 100644 --- a/src/Microsoft.AspNetCore.Dispatcher.Abstractions/Endpoint.cs +++ b/src/Microsoft.AspNetCore.Dispatcher.Abstractions/Endpoint.cs @@ -1,10 +1,14 @@ // 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.Collections.Generic; + namespace Microsoft.AspNetCore.Dispatcher { public abstract class Endpoint { public abstract string DisplayName { get; } + + public abstract IReadOnlyList Metadata { get; } } } diff --git a/src/Microsoft.AspNetCore.Dispatcher.Abstractions/EndpointHandlerFactory.cs b/src/Microsoft.AspNetCore.Dispatcher.Abstractions/EndpointHandlerFactory.cs new file mode 100644 index 0000000000..54f5c2676b --- /dev/null +++ b/src/Microsoft.AspNetCore.Dispatcher.Abstractions/EndpointHandlerFactory.cs @@ -0,0 +1,17 @@ +// 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 delegate which attempts to create a for the selected . + /// + /// The selected by the dispatcher. + /// + /// A that invokes the operation represented by the , or null. + /// + public delegate Func EndpointHandlerFactory(Endpoint endpoint); +} diff --git a/src/Microsoft.AspNetCore.Dispatcher.Abstractions/IDispatcherFeature.cs b/src/Microsoft.AspNetCore.Dispatcher.Abstractions/IDispatcherFeature.cs index 85e2645c9e..86f7e07737 100644 --- a/src/Microsoft.AspNetCore.Dispatcher.Abstractions/IDispatcherFeature.cs +++ b/src/Microsoft.AspNetCore.Dispatcher.Abstractions/IDispatcherFeature.cs @@ -7,8 +7,8 @@ namespace Microsoft.AspNetCore.Dispatcher { public interface IDispatcherFeature { - Endpoint Endpoint { get; } + Endpoint Endpoint { get; set; } - RequestDelegate RequestDelegate { get; } + RequestDelegate RequestDelegate { get; set; } } } diff --git a/src/Microsoft.AspNetCore.Dispatcher.Abstractions/Microsoft.AspNetCore.Dispatcher.Abstractions.csproj b/src/Microsoft.AspNetCore.Dispatcher.Abstractions/Microsoft.AspNetCore.Dispatcher.Abstractions.csproj index d0caf3306c..179b35ceb4 100644 --- a/src/Microsoft.AspNetCore.Dispatcher.Abstractions/Microsoft.AspNetCore.Dispatcher.Abstractions.csproj +++ b/src/Microsoft.AspNetCore.Dispatcher.Abstractions/Microsoft.AspNetCore.Dispatcher.Abstractions.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/Microsoft.AspNetCore.Dispatcher/DispatcherEndpoint.cs b/src/Microsoft.AspNetCore.Dispatcher/DispatcherBase.cs similarity index 52% rename from src/Microsoft.AspNetCore.Dispatcher/DispatcherEndpoint.cs rename to src/Microsoft.AspNetCore.Dispatcher/DispatcherBase.cs index ea89051708..85423f0aef 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/DispatcherEndpoint.cs +++ b/src/Microsoft.AspNetCore.Dispatcher/DispatcherBase.cs @@ -1,15 +1,13 @@ // 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 { - public class DispatcherEndpoint : Endpoint + public abstract class DispatcherBase { - public DispatcherEndpoint(string displayName) - { - DisplayName = displayName; - } - - public override string DisplayName { get; } + public abstract Task InvokeAsync(HttpContext httpContext); } } diff --git a/src/Microsoft.AspNetCore.Dispatcher/DispatcherEntry.cs b/src/Microsoft.AspNetCore.Dispatcher/DispatcherEntry.cs deleted file mode 100644 index e1709db348..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/DispatcherEntry.cs +++ /dev/null @@ -1,15 +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.Collections.Generic; -using Microsoft.AspNetCore.Routing.Template; - -namespace Microsoft.AspNetCore.Dispatcher -{ - public class DispatcherEntry - { - public IList Endpoints { get; set; } - - public RouteTemplate RouteTemplate { get; set; } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/DispatcherMiddleware.cs b/src/Microsoft.AspNetCore.Dispatcher/DispatcherMiddleware.cs index 274ad424f9..b0eb305ff1 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/DispatcherMiddleware.cs +++ b/src/Microsoft.AspNetCore.Dispatcher/DispatcherMiddleware.cs @@ -4,8 +4,6 @@ using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Routing; -using Microsoft.AspNetCore.Routing.Template; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Dispatcher @@ -17,82 +15,35 @@ namespace Microsoft.AspNetCore.Dispatcher public DispatcherMiddleware(IOptions options, RequestDelegate next) { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + if (next == null) + { + throw new ArgumentNullException(nameof(next)); + } + _options = options.Value; _next = next; } public async Task Invoke(HttpContext httpContext) { - foreach (var entry in _options.DispatcherEntryList) + var feature = new DispatcherFeature(); + httpContext.Features.Set(feature); + + foreach (var entry in _options.Dispatchers) { - var parsedTemplate = entry.RouteTemplate; - var defaults = GetDefaults(parsedTemplate); - var templateMatcher = new TemplateMatcher(parsedTemplate, defaults); - var values = new RouteValueDictionary(); - - foreach (var endpoint in entry.Endpoints) + await entry(httpContext); + if (feature.Endpoint != null || feature.RequestDelegate != null) { - if (templateMatcher.TryMatch(httpContext.Request.Path, values)) - { - if (!CompareRouteValues(values, endpoint.RequiredValues)) - { - values.Clear(); - } - - else - { - var dispatcherFeature = new DispatcherFeature - { - Endpoint = endpoint, - RequestDelegate = endpoint.RequestDelegate - }; - - httpContext.Features.Set(dispatcherFeature); - break; - } - } + break; } } await _next(httpContext); } - - private RouteValueDictionary GetDefaults(RouteTemplate parsedTemplate) - { - var result = new RouteValueDictionary(); - - foreach (var parameter in parsedTemplate.Parameters) - { - if (parameter.DefaultValue != null) - { - result.Add(parameter.Name, parameter.DefaultValue); - } - } - - return result; - } - - private bool CompareRouteValues(RouteValueDictionary values, RouteValueDictionary 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.Dispatcher/DispatcherOptions.cs b/src/Microsoft.AspNetCore.Dispatcher/DispatcherOptions.cs index ac966dc0b5..7f01d4c8f1 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/DispatcherOptions.cs +++ b/src/Microsoft.AspNetCore.Dispatcher/DispatcherOptions.cs @@ -1,12 +1,15 @@ // 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; using System.Collections.Generic; namespace Microsoft.AspNetCore.Dispatcher { public class DispatcherOptions { - public IList DispatcherEntryList { get; set; } + public IList Dispatchers { get; } = new List(); + + public IList HandlerFactories { get; } = new List(); } } diff --git a/src/Microsoft.AspNetCore.Dispatcher/EndpointMiddleware.cs b/src/Microsoft.AspNetCore.Dispatcher/EndpointMiddleware.cs index 363059fae6..2b3b9b92f1 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/EndpointMiddleware.cs +++ b/src/Microsoft.AspNetCore.Dispatcher/EndpointMiddleware.cs @@ -1,31 +1,56 @@ // 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; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Dispatcher { public class EndpointMiddleware { + private readonly DispatcherOptions _options; private RequestDelegate _next; - public EndpointMiddleware(RequestDelegate next) + public EndpointMiddleware(IOptions options, RequestDelegate next) { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + if (next == null) + { + throw new ArgumentNullException(nameof(next)); + } + + _options = options.Value; _next = next; } public async Task Invoke(HttpContext context) { var feature = context.Features.Get(); - if (feature.RequestDelegate == null) + if (feature.Endpoint != null && feature.RequestDelegate == null) { - await _next(context); + for (var i = 0; i < _options.HandlerFactories.Count; i++) + { + var handler = _options.HandlerFactories[i](feature.Endpoint); + if (handler != null) + { + feature.RequestDelegate = handler(_next); + break; + } + } } - else + + if (feature.RequestDelegate != null) { await feature.RequestDelegate(context); } + + await _next(context); } } } diff --git a/src/Microsoft.AspNetCore.Dispatcher/Microsoft.AspNetCore.Dispatcher.csproj b/src/Microsoft.AspNetCore.Dispatcher/Microsoft.AspNetCore.Dispatcher.csproj index ade389a92d..31a0a60990 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/Microsoft.AspNetCore.Dispatcher.csproj +++ b/src/Microsoft.AspNetCore.Dispatcher/Microsoft.AspNetCore.Dispatcher.csproj @@ -9,12 +9,13 @@ - - + + + diff --git a/src/Microsoft.AspNetCore.Dispatcher/RouteValuesEndpoint.cs b/src/Microsoft.AspNetCore.Dispatcher/RouteValuesEndpoint.cs deleted file mode 100644 index 457733d655..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/RouteValuesEndpoint.cs +++ /dev/null @@ -1,22 +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 Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Routing; - -namespace Microsoft.AspNetCore.Dispatcher -{ - public class RouteValuesEndpoint : Endpoint - { - public RouteValuesEndpoint(string displayName) - { - DisplayName = displayName; - } - - public override string DisplayName { get; } - - public RequestDelegate RequestDelegate { get; set; } - - public RouteValueDictionary RequiredValues { get; set; } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/SimpleEndpoint.cs b/src/Microsoft.AspNetCore.Dispatcher/SimpleEndpoint.cs new file mode 100644 index 0000000000..667667456b --- /dev/null +++ b/src/Microsoft.AspNetCore.Dispatcher/SimpleEndpoint.cs @@ -0,0 +1,73 @@ +// 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.Linq; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Dispatcher +{ + public class SimpleEndpoint : Endpoint + { + public SimpleEndpoint(RequestDelegate requestDelegate) + : this(requestDelegate, Array.Empty(), null) + { + } + + public SimpleEndpoint(Func delegateFactory) + : this(delegateFactory, Array.Empty(), null) + { + } + + public SimpleEndpoint(RequestDelegate requestDelegate, IEnumerable metadata) + : this(requestDelegate, metadata, null) + { + } + + public SimpleEndpoint(Func delegateFactory, IEnumerable metadata) + : this(delegateFactory, metadata, null) + { + } + + public SimpleEndpoint(RequestDelegate requestDelegate, IEnumerable metadata, string displayName) + { + if (metadata == null) + { + throw new ArgumentNullException(nameof(metadata)); + } + + if (requestDelegate == null) + { + throw new ArgumentNullException(nameof(requestDelegate)); + } + + DisplayName = displayName; + Metadata = metadata.ToArray(); + DelegateFactory = (next) => requestDelegate; + } + + public SimpleEndpoint(Func delegateFactory, IEnumerable metadata, string displayName) + { + if (metadata == null) + { + throw new ArgumentNullException(nameof(metadata)); + } + + if (delegateFactory == null) + { + throw new ArgumentNullException(nameof(delegateFactory)); + } + + DisplayName = displayName; + Metadata = metadata.ToArray(); + DelegateFactory = delegateFactory; + } + + public override string DisplayName { get; } + + public override IReadOnlyList Metadata { get; } + + public Func DelegateFactory { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Routing/Dispatcher/RouteValuesEndpoint.cs b/src/Microsoft.AspNetCore.Routing/Dispatcher/RouteValuesEndpoint.cs new file mode 100644 index 0000000000..71b0c746e4 --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/Dispatcher/RouteValuesEndpoint.cs @@ -0,0 +1,59 @@ +// 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.Linq; +using Microsoft.AspNetCore.Dispatcher; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Routing.Dispatcher +{ + public class RouteValuesEndpoint : Endpoint + { + public RouteValuesEndpoint(RouteValueDictionary requiredValues, RequestDelegate requestDelegate) + : this(requiredValues, requestDelegate, Array.Empty(), null) + { + } + + public RouteValuesEndpoint(RouteValueDictionary requiredValues, RequestDelegate requestDelegate, IEnumerable metadata) + : this(requiredValues, requestDelegate, metadata, null) + { + } + + public RouteValuesEndpoint( + RouteValueDictionary requiredValues, + RequestDelegate requestDelegate, + IEnumerable metadata, + string displayName) + { + if (requiredValues == null) + { + throw new ArgumentNullException(nameof(requiredValues)); + } + + if (requestDelegate == null) + { + throw new ArgumentNullException(nameof(requestDelegate)); + } + + if (metadata == null) + { + throw new ArgumentNullException(nameof(metadata)); + } + + RequiredValues = requiredValues; + HandlerFactory = (next) => requestDelegate; + Metadata = metadata.ToArray(); + DisplayName = displayName; + } + + public override string DisplayName { get; } + + public override IReadOnlyList Metadata { get; } + + public Func HandlerFactory { get; set; } + + public RouteValueDictionary RequiredValues { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.Routing/Dispatcher/RouterDispatcher.cs b/src/Microsoft.AspNetCore.Routing/Dispatcher/RouterDispatcher.cs new file mode 100644 index 0000000000..54b3e97a06 --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/Dispatcher/RouterDispatcher.cs @@ -0,0 +1,40 @@ +// 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; +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 IRouter _router; + + public RouterDispatcher(IRouter router) + { + if (router == null) + { + throw new ArgumentNullException(nameof(router)); + } + + _router = router; + } + + public async override Task InvokeAsync(HttpContext httpContext) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + var routeContext = new RouteContext(httpContext); + await _router.RouteAsync(routeContext); + } + } +} + diff --git a/src/Microsoft.AspNetCore.Routing/Dispatcher/RouterEndpointSelector.cs b/src/Microsoft.AspNetCore.Routing/Dispatcher/RouterEndpointSelector.cs new file mode 100644 index 0000000000..67c1e5e92e --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/Dispatcher/RouterEndpointSelector.cs @@ -0,0 +1,110 @@ +// 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.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Dispatcher; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Routing.Dispatcher +{ + public class RouterEndpointSelector : IRouter, IRouteHandler + { + private readonly RouteValuesEndpoint[] _endpoints; + + public RouterEndpointSelector(IEnumerable endpoints) + { + if (endpoints == null) + { + throw new ArgumentNullException(nameof(endpoints)); + } + + _endpoints = endpoints.ToArray(); + } + + public RequestDelegate GetRequestHandler(HttpContext httpContext, RouteData routeData) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + if (routeData == null) + { + throw new ArgumentNullException(nameof(routeData)); + } + + var dispatcherFeature = httpContext.Features.Get(); + if (dispatcherFeature == null) + { + throw new InvalidOperationException(Resources.FormatDispatcherFeatureIsRequired( + nameof(HttpContext), + nameof(IDispatcherFeature), + nameof(RouterEndpointSelector))); + } + + for (var i = 0; i < _endpoints.Length; i++) + { + var endpoint = _endpoints[i]; + if (CompareRouteValues(routeData.Values, endpoint.RequiredValues)) + { + dispatcherFeature.Endpoint = endpoint; + return null; + } + } + + return null; + } + + public VirtualPathData GetVirtualPath(VirtualPathContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return null; + } + + public Task RouteAsync(RouteContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var handler = GetRequestHandler(context.HttpContext, context.RouteData); + if (handler != null) + { + context.Handler = handler; + } + + return Task.CompletedTask; + } + + private bool CompareRouteValues(RouteValueDictionary values, RouteValueDictionary 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/Microsoft.AspNetCore.Routing.csproj b/src/Microsoft.AspNetCore.Routing/Microsoft.AspNetCore.Routing.csproj index 273076621d..9b08d3c8e8 100644 --- a/src/Microsoft.AspNetCore.Routing/Microsoft.AspNetCore.Routing.csproj +++ b/src/Microsoft.AspNetCore.Routing/Microsoft.AspNetCore.Routing.csproj @@ -17,6 +17,8 @@ Microsoft.AspNetCore.Routing.RouteCollection + + diff --git a/src/Microsoft.AspNetCore.Routing/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Routing/Properties/Resources.Designer.cs index 5cb82620c1..cf107358c3 100644 --- a/src/Microsoft.AspNetCore.Routing/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Routing/Properties/Resources.Designer.cs @@ -402,6 +402,20 @@ namespace Microsoft.AspNetCore.Routing internal static string FormatTemplateRoute_Exception(object p0, object p1) => string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_Exception"), p0, p1); + /// + /// The '{0}' has no '{1}'. '{2}' requires a dispatcher. + /// + internal static string DispatcherFeatureIsRequired + { + get => GetString("DispatcherFeatureIsRequired"); + } + + /// + /// The '{0}' has no '{1}'. '{2}' requires a dispatcher. + /// + internal static string FormatDispatcherFeatureIsRequired(object p0, object p1, object p2) + => string.Format(CultureInfo.CurrentCulture, GetString("DispatcherFeatureIsRequired"), p0, p1, p2); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNetCore.Routing/Resources.resx b/src/Microsoft.AspNetCore.Routing/Resources.resx index 96ecf1f445..ceb82ab3b6 100644 --- a/src/Microsoft.AspNetCore.Routing/Resources.resx +++ b/src/Microsoft.AspNetCore.Routing/Resources.resx @@ -1,17 +1,17 @@  - @@ -201,4 +201,7 @@ An error occurred while creating the route with name '{0}' and template '{1}'. + + The '{0}' has no '{1}'. '{2}' requires a dispatcher. + \ No newline at end of file