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.
This commit is contained in:
Ryan Nowak 2017-09-17 00:06:58 -07:00
parent b01072eb47
commit 134096d9cb
21 changed files with 462 additions and 216 deletions

View File

@ -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; }
}
}

View File

@ -6,9 +6,6 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.Dispatcher\Microsoft.AspNetCore.Dispatcher.csproj" />
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.Dispatcher.Abstractions\Microsoft.AspNetCore.Dispatcher.Abstractions.csproj" />
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.Routing.Abstractions\Microsoft.AspNetCore.Routing.Abstractions.csproj" />
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.Routing\Microsoft.AspNetCore.Routing.csproj" /> <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Routing\Microsoft.AspNetCore.Routing.csproj" />
</ItemGroup> </ItemGroup>

View File

@ -1,73 +1,70 @@
// Copyright (c) .NET Foundation. All rights reserved. // Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic; using System;
using System.Linq;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Dispatcher; using Microsoft.AspNetCore.Dispatcher;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Template; using Microsoft.AspNetCore.Routing.Dispatcher;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace DispatcherSample namespace DispatcherSample
{ {
public class Startup public class Startup
{ {
private readonly static IInlineConstraintResolver ConstraintResolver = new DefaultInlineConstraintResolver(
new OptionsManager<RouteOptions>(
new OptionsFactory<RouteOptions>(
Enumerable.Empty<IConfigureOptions<RouteOptions>>(),
Enumerable.Empty<IPostConfigureOptions<RouteOptions>>())));
public void ConfigureServices(IServiceCollection services) public void ConfigureServices(IServiceCollection services)
{ {
services.Configure<DispatcherOptions>(options => services.Configure<DispatcherOptions>(options =>
{ {
options.DispatcherEntryList = new List<DispatcherEntry>() options.Dispatchers.Add(CreateDispatcher(
{ "{Endpoint=example}",
new DispatcherEntry new RouteValuesEndpoint(
{ new RouteValueDictionary(new { Endpoint = "First" }),
RouteTemplate = TemplateParser.Parse("{Endpoint=example}"), async (context) =>
Endpoints = new []
{ {
new RouteValuesEndpoint("example") await context.Response.WriteAsync("Hello from the example!");
{ },
RequiredValues = new RouteValueDictionary(new { Endpoint = "First" }), Array.Empty<object>(),
RequestDelegate = async (context) => "example"),
{ new RouteValuesEndpoint(
await context.Response.WriteAsync("Hello from the example!"); new RouteValueDictionary(new { Endpoint = "Second" }),
} async (context) =>
}, {
new RouteValuesEndpoint("example2") await context.Response.WriteAsync("Hello from the second example!");
{ },
RequiredValues = new RouteValueDictionary(new { Endpoint = "Second" }), Array.Empty<object>(),
RequestDelegate = async (context) => "example2")));
{
await context.Response.WriteAsync("Hello from the second example!");
}
},
}
},
new DispatcherEntry options.Dispatchers.Add(CreateDispatcher(
{ "{Endpoint=example}/{Parameter=foo}",
RouteTemplate = TemplateParser.Parse("{Endpoint=example}/{Parameter=foo}"), new RouteValuesEndpoint(
Endpoints = new [] new RouteValueDictionary(new { Endpoint = "First", Parameter = "param1" }),
async (context) =>
{ {
new RouteValuesEndpoint("example") await context.Response.WriteAsync("Hello from the example for foo!");
{ },
RequiredValues = new RouteValueDictionary(new { Endpoint = "First", Parameter = "param1"}), Array.Empty<object>(),
RequestDelegate = async (context) => "example"),
{ new RouteValuesEndpoint(
await context.Response.WriteAsync("Hello from the example for foo!"); new RouteValueDictionary(new { Endpoint = "Second", Parameter = "param2" }),
} async (context) =>
}, {
new RouteValuesEndpoint("example2") await context.Response.WriteAsync("Hello from the second example for foo!");
{ },
RequiredValues = new RouteValueDictionary(new { Endpoint = "Second", Parameter = "param2"}), Array.Empty<object>(),
RequestDelegate = async (context) => "example2")));
{
await context.Response.WriteAsync("Hello from the second example for foo!"); options.HandlerFactories.Add((endpoint) => (endpoint as RouteValuesEndpoint)?.HandlerFactory);
}
},
}
}
};
}); });
services.AddSingleton<UrlGenerator>(); services.AddSingleton<UrlGenerator>();
@ -99,5 +96,11 @@ namespace DispatcherSample
await next.Invoke(); 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;
}
} }
} }

View File

@ -1,10 +1,14 @@
// Copyright (c) .NET Foundation. All rights reserved. // Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
namespace Microsoft.AspNetCore.Dispatcher namespace Microsoft.AspNetCore.Dispatcher
{ {
public abstract class Endpoint public abstract class Endpoint
{ {
public abstract string DisplayName { get; } public abstract string DisplayName { get; }
public abstract IReadOnlyList<object> Metadata { get; }
} }
} }

View File

@ -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
{
/// <summary>
/// A delegate which attempts to create a <see cref="Func{RequestDelegate, RequestDelegate}"/> for the selected <see cref="Endpoint"/>.
/// </summary>
/// <param name="endpoint">The <see cref="Endpoint"/> selected by the dispatcher.</param>
/// <returns>
/// A <see cref="Func{RequestDelegate, RequestDelegate}"/> that invokes the operation represented by the <see cref="Endpoint"/>, or <c>null</c>.
/// </returns>
public delegate Func<RequestDelegate, RequestDelegate> EndpointHandlerFactory(Endpoint endpoint);
}

View File

@ -7,8 +7,8 @@ namespace Microsoft.AspNetCore.Dispatcher
{ {
public interface IDispatcherFeature public interface IDispatcherFeature
{ {
Endpoint Endpoint { get; } Endpoint Endpoint { get; set; }
RequestDelegate RequestDelegate { get; } RequestDelegate RequestDelegate { get; set; }
} }
} }

View File

@ -8,7 +8,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" /> <PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -1,15 +1,13 @@
// Copyright (c) .NET Foundation. All rights reserved. // Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Dispatcher namespace Microsoft.AspNetCore.Dispatcher
{ {
public class DispatcherEndpoint : Endpoint public abstract class DispatcherBase
{ {
public DispatcherEndpoint(string displayName) public abstract Task InvokeAsync(HttpContext httpContext);
{
DisplayName = displayName;
}
public override string DisplayName { get; }
} }
} }

View File

@ -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<RouteValuesEndpoint> Endpoints { get; set; }
public RouteTemplate RouteTemplate { get; set; }
}
}

View File

@ -4,8 +4,6 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Template;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Dispatcher namespace Microsoft.AspNetCore.Dispatcher
@ -17,82 +15,35 @@ namespace Microsoft.AspNetCore.Dispatcher
public DispatcherMiddleware(IOptions<DispatcherOptions> options, RequestDelegate next) public DispatcherMiddleware(IOptions<DispatcherOptions> options, RequestDelegate next)
{ {
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
if (next == null)
{
throw new ArgumentNullException(nameof(next));
}
_options = options.Value; _options = options.Value;
_next = next; _next = next;
} }
public async Task Invoke(HttpContext httpContext) public async Task Invoke(HttpContext httpContext)
{ {
foreach (var entry in _options.DispatcherEntryList) var feature = new DispatcherFeature();
httpContext.Features.Set<IDispatcherFeature>(feature);
foreach (var entry in _options.Dispatchers)
{ {
var parsedTemplate = entry.RouteTemplate; await entry(httpContext);
var defaults = GetDefaults(parsedTemplate); if (feature.Endpoint != null || feature.RequestDelegate != null)
var templateMatcher = new TemplateMatcher(parsedTemplate, defaults);
var values = new RouteValueDictionary();
foreach (var endpoint in entry.Endpoints)
{ {
if (templateMatcher.TryMatch(httpContext.Request.Path, values)) break;
{
if (!CompareRouteValues(values, endpoint.RequiredValues))
{
values.Clear();
}
else
{
var dispatcherFeature = new DispatcherFeature
{
Endpoint = endpoint,
RequestDelegate = endpoint.RequestDelegate
};
httpContext.Features.Set<IDispatcherFeature>(dispatcherFeature);
break;
}
}
} }
} }
await _next(httpContext); 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;
}
} }
} }

View File

@ -1,12 +1,15 @@
// Copyright (c) .NET Foundation. All rights reserved. // Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Http;
using System.Collections.Generic; using System.Collections.Generic;
namespace Microsoft.AspNetCore.Dispatcher namespace Microsoft.AspNetCore.Dispatcher
{ {
public class DispatcherOptions public class DispatcherOptions
{ {
public IList<DispatcherEntry> DispatcherEntryList { get; set; } public IList<RequestDelegate> Dispatchers { get; } = new List<RequestDelegate>();
public IList<EndpointHandlerFactory> HandlerFactories { get; } = new List<EndpointHandlerFactory>();
} }
} }

View File

@ -1,31 +1,56 @@
// Copyright (c) .NET Foundation. All rights reserved. // Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Dispatcher namespace Microsoft.AspNetCore.Dispatcher
{ {
public class EndpointMiddleware public class EndpointMiddleware
{ {
private readonly DispatcherOptions _options;
private RequestDelegate _next; private RequestDelegate _next;
public EndpointMiddleware(RequestDelegate next) public EndpointMiddleware(IOptions<DispatcherOptions> options, RequestDelegate next)
{ {
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
if (next == null)
{
throw new ArgumentNullException(nameof(next));
}
_options = options.Value;
_next = next; _next = next;
} }
public async Task Invoke(HttpContext context) public async Task Invoke(HttpContext context)
{ {
var feature = context.Features.Get<IDispatcherFeature>(); var feature = context.Features.Get<IDispatcherFeature>();
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 feature.RequestDelegate(context);
} }
await _next(context);
} }
} }
} }

View File

@ -9,12 +9,13 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Microsoft.AspNetCore.Dispatcher.Abstractions\Microsoft.AspNetCore.Dispatcher.Abstractions.csproj" /> <ProjectReference Include="..\Microsoft.AspNetCore.Dispatcher.Abstractions\Microsoft.AspNetCore.Dispatcher.Abstractions.csproj" />
<ProjectReference Include="..\Microsoft.AspNetCore.Routing\Microsoft.AspNetCore.Routing.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Hosting" /> <PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" />
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" /> <PackageReference Include="Microsoft.AspNetCore.Http.Extensions" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Options" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -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; }
}
}

View File

@ -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<object>(), null)
{
}
public SimpleEndpoint(Func<RequestDelegate, RequestDelegate> delegateFactory)
: this(delegateFactory, Array.Empty<object>(), null)
{
}
public SimpleEndpoint(RequestDelegate requestDelegate, IEnumerable<object> metadata)
: this(requestDelegate, metadata, null)
{
}
public SimpleEndpoint(Func<RequestDelegate, RequestDelegate> delegateFactory, IEnumerable<object> metadata)
: this(delegateFactory, metadata, null)
{
}
public SimpleEndpoint(RequestDelegate requestDelegate, IEnumerable<object> 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<RequestDelegate, RequestDelegate> delegateFactory, IEnumerable<object> 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<object> Metadata { get; }
public Func<RequestDelegate, RequestDelegate> DelegateFactory { get; }
}
}

View File

@ -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<object>(), null)
{
}
public RouteValuesEndpoint(RouteValueDictionary requiredValues, RequestDelegate requestDelegate, IEnumerable<object> metadata)
: this(requiredValues, requestDelegate, metadata, null)
{
}
public RouteValuesEndpoint(
RouteValueDictionary requiredValues,
RequestDelegate requestDelegate,
IEnumerable<object> 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<object> Metadata { get; }
public Func<RequestDelegate, RequestDelegate> HandlerFactory { get; set; }
public RouteValueDictionary RequiredValues { get; set; }
}
}

View File

@ -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
{
/// <summary>
/// An adapter to plug an <see cref="IRouter"/> into a dispatcher.
/// </summary>
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);
}
}
}

View File

@ -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<RouteValuesEndpoint> 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<IDispatcherFeature>();
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;
}
}
}

View File

@ -17,6 +17,8 @@ Microsoft.AspNetCore.Routing.RouteCollection</Description>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Microsoft.AspNetCore.Routing.Abstractions\Microsoft.AspNetCore.Routing.Abstractions.csproj" /> <ProjectReference Include="..\Microsoft.AspNetCore.Routing.Abstractions\Microsoft.AspNetCore.Routing.Abstractions.csproj" />
<ProjectReference Include="..\Microsoft.AspNetCore.Dispatcher.Abstractions\Microsoft.AspNetCore.Dispatcher.Abstractions.csproj" />
<ProjectReference Include="..\Microsoft.AspNetCore.Dispatcher\Microsoft.AspNetCore.Dispatcher.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -402,6 +402,20 @@ namespace Microsoft.AspNetCore.Routing
internal static string FormatTemplateRoute_Exception(object p0, object p1) internal static string FormatTemplateRoute_Exception(object p0, object p1)
=> string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_Exception"), p0, p1); => string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_Exception"), p0, p1);
/// <summary>
/// The '{0}' has no '{1}'. '{2}' requires a dispatcher.
/// </summary>
internal static string DispatcherFeatureIsRequired
{
get => GetString("DispatcherFeatureIsRequired");
}
/// <summary>
/// The '{0}' has no '{1}'. '{2}' requires a dispatcher.
/// </summary>
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) private static string GetString(string name, params string[] formatterNames)
{ {
var value = _resourceManager.GetString(name); var value = _resourceManager.GetString(name);

View File

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!-- <!--
Microsoft ResX Schema Microsoft ResX Schema
Version 2.0 Version 2.0
The primary goals of this format is to allow a simple XML format The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes various data types are done through the TypeConverter classes
associated with the data types. associated with the data types.
Example: Example:
... ado.net/XML headers & schema ... ... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader> <resheader name="version">2.0</resheader>
@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment> <comment>This is a comment</comment>
</data> </data>
There are any number of "resheader" rows that contain simple There are any number of "resheader" rows that contain simple
name/value pairs. name/value pairs.
Each data row contains a name, and value. The row also contains a Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture. text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the Classes that don't support this are serialized and stored with the
mimetype set. mimetype set.
The mimetype is used for serialized objects, and tells the The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly: extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below. read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64 mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64 mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64 mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter : using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
--> -->
@ -201,4 +201,7 @@
<data name="TemplateRoute_Exception" xml:space="preserve"> <data name="TemplateRoute_Exception" xml:space="preserve">
<value>An error occurred while creating the route with name '{0}' and template '{1}'.</value> <value>An error occurred while creating the route with name '{0}' and template '{1}'.</value>
</data> </data>
<data name="DispatcherFeatureIsRequired" xml:space="preserve">
<value>The '{0}' has no '{1}'. '{2}' requires a dispatcher.</value>
</data>
</root> </root>