Add endpoint disambiguation
- Better sample of metadata - Sample shows how conventional routing would work - Added endpoint disambiguation
This commit is contained in:
parent
57bf1494dd
commit
41f26dc69d
|
|
@ -11,6 +11,7 @@
|
||||||
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Hosting" />
|
<PackageReference Include="Microsoft.AspNetCore.Hosting" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration" />
|
<PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" />
|
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" />
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
// 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.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Logging.Console;
|
||||||
|
|
||||||
namespace DispatcherSample
|
namespace DispatcherSample
|
||||||
{
|
{
|
||||||
|
|
@ -13,6 +15,7 @@ namespace DispatcherSample
|
||||||
.UseIISIntegration()
|
.UseIISIntegration()
|
||||||
.UseKestrel()
|
.UseKestrel()
|
||||||
.UseStartup<Startup>()
|
.UseStartup<Startup>()
|
||||||
|
.ConfigureLogging((c, b) => b.AddProvider(new ConsoleLoggerProvider((category, level) => true, includeScopes: false)))
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
host.Run();
|
host.Run();
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Dispatcher;
|
using Microsoft.AspNetCore.Dispatcher;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
|
@ -10,6 +11,7 @@ using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
using Microsoft.AspNetCore.Routing.Dispatcher;
|
using Microsoft.AspNetCore.Routing.Dispatcher;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace DispatcherSample
|
namespace DispatcherSample
|
||||||
|
|
@ -26,45 +28,24 @@ namespace DispatcherSample
|
||||||
{
|
{
|
||||||
services.Configure<DispatcherOptions>(options =>
|
services.Configure<DispatcherOptions>(options =>
|
||||||
{
|
{
|
||||||
options.Dispatchers.Add(CreateDispatcher(
|
options.Dispatchers.Add(new RouteTemplateDispatcher("{controller=Home}/{action=Index}/{id?}", ConstraintResolver)
|
||||||
"{Endpoint=example}",
|
{
|
||||||
new RouteValuesEndpoint(
|
Endpoints =
|
||||||
new RouteValueDictionary(new { Endpoint = "First" }),
|
{
|
||||||
async (context) =>
|
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()"),
|
||||||
await context.Response.WriteAsync("Hello from the example!");
|
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()"),
|
||||||
Array.Empty<object>(),
|
new SimpleEndpoint(Admin_EditUsers, new object[] { new HttpMethodMetadata("POST"), new AuthorizationPolicyMetadata("Admin"), }, new { controller = "Admin", action = "Users", }, "Admin:EditUsers()"),
|
||||||
"example"),
|
},
|
||||||
new RouteValuesEndpoint(
|
Selectors =
|
||||||
new RouteValueDictionary(new { Endpoint = "Second" }),
|
{
|
||||||
async (context) =>
|
new DispatcherValueEndpointSelector(),
|
||||||
{
|
new HttpMethodEndpointSelector(),
|
||||||
await context.Response.WriteAsync("Hello from the second example!");
|
}
|
||||||
},
|
}.InvokeAsync);
|
||||||
Array.Empty<object>(),
|
|
||||||
"example2")));
|
|
||||||
|
|
||||||
options.Dispatchers.Add(CreateDispatcher(
|
options.HandlerFactories.Add((endpoint) => (endpoint as SimpleEndpoint)?.HandlerFactory);
|
||||||
"{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);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
services.AddSingleton<UrlGenerator>();
|
services.AddSingleton<UrlGenerator>();
|
||||||
|
|
@ -72,35 +53,86 @@ namespace DispatcherSample
|
||||||
services.AddDispatcher();
|
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.UseDispatcher();
|
||||||
|
|
||||||
app.Use(async (context, next) =>
|
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();
|
await next.Invoke();
|
||||||
});
|
});
|
||||||
|
|
||||||
app.Use(async (context, next) =>
|
app.Use(async (context, next) =>
|
||||||
{
|
{
|
||||||
var urlGenerator = app.ApplicationServices.GetService<UrlGenerator>();
|
logger.LogInformation("Executing fake AuthZ middleware");
|
||||||
var url = urlGenerator.GenerateURL(new RouteValueDictionary(new { Movie = "The Lion King", Character = "Mufasa" }), context);
|
|
||||||
await context.Response.WriteAsync($"<p>Generated url: {url}</p>");
|
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();
|
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));
|
var urlGenerator = httpContext.RequestServices.GetService<UrlGenerator>();
|
||||||
return dispatcher.InvokeAsync;
|
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>");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,5 +10,7 @@ namespace Microsoft.AspNetCore.Dispatcher
|
||||||
Endpoint Endpoint { get; set; }
|
Endpoint Endpoint { get; set; }
|
||||||
|
|
||||||
RequestDelegate RequestDelegate { get; set; }
|
RequestDelegate RequestDelegate { get; set; }
|
||||||
|
|
||||||
|
DispatcherValueCollection Values { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
// 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.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
|
|
@ -8,6 +11,71 @@ namespace Microsoft.AspNetCore.Dispatcher
|
||||||
{
|
{
|
||||||
public abstract class DispatcherBase
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,33 +7,10 @@ namespace Microsoft.AspNetCore.Dispatcher
|
||||||
{
|
{
|
||||||
public class DispatcherFeature : IDispatcherFeature
|
public class DispatcherFeature : IDispatcherFeature
|
||||||
{
|
{
|
||||||
private Endpoint _endpoint;
|
public Endpoint Endpoint { get; set; }
|
||||||
private RequestDelegate _next;
|
|
||||||
|
|
||||||
public Endpoint Endpoint
|
public RequestDelegate RequestDelegate { get; set; }
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return _endpoint;
|
|
||||||
}
|
|
||||||
|
|
||||||
set
|
public DispatcherValueCollection Values { get; set; }
|
||||||
{
|
|
||||||
_endpoint = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public RequestDelegate RequestDelegate
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return _next;
|
|
||||||
}
|
|
||||||
|
|
||||||
set
|
|
||||||
{
|
|
||||||
_next = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,28 +4,36 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Dispatcher
|
namespace Microsoft.AspNetCore.Dispatcher
|
||||||
{
|
{
|
||||||
public class DispatcherMiddleware
|
public class DispatcherMiddleware
|
||||||
{
|
{
|
||||||
|
private readonly ILogger _logger;
|
||||||
private readonly DispatcherOptions _options;
|
private readonly DispatcherOptions _options;
|
||||||
private readonly RequestDelegate _next;
|
private readonly RequestDelegate _next;
|
||||||
|
|
||||||
public DispatcherMiddleware(IOptions<DispatcherOptions> options, RequestDelegate next)
|
public DispatcherMiddleware(IOptions<DispatcherOptions> options, ILogger<DispatcherMiddleware> logger, RequestDelegate next)
|
||||||
{
|
{
|
||||||
if (options == null)
|
if (options == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(options));
|
throw new ArgumentNullException(nameof(options));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (logger == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(logger));
|
||||||
|
}
|
||||||
|
|
||||||
if (next == null)
|
if (next == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(next));
|
throw new ArgumentNullException(nameof(next));
|
||||||
}
|
}
|
||||||
|
|
||||||
_options = options.Value;
|
_options = options.Value;
|
||||||
|
_logger = logger;
|
||||||
_next = next;
|
_next = next;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,6 +47,7 @@ namespace Microsoft.AspNetCore.Dispatcher
|
||||||
await entry(httpContext);
|
await entry(httpContext);
|
||||||
if (feature.Endpoint != null || feature.RequestDelegate != null)
|
if (feature.Endpoint != null || feature.RequestDelegate != null)
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation("Matched endpoint {Endpoint}", feature.Endpoint.DisplayName);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,28 +4,36 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Dispatcher
|
namespace Microsoft.AspNetCore.Dispatcher
|
||||||
{
|
{
|
||||||
public class EndpointMiddleware
|
public class EndpointMiddleware
|
||||||
{
|
{
|
||||||
|
private readonly ILogger _logger;
|
||||||
private readonly DispatcherOptions _options;
|
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)
|
if (options == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(options));
|
throw new ArgumentNullException(nameof(options));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (logger == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(logger));
|
||||||
|
}
|
||||||
|
|
||||||
if (next == null)
|
if (next == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(next));
|
throw new ArgumentNullException(nameof(next));
|
||||||
}
|
}
|
||||||
|
|
||||||
_options = options.Value;
|
_options = options.Value;
|
||||||
|
_logger = logger;
|
||||||
_next = next;
|
_next = next;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -47,7 +55,17 @@ namespace Microsoft.AspNetCore.Dispatcher
|
||||||
|
|
||||||
if (feature.RequestDelegate != null)
|
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);
|
await _next(context);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,29 +8,34 @@ using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Dispatcher
|
namespace Microsoft.AspNetCore.Dispatcher
|
||||||
{
|
{
|
||||||
public class SimpleEndpoint : Endpoint
|
public class SimpleEndpoint : Endpoint, IDispatcherValueSelectableEndpoint
|
||||||
{
|
{
|
||||||
public SimpleEndpoint(RequestDelegate requestDelegate)
|
public SimpleEndpoint(RequestDelegate requestDelegate)
|
||||||
: this(requestDelegate, Array.Empty<object>(), null)
|
: this(requestDelegate, Array.Empty<object>(), null, null)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public SimpleEndpoint(Func<RequestDelegate, RequestDelegate> delegateFactory)
|
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)
|
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)
|
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)
|
if (metadata == null)
|
||||||
{
|
{
|
||||||
|
|
@ -42,12 +47,13 @@ namespace Microsoft.AspNetCore.Dispatcher
|
||||||
throw new ArgumentNullException(nameof(requestDelegate));
|
throw new ArgumentNullException(nameof(requestDelegate));
|
||||||
}
|
}
|
||||||
|
|
||||||
DisplayName = displayName;
|
HandlerFactory = (next) => requestDelegate;
|
||||||
Metadata = metadata.ToArray();
|
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)
|
if (metadata == null)
|
||||||
{
|
{
|
||||||
|
|
@ -59,15 +65,18 @@ namespace Microsoft.AspNetCore.Dispatcher
|
||||||
throw new ArgumentNullException(nameof(delegateFactory));
|
throw new ArgumentNullException(nameof(delegateFactory));
|
||||||
}
|
}
|
||||||
|
|
||||||
DisplayName = displayName;
|
HandlerFactory = delegateFactory;
|
||||||
Metadata = metadata.ToArray();
|
Metadata = metadata.ToArray();
|
||||||
DelegateFactory = delegateFactory;
|
Values = new DispatcherValueCollection(values);
|
||||||
|
DisplayName = displayName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string DisplayName { get; }
|
public override string DisplayName { get; }
|
||||||
|
|
||||||
public override IReadOnlyList<object> Metadata { get; }
|
public override IReadOnlyList<object> Metadata { get; }
|
||||||
|
|
||||||
public Func<RequestDelegate, RequestDelegate> DelegateFactory { get; }
|
public Func<RequestDelegate, RequestDelegate> HandlerFactory { get; }
|
||||||
|
|
||||||
|
public DispatcherValueCollection Values { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
// 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;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Dispatcher;
|
using Microsoft.AspNetCore.Dispatcher;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
@ -13,6 +14,7 @@ namespace Microsoft.AspNetCore.Routing.Dispatcher
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class RouterDispatcher : DispatcherBase
|
public class RouterDispatcher : DispatcherBase
|
||||||
{
|
{
|
||||||
|
private readonly Endpoint _fallbackEndpoint;
|
||||||
private readonly IRouter _router;
|
private readonly IRouter _router;
|
||||||
|
|
||||||
public RouterDispatcher(IRouter router)
|
public RouterDispatcher(IRouter router)
|
||||||
|
|
@ -23,17 +25,47 @@ namespace Microsoft.AspNetCore.Routing.Dispatcher
|
||||||
}
|
}
|
||||||
|
|
||||||
_router = router;
|
_router = router;
|
||||||
|
_fallbackEndpoint = new UnknownEndpoint(_router);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async override Task InvokeAsync(HttpContext httpContext)
|
protected override async Task<bool> TryMatchAsync(HttpContext httpContext)
|
||||||
{
|
{
|
||||||
if (httpContext == null)
|
if (httpContext == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(httpContext));
|
throw new ArgumentNullException(nameof(httpContext));
|
||||||
}
|
}
|
||||||
|
|
||||||
var routeContext = new RouteContext(httpContext);
|
var routeContext = new RouteContext(httpContext);
|
||||||
await _router.RouteAsync(routeContext);
|
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>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue