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