Add endpoint disambiguation

- Better sample of metadata
- Sample shows how conventional routing would work
- Added endpoint disambiguation
This commit is contained in:
Ryan Nowak 2017-09-21 15:35:06 -07:00
parent 57bf1494dd
commit 41f26dc69d
23 changed files with 753 additions and 267 deletions

View File

@ -11,6 +11,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
<PackageReference Include="Microsoft.AspNetCore.Hosting" />
<PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration" />
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" />

View File

@ -0,0 +1,86 @@
// 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;
namespace DispatcherSample
{
public class HttpMethodEndpointSelector : EndpointSelector
{
public override async Task SelectAsync(EndpointSelectorContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var snapshot = context.CreateSnapshot();
var fallback = new List<Endpoint>();
for (var i = context.Endpoints.Count - 1; i >= 0; i--)
{
var endpoint = context.Endpoints[i];
IHttpMethodMetadata metadata = null;
for (var j = endpoint.Metadata.Count - 1; j >= 0; j--)
{
metadata = endpoint.Metadata[j] as IHttpMethodMetadata;
if (metadata != null)
{
break;
}
}
if (metadata == null)
{
// No metadata.
fallback.Add(endpoint);
context.Endpoints.RemoveAt(i);
}
else if (Matches(metadata, context.HttpContext.Request.Method))
{
// Do thing, this one matches
}
else
{
// Not a match.
context.Endpoints.RemoveAt(i);
}
}
// Now the list of endpoints only contains those that have an HTTP method preference AND match the current
// request.
await context.InvokeNextAsync();
if (context.Endpoints.Count == 0)
{
// Nothing matched, do the fallback.
context.RestoreSnapshot(snapshot);
context.Endpoints.Clear();
for (var i = 0; i < fallback.Count; i++)
{
context.Endpoints.Add(fallback[i]);
}
await context.InvokeNextAsync();
}
}
private bool Matches(IHttpMethodMetadata metadata, string httpMethod)
{
for (var i = 0; i < metadata.AllowedMethods.Count; i++)
{
if (string.Equals(metadata.AllowedMethods[i], httpMethod, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
}
}

View File

@ -0,0 +1,23 @@
// 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;
namespace DispatcherSample
{
public class HttpMethodMetadata : IHttpMethodMetadata
{
public HttpMethodMetadata(string httpMethod)
{
if (httpMethod == null)
{
throw new ArgumentNullException(nameof(httpMethod));
}
AllowedMethods = new[] { httpMethod, };
}
public IReadOnlyList<string> AllowedMethods { get; }
}
}

View File

@ -0,0 +1,20 @@
// 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.
namespace DispatcherSample
{
public interface IAuthorizationPolicyMetadata
{
string Name { get; }
}
public class AuthorizationPolicyMetadata : IAuthorizationPolicyMetadata
{
public AuthorizationPolicyMetadata(string name)
{
Name = name;
}
public string Name { get; }
}
}

View File

@ -0,0 +1,20 @@
// 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.
namespace DispatcherSample
{
public interface ICorsPolicyMetadata
{
string Name { get; }
}
public class CorsPolicyMetadata : ICorsPolicyMetadata
{
public CorsPolicyMetadata(string name)
{
Name = name;
}
public string Name { get; }
}
}

View File

@ -0,0 +1,12 @@
// 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 DispatcherSample
{
public interface IHttpMethodMetadata
{
IReadOnlyList<string> AllowedMethods { get; }
}
}

View File

@ -2,6 +2,8 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Console;
namespace DispatcherSample
{
@ -13,6 +15,7 @@ namespace DispatcherSample
.UseIISIntegration()
.UseKestrel()
.UseStartup<Startup>()
.ConfigureLogging((c, b) => b.AddProvider(new ConsoleLoggerProvider((category, level) => true, includeScopes: false)))
.Build();
host.Run();

View File

@ -3,6 +3,7 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Dispatcher;
using Microsoft.AspNetCore.Hosting;
@ -10,6 +11,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Dispatcher;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace DispatcherSample
@ -26,45 +28,24 @@ namespace DispatcherSample
{
services.Configure<DispatcherOptions>(options =>
{
options.Dispatchers.Add(CreateDispatcher(
"{Endpoint=example}",
new RouteValuesEndpoint(
new RouteValueDictionary(new { Endpoint = "First" }),
async (context) =>
{
await context.Response.WriteAsync("Hello from the example!");
},
Array.Empty<object>(),
"example"),
new RouteValuesEndpoint(
new RouteValueDictionary(new { Endpoint = "Second" }),
async (context) =>
{
await context.Response.WriteAsync("Hello from the second example!");
},
Array.Empty<object>(),
"example2")));
options.Dispatchers.Add(new RouteTemplateDispatcher("{controller=Home}/{action=Index}/{id?}", ConstraintResolver)
{
Endpoints =
{
new SimpleEndpoint(Home_Index, Array.Empty<object>(), new { controller = "Home", action = "Index", }, "Home:Index()"),
new SimpleEndpoint(Home_About, Array.Empty<object>(), new { controller = "Home", action = "About", }, "Home:About()"),
new SimpleEndpoint(Admin_Index, Array.Empty<object>(), new { controller = "Admin", action = "Index", }, "Admin:Index()"),
new SimpleEndpoint(Admin_GetUsers, new object[] { new HttpMethodMetadata("GET"), new AuthorizationPolicyMetadata("Admin"), }, new { controller = "Admin", action = "Users", }, "Admin:GetUsers()"),
new SimpleEndpoint(Admin_EditUsers, new object[] { new HttpMethodMetadata("POST"), new AuthorizationPolicyMetadata("Admin"), }, new { controller = "Admin", action = "Users", }, "Admin:EditUsers()"),
},
Selectors =
{
new DispatcherValueEndpointSelector(),
new HttpMethodEndpointSelector(),
}
}.InvokeAsync);
options.Dispatchers.Add(CreateDispatcher(
"{Endpoint=example}/{Parameter=foo}",
new RouteValuesEndpoint(
new RouteValueDictionary(new { Endpoint = "First", Parameter = "param1" }),
async (context) =>
{
await context.Response.WriteAsync("Hello from the example for foo!");
},
Array.Empty<object>(),
"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<object>(),
"example2")));
options.HandlerFactories.Add((endpoint) => (endpoint as RouteValuesEndpoint)?.HandlerFactory);
options.HandlerFactories.Add((endpoint) => (endpoint as SimpleEndpoint)?.HandlerFactory);
});
services.AddSingleton<UrlGenerator>();
@ -72,35 +53,86 @@ namespace DispatcherSample
services.AddDispatcher();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILogger<Startup> logger)
{
app.Use(async (context, next) =>
{
await context.Response.WriteAsync("<p>Middleware 1</p>");
await next.Invoke();
});
app.UseDispatcher();
app.Use(async (context, next) =>
{
await context.Response.WriteAsync("<p>Middleware 2</p>");
logger.LogInformation("Executing fake CORS middleware");
var feature = context.Features.Get<IDispatcherFeature>();
var policy = feature.Endpoint?.Metadata.OfType<ICorsPolicyMetadata>().LastOrDefault();
logger.LogInformation("using CORS policy {PolicyName}", policy?.Name ?? "default");
await next.Invoke();
});
app.Use(async (context, next) =>
{
var urlGenerator = app.ApplicationServices.GetService<UrlGenerator>();
var url = urlGenerator.GenerateURL(new RouteValueDictionary(new { Movie = "The Lion King", Character = "Mufasa" }), context);
await context.Response.WriteAsync($"<p>Generated url: {url}</p>");
logger.LogInformation("Executing fake AuthZ middleware");
var feature = context.Features.Get<IDispatcherFeature>();
var policy = feature.Endpoint?.Metadata.OfType<IAuthorizationPolicyMetadata>().LastOrDefault();
if (policy != null)
{
logger.LogInformation("using Auth policy {PolicyName}", policy.Name);
}
await next.Invoke();
});
}
private static RequestDelegate CreateDispatcher(string routeTemplate, RouteValuesEndpoint endpoint, params RouteValuesEndpoint[] endpoints)
public static Task Home_Index(HttpContext httpContext)
{
var dispatcher = new RouterDispatcher(new Route(new RouterEndpointSelector(new[] { endpoint }.Concat(endpoints)), routeTemplate, ConstraintResolver));
return dispatcher.InvokeAsync;
var urlGenerator = httpContext.RequestServices.GetService<UrlGenerator>();
var url = urlGenerator.GenerateURL(new RouteValueDictionary(new { Movie = "The Lion King", Character = "Mufasa" }), httpContext);
return httpContext.Response.WriteAsync(
$"<html>" +
$"<body>" +
$"<p>Generated url: {url}</p>" +
$"</body>" +
$"</html>");
}
public static Task Home_About(HttpContext httpContext)
{
return httpContext.Response.WriteAsync(
$"<html>" +
$"<body>" +
$"<p>This is a dispatcher sample.</p>" +
$"</body>" +
$"</html>");
}
public static Task Admin_Index(HttpContext httpContext)
{
return httpContext.Response.WriteAsync(
$"<html>" +
$"<body>" +
$"<p>This is the admin page.</p>" +
$"</body>" +
$"</html>");
}
public static Task Admin_GetUsers(HttpContext httpContext)
{
return httpContext.Response.WriteAsync(
$"<html>" +
$"<body>" +
$"<p>Users: rynowak, jbagga</p>" +
$"</body>" +
$"</html>");
}
public static Task Admin_EditUsers(HttpContext httpContext)
{
return httpContext.Response.WriteAsync(
$"<html>" +
$"<body>" +
$"<p>blerp</p>" +
$"</body>" +
$"</html>");
}
}
}

View File

@ -10,5 +10,7 @@ namespace Microsoft.AspNetCore.Dispatcher
Endpoint Endpoint { get; set; }
RequestDelegate RequestDelegate { get; set; }
DispatcherValueCollection Values { get; set; }
}
}

View File

@ -1,6 +1,9 @@
// 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.Http;
@ -8,6 +11,71 @@ namespace Microsoft.AspNetCore.Dispatcher
{
public abstract class DispatcherBase
{
public abstract Task InvokeAsync(HttpContext httpContext);
private IList<Endpoint> _endpoints;
private IList<EndpointSelector> _endpointSelectors;
public virtual IList<Endpoint> Endpoints
{
get
{
if (_endpoints == null)
{
_endpoints = new List<Endpoint>();
}
return _endpoints;
}
}
public virtual IList<EndpointSelector> Selectors
{
get
{
if (_endpointSelectors == null)
{
_endpointSelectors = new List<EndpointSelector>();
}
return _endpointSelectors;
}
}
public virtual async Task InvokeAsync(HttpContext httpContext)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
var feature = httpContext.Features.Get<IDispatcherFeature>();
if (await TryMatchAsync(httpContext))
{
if (feature.RequestDelegate != null)
{
// Short circuit, no need to select an endpoint.
return;
}
var selectorContext = new EndpointSelectorContext(httpContext, Endpoints.ToList(), Selectors);
await selectorContext.InvokeNextAsync();
switch (selectorContext.Endpoints.Count)
{
case 0:
break;
case 1:
feature.Endpoint = selectorContext.Endpoints[0];
break;
default:
throw new InvalidOperationException("Ambiguous bro!");
}
}
}
protected abstract Task<bool> TryMatchAsync(HttpContext httpContext);
}
}

View File

@ -7,33 +7,10 @@ namespace Microsoft.AspNetCore.Dispatcher
{
public class DispatcherFeature : IDispatcherFeature
{
private Endpoint _endpoint;
private RequestDelegate _next;
public Endpoint Endpoint { get; set; }
public Endpoint Endpoint
{
get
{
return _endpoint;
}
public RequestDelegate RequestDelegate { get; set; }
set
{
_endpoint = value;
}
}
public RequestDelegate RequestDelegate
{
get
{
return _next;
}
set
{
_next = value;
}
}
public DispatcherValueCollection Values { get; set; }
}
}

View File

@ -4,28 +4,36 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Dispatcher
{
public class DispatcherMiddleware
{
private readonly ILogger _logger;
private readonly DispatcherOptions _options;
private readonly RequestDelegate _next;
public DispatcherMiddleware(IOptions<DispatcherOptions> options, RequestDelegate next)
public DispatcherMiddleware(IOptions<DispatcherOptions> options, ILogger<DispatcherMiddleware> logger, RequestDelegate next)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
if (logger == null)
{
throw new ArgumentNullException(nameof(logger));
}
if (next == null)
{
throw new ArgumentNullException(nameof(next));
}
_options = options.Value;
_logger = logger;
_next = next;
}
@ -39,6 +47,7 @@ namespace Microsoft.AspNetCore.Dispatcher
await entry(httpContext);
if (feature.Endpoint != null || feature.RequestDelegate != null)
{
_logger.LogInformation("Matched endpoint {Endpoint}", feature.Endpoint.DisplayName);
break;
}
}

View File

@ -0,0 +1,55 @@
// 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 DispatcherValueEndpointSelector : EndpointSelector
{
public override Task SelectAsync(EndpointSelectorContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var dispatcherFeature = context.HttpContext.Features.Get<IDispatcherFeature>();
for (var i = context.Endpoints.Count - 1; i >= 0; i--)
{
var endpoint = context.Endpoints[i] as IDispatcherValueSelectableEndpoint;
if (!CompareRouteValues(dispatcherFeature.Values, endpoint.Values))
{
context.Endpoints.RemoveAt(i);
}
}
return context.InvokeNextAsync();
}
private bool CompareRouteValues(DispatcherValueCollection values, DispatcherValueCollection requiredValues)
{
foreach (var kvp in requiredValues)
{
if (string.IsNullOrEmpty(kvp.Value.ToString()))
{
if (values.TryGetValue(kvp.Key, out var routeValue) && !string.IsNullOrEmpty(routeValue.ToString()))
{
return false;
}
}
else
{
if (!values.TryGetValue(kvp.Key, out var routeValue) || !string.Equals(kvp.Value.ToString(), routeValue.ToString(), StringComparison.OrdinalIgnoreCase))
{
return false;
}
}
}
return true;
}
}
}

View File

@ -4,28 +4,36 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Dispatcher
{
public class EndpointMiddleware
{
private readonly ILogger _logger;
private readonly DispatcherOptions _options;
private RequestDelegate _next;
private readonly RequestDelegate _next;
public EndpointMiddleware(IOptions<DispatcherOptions> options, RequestDelegate next)
public EndpointMiddleware(IOptions<DispatcherOptions> options, ILogger<DispatcherMiddleware> logger, RequestDelegate next)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
if (logger == null)
{
throw new ArgumentNullException(nameof(logger));
}
if (next == null)
{
throw new ArgumentNullException(nameof(next));
}
_options = options.Value;
_logger = logger;
_next = next;
}
@ -47,7 +55,17 @@ namespace Microsoft.AspNetCore.Dispatcher
if (feature.RequestDelegate != null)
{
await feature.RequestDelegate(context);
_logger.LogInformation("Executing endpoint {Endpoint}", feature.Endpoint.DisplayName);
try
{
await feature.RequestDelegate(context);
}
finally
{
_logger.LogInformation("Executed endpoint {Endpoint}", feature.Endpoint.DisplayName);
}
return;
}
await _next(context);

View File

@ -0,0 +1,12 @@
// 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;
namespace Microsoft.AspNetCore.Dispatcher
{
public abstract class EndpointSelector
{
public abstract Task SelectAsync(EndpointSelectorContext context);
}
}

View File

@ -0,0 +1,88 @@
// 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.Http;
namespace Microsoft.AspNetCore.Dispatcher
{
public sealed class EndpointSelectorContext
{
private int _index;
public EndpointSelectorContext(HttpContext httpContext, IList<Endpoint> endpoints, IList<EndpointSelector> selectors)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
if (endpoints == null)
{
throw new ArgumentNullException(nameof(endpoints));
}
if (selectors == null)
{
throw new ArgumentNullException(nameof(selectors));
}
HttpContext = httpContext;
Endpoints = endpoints;
Selectors = selectors;
}
public IList<Endpoint> Endpoints { get; }
public HttpContext HttpContext { get; }
public IList<EndpointSelector> Selectors { get; }
public Task InvokeNextAsync()
{
if (_index >= Selectors.Count)
{
return Task.CompletedTask;
}
var selector = Selectors[_index++];
return selector.SelectAsync(this);
}
public Snapshot CreateSnapshot()
{
return new Snapshot(_index, Endpoints);
}
public void RestoreSnapshot(Snapshot snapshot)
{
snapshot.Apply(this);
}
public struct Snapshot
{
private readonly int _index;
private readonly Endpoint[] _endpoints;
internal Snapshot(int index, IList<Endpoint> endpoints)
{
_index = index;
_endpoints = endpoints.ToArray();
}
internal void Apply(EndpointSelectorContext context)
{
context._index = _index;
context.Endpoints.Clear();
for (var i = 0; i < _endpoints.Length; i++)
{
context.Endpoints.Add(_endpoints[i]);
}
}
}
}
}

View File

@ -0,0 +1,10 @@
// 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.
namespace Microsoft.AspNetCore.Dispatcher
{
public interface IDispatcherValueSelectableEndpoint
{
DispatcherValueCollection Values { get; }
}
}

View File

@ -8,29 +8,34 @@ using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Dispatcher
{
public class SimpleEndpoint : Endpoint
public class SimpleEndpoint : Endpoint, IDispatcherValueSelectableEndpoint
{
public SimpleEndpoint(RequestDelegate requestDelegate)
: this(requestDelegate, Array.Empty<object>(), null)
: this(requestDelegate, Array.Empty<object>(), null, null)
{
}
public SimpleEndpoint(Func<RequestDelegate, RequestDelegate> delegateFactory)
: this(delegateFactory, Array.Empty<object>(), null)
: this(delegateFactory, Array.Empty<object>(), null, null)
{
}
public SimpleEndpoint(RequestDelegate requestDelegate, IEnumerable<object> metadata)
: this(requestDelegate, metadata, null)
: this(requestDelegate, metadata, null, null)
{
}
public SimpleEndpoint(Func<RequestDelegate, RequestDelegate> delegateFactory, IEnumerable<object> metadata)
: this(delegateFactory, metadata, null)
: this(delegateFactory, metadata, null, null)
{
}
public SimpleEndpoint(RequestDelegate requestDelegate, IEnumerable<object> metadata, string displayName)
public SimpleEndpoint(Func<RequestDelegate, RequestDelegate> delegateFactory, IEnumerable<object> metadata, object values)
: this(delegateFactory, metadata, null, null)
{
}
public SimpleEndpoint(RequestDelegate requestDelegate, IEnumerable<object> metadata, object values, string displayName)
{
if (metadata == null)
{
@ -42,12 +47,13 @@ namespace Microsoft.AspNetCore.Dispatcher
throw new ArgumentNullException(nameof(requestDelegate));
}
DisplayName = displayName;
HandlerFactory = (next) => requestDelegate;
Metadata = metadata.ToArray();
DelegateFactory = (next) => requestDelegate;
Values = new DispatcherValueCollection(values);
DisplayName = displayName;
}
public SimpleEndpoint(Func<RequestDelegate, RequestDelegate> delegateFactory, IEnumerable<object> metadata, string displayName)
public SimpleEndpoint(Func<RequestDelegate, RequestDelegate> delegateFactory, IEnumerable<object> metadata, object values, string displayName)
{
if (metadata == null)
{
@ -59,15 +65,18 @@ namespace Microsoft.AspNetCore.Dispatcher
throw new ArgumentNullException(nameof(delegateFactory));
}
DisplayName = displayName;
HandlerFactory = delegateFactory;
Metadata = metadata.ToArray();
DelegateFactory = delegateFactory;
Values = new DispatcherValueCollection(values);
DisplayName = displayName;
}
public override string DisplayName { get; }
public override IReadOnlyList<object> Metadata { get; }
public Func<RequestDelegate, RequestDelegate> DelegateFactory { get; }
public Func<RequestDelegate, RequestDelegate> HandlerFactory { get; }
public DispatcherValueCollection Values { get; }
}
}

View File

@ -0,0 +1,21 @@
// 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.Routing;
namespace Microsoft.AspNetCore.Dispatcher
{
public static class DispatcherValueCollectionExtensions
{
public static RouteValueDictionary AsRouteValueDictionary(this DispatcherValueCollection values)
{
if (values == null)
{
throw new ArgumentNullException(nameof(values));
}
return values as RouteValueDictionary ?? new RouteValueDictionary(values);
}
}
}

View File

@ -0,0 +1,157 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Dispatcher;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Template;
namespace Microsoft.AspNetCore.Routing.Dispatcher
{
public class RouteTemplateDispatcher : DispatcherBase
{
private readonly IDictionary<string, IRouteConstraint> _constraints;
private readonly RouteValueDictionary _defaults;
private readonly TemplateMatcher _matcher;
private readonly RouteTemplate _parsedTemplate;
public RouteTemplateDispatcher(
string routeTemplate,
IInlineConstraintResolver constraintResolver)
: this(routeTemplate, constraintResolver, null, null)
{
}
public RouteTemplateDispatcher(
string routeTemplate,
IInlineConstraintResolver constraintResolver,
RouteValueDictionary defaults)
: this(routeTemplate, constraintResolver, defaults, null)
{
}
public RouteTemplateDispatcher(
string routeTemplate,
IInlineConstraintResolver constraintResolver,
RouteValueDictionary defaults,
IDictionary<string, object> constraints)
{
if (routeTemplate == null)
{
throw new ArgumentNullException(nameof(routeTemplate));
}
if (constraintResolver == null)
{
throw new ArgumentNullException(nameof(constraintResolver));
}
RouteTemplate = routeTemplate;
try
{
// Data we parse from the template will be used to fill in the rest of the constraints or
// defaults. The parser will throw for invalid routes.
_parsedTemplate = TemplateParser.Parse(routeTemplate);
_constraints = GetConstraints(constraintResolver, _parsedTemplate, constraints);
_defaults = GetDefaults(_parsedTemplate, defaults);
}
catch (Exception exception)
{
throw new RouteCreationException(Resources.FormatTemplateRoute_Exception(string.Empty, routeTemplate), exception);
}
_matcher = new TemplateMatcher(_parsedTemplate, _defaults);
}
public string RouteTemplate { get; }
protected override Task<bool> TryMatchAsync(HttpContext httpContext)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
var feature = httpContext.Features.Get<IDispatcherFeature>();
feature.Values = feature.Values ?? new RouteValueDictionary();
if (!_matcher.TryMatch(httpContext.Request.Path, (RouteValueDictionary)feature.Values))
{
// If we got back a null value set, that means the URI did not match
return Task.FromResult(false);
}
foreach (var kvp in _constraints)
{
var constraint = kvp.Value;
if (!constraint.Match(httpContext, null, kvp.Key, (RouteValueDictionary)feature.Values, RouteDirection.IncomingRequest))
{
return Task.FromResult(false);
}
}
return Task.FromResult(true);
}
private static IDictionary<string, IRouteConstraint> GetConstraints(
IInlineConstraintResolver inlineConstraintResolver,
RouteTemplate parsedTemplate,
IDictionary<string, object> constraints)
{
var constraintBuilder = new RouteConstraintBuilder(inlineConstraintResolver, parsedTemplate.TemplateText);
if (constraints != null)
{
foreach (var kvp in constraints)
{
constraintBuilder.AddConstraint(kvp.Key, kvp.Value);
}
}
foreach (var parameter in parsedTemplate.Parameters)
{
if (parameter.IsOptional)
{
constraintBuilder.SetOptional(parameter.Name);
}
foreach (var inlineConstraint in parameter.InlineConstraints)
{
constraintBuilder.AddResolvedConstraint(parameter.Name, inlineConstraint.Constraint);
}
}
return constraintBuilder.Build();
}
private static RouteValueDictionary GetDefaults(
RouteTemplate parsedTemplate,
RouteValueDictionary defaults)
{
var result = defaults == null ? new RouteValueDictionary() : new RouteValueDictionary(defaults);
foreach (var parameter in parsedTemplate.Parameters)
{
if (parameter.DefaultValue != null)
{
if (result.ContainsKey(parameter.Name))
{
throw new InvalidOperationException(
Resources.FormatTemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly(
parameter.Name));
}
else
{
result.Add(parameter.Name, parameter.DefaultValue);
}
}
}
return result;
}
}
}

View File

@ -1,59 +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.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

@ -2,6 +2,7 @@
// 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;
@ -13,6 +14,7 @@ namespace Microsoft.AspNetCore.Routing.Dispatcher
/// </summary>
public class RouterDispatcher : DispatcherBase
{
private readonly Endpoint _fallbackEndpoint;
private readonly IRouter _router;
public RouterDispatcher(IRouter router)
@ -23,17 +25,47 @@ namespace Microsoft.AspNetCore.Routing.Dispatcher
}
_router = router;
_fallbackEndpoint = new UnknownEndpoint(_router);
}
public async override Task InvokeAsync(HttpContext httpContext)
protected override async Task<bool> TryMatchAsync(HttpContext httpContext)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
var routeContext = new RouteContext(httpContext);
await _router.RouteAsync(routeContext);
var feature = httpContext.Features.Get<IDispatcherFeature>();
if (routeContext.Handler == null)
{
// The route did not match, clear everything as it may have been set by the route.
feature.Endpoint = null;
feature.RequestDelegate = null;
feature.Values = null;
return false;
}
else
{
feature.Endpoint = feature.Endpoint ?? _fallbackEndpoint;
feature.RequestDelegate = routeContext.Handler;
feature.Values = routeContext.RouteData.Values;
return true;
}
}
private class UnknownEndpoint : Endpoint
{
public UnknownEndpoint(IRouter router)
{
DisplayName = $"Endpoint for '{router}";
}
public override string DisplayName { get; }
public override IReadOnlyList<object> Metadata => Array.Empty<object>();
}
}
}

View File

@ -1,110 +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.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;
}
}
}