Merge branch 'merge/release/2.2-to-master' of https://github.com/dotnet-maestro-bot/Routing into dotnet-maestro-bot-merge/release/2.2-to-master
This commit is contained in:
commit
825141eeeb
|
|
@ -18,6 +18,6 @@
|
|||
<PackageSigningCertName>MicrosoftNuGet</PackageSigningCertName>
|
||||
<PublicSign Condition="'$(OS)' != 'Windows_NT'">true</PublicSign>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<LangVersion>7.2</LangVersion>
|
||||
<LangVersion>7.3</LangVersion>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
|
|
|||
30
Routing.sln
30
Routing.sln
|
|
@ -51,7 +51,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarkapps", "benchmarka
|
|||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmarks", "benchmarkapps\Benchmarks\Benchmarks.csproj", "{91F47A60-9A78-4968-B10D-157D9BFAC37F}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Swaggatherer", "benchmarks\Swaggatherer\Swaggatherer.csproj", "{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}"
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{6824486A-3EFF-45D1-BEE8-8B137639C890}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Swaggatherer", "tools\Swaggatherer\Swaggatherer.csproj", "{B8516771-E850-4724-BEC3-63FC00C2AE57}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
|
|
@ -165,18 +167,18 @@ Global
|
|||
{91F47A60-9A78-4968-B10D-157D9BFAC37F}.Release|Mixed Platforms.Build.0 = Release|Any CPU
|
||||
{91F47A60-9A78-4968-B10D-157D9BFAC37F}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{91F47A60-9A78-4968-B10D-157D9BFAC37F}.Release|x86.Build.0 = Release|Any CPU
|
||||
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
|
||||
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
|
||||
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
|
||||
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}.Release|Mixed Platforms.Build.0 = Release|Any CPU
|
||||
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}.Release|x86.Build.0 = Release|Any CPU
|
||||
{B8516771-E850-4724-BEC3-63FC00C2AE57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B8516771-E850-4724-BEC3-63FC00C2AE57}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B8516771-E850-4724-BEC3-63FC00C2AE57}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
|
||||
{B8516771-E850-4724-BEC3-63FC00C2AE57}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
|
||||
{B8516771-E850-4724-BEC3-63FC00C2AE57}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{B8516771-E850-4724-BEC3-63FC00C2AE57}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{B8516771-E850-4724-BEC3-63FC00C2AE57}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B8516771-E850-4724-BEC3-63FC00C2AE57}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B8516771-E850-4724-BEC3-63FC00C2AE57}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
|
||||
{B8516771-E850-4724-BEC3-63FC00C2AE57}.Release|Mixed Platforms.Build.0 = Release|Any CPU
|
||||
{B8516771-E850-4724-BEC3-63FC00C2AE57}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{B8516771-E850-4724-BEC3-63FC00C2AE57}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
@ -191,7 +193,7 @@ Global
|
|||
{5C73140B-41F3-466F-A07B-3614E4D80DF9} = {95359B4B-4C85-4B44-A75B-0621905C4CF6}
|
||||
{F3D86714-4E64-41A6-9B36-A47B3683CF5D} = {D5F39F59-5725-4127-82E7-67028D006185}
|
||||
{91F47A60-9A78-4968-B10D-157D9BFAC37F} = {7F5914E2-C63F-4759-898E-462804357C90}
|
||||
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D} = {D5F39F59-5725-4127-82E7-67028D006185}
|
||||
{B8516771-E850-4724-BEC3-63FC00C2AE57} = {6824486A-3EFF-45D1-BEE8-8B137639C890}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {36C8D815-B7F1-479D-894B-E606FB8DECDA}
|
||||
|
|
|
|||
|
|
@ -29,11 +29,11 @@ namespace Benchmarks
|
|||
.UseKestrel();
|
||||
|
||||
var scenario = config["scenarios"]?.ToLower();
|
||||
if (scenario == "plaintextdispatcher" || scenario == "plaintextglobalrouting")
|
||||
if (scenario == "plaintextdispatcher" || scenario == "plaintextendpointrouting")
|
||||
{
|
||||
webHostBuilder.UseStartup<StartupUsingGlobalRouting>();
|
||||
webHostBuilder.UseStartup<StartupUsingEndpointRouting>();
|
||||
// for testing
|
||||
webHostBuilder.UseSetting("Startup", nameof(StartupUsingGlobalRouting));
|
||||
webHostBuilder.UseSetting("Startup", nameof(StartupUsingEndpointRouting));
|
||||
}
|
||||
else if (scenario == "plaintextrouting" || scenario == "plaintextrouter")
|
||||
{
|
||||
|
|
@ -44,7 +44,7 @@ namespace Benchmarks
|
|||
else
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Invalid scenario '{scenario}'. Allowed scenarios are PlaintextGlobalRouting and PlaintextRouter");
|
||||
$"Invalid scenario '{scenario}'. Allowed scenarios are PlaintextEndpointRouting and PlaintextRouter");
|
||||
}
|
||||
|
||||
return webHostBuilder;
|
||||
|
|
|
|||
|
|
@ -2,15 +2,16 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Internal;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.Routing.Matchers;
|
||||
using Microsoft.AspNetCore.Routing.Patterns;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
namespace Benchmarks
|
||||
{
|
||||
public class StartupUsingGlobalRouting
|
||||
public class StartupUsingEndpointRouting
|
||||
{
|
||||
private static readonly byte[] _helloWorldPayload = Encoding.UTF8.GetBytes("Hello, World!");
|
||||
|
||||
|
|
@ -18,12 +19,10 @@ namespace Benchmarks
|
|||
{
|
||||
services.AddRouting();
|
||||
|
||||
services.Configure<EndpointOptions>(options =>
|
||||
{
|
||||
options.DataSources.Add(new DefaultEndpointDataSource(new[]
|
||||
var endpointDataSource = new DefaultEndpointDataSource(new[]
|
||||
{
|
||||
new MatcherEndpoint(
|
||||
invoker: (next) => (httpContext) =>
|
||||
new RouteEndpoint(
|
||||
requestDelegate: (httpContext) =>
|
||||
{
|
||||
var response = httpContext.Response;
|
||||
var payloadLength = _helloWorldPayload.Length;
|
||||
|
|
@ -33,17 +32,17 @@ namespace Benchmarks
|
|||
return response.Body.WriteAsync(_helloWorldPayload, 0, payloadLength);
|
||||
},
|
||||
routePattern: RoutePatternFactory.Parse("/plaintext"),
|
||||
requiredValues: new RouteValueDictionary(),
|
||||
order: 0,
|
||||
metadata: EndpointMetadataCollection.Empty,
|
||||
displayName: "Plaintext"),
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<EndpointDataSource>(endpointDataSource));
|
||||
}
|
||||
|
||||
public void Configure(IApplicationBuilder app)
|
||||
public void Configure(Microsoft.AspNetCore.Builder.IApplicationBuilder app)
|
||||
{
|
||||
app.UseGlobalRouting();
|
||||
app.UseEndpointRouting();
|
||||
|
||||
app.UseEndpoint();
|
||||
}
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
"PlaintextDispatcher": {
|
||||
"Path": "/plaintext"
|
||||
},
|
||||
"PlaintextGlobalRouting": {
|
||||
"PlaintextEndpointRouting": {
|
||||
"Path": "/plaintext"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,128 @@
|
|||
// 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 BenchmarkDotNet.Attributes;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
public class EndpointMetadataCollectionBenchmark
|
||||
{
|
||||
private object[] _items;
|
||||
private EndpointMetadataCollection _collection;
|
||||
|
||||
[Params(3, 10, 25)]
|
||||
public int Count { get; set; }
|
||||
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
var seeds = new Type[]
|
||||
{
|
||||
typeof(Metadata1),
|
||||
typeof(Metadata2),
|
||||
typeof(Metadata3),
|
||||
typeof(Metadata4),
|
||||
typeof(Metadata5),
|
||||
typeof(Metadata6),
|
||||
typeof(Metadata7),
|
||||
typeof(Metadata8),
|
||||
typeof(Metadata9),
|
||||
};
|
||||
|
||||
_items = new object[Count];
|
||||
for (var i = 0; i < _items.Length; i++)
|
||||
{
|
||||
_items[i] = seeds[i % seeds.Length];
|
||||
}
|
||||
|
||||
_collection = new EndpointMetadataCollection(_items);
|
||||
}
|
||||
|
||||
// This is a synthetic baseline that visits each item and does an as-cast.
|
||||
[Benchmark(Baseline = true, OperationsPerInvoke = 5)]
|
||||
public void Baseline()
|
||||
{
|
||||
var items = _items;
|
||||
for (var i = items.Length - 1; i >= 0; i--)
|
||||
{
|
||||
GC.KeepAlive(_items[i] as IMetadata1);
|
||||
}
|
||||
|
||||
for (var i = items.Length - 1; i >= 0; i--)
|
||||
{
|
||||
GC.KeepAlive(_items[i] as IMetadata2);
|
||||
}
|
||||
|
||||
for (var i = items.Length - 1; i >= 0; i--)
|
||||
{
|
||||
GC.KeepAlive(_items[i] as IMetadata3);
|
||||
}
|
||||
|
||||
for (var i = items.Length - 1; i >= 0; i--)
|
||||
{
|
||||
GC.KeepAlive(_items[i] as IMetadata4);
|
||||
}
|
||||
|
||||
for (var i = items.Length - 1; i >= 0; i--)
|
||||
{
|
||||
GC.KeepAlive(_items[i] as IMetadata5);
|
||||
}
|
||||
}
|
||||
|
||||
[Benchmark(OperationsPerInvoke = 5)]
|
||||
public void GetMetadata()
|
||||
{
|
||||
GC.KeepAlive(_collection.GetMetadata<IMetadata1>());
|
||||
GC.KeepAlive(_collection.GetMetadata<IMetadata2>());
|
||||
GC.KeepAlive(_collection.GetMetadata<IMetadata3>());
|
||||
GC.KeepAlive(_collection.GetMetadata<IMetadata4>());
|
||||
GC.KeepAlive(_collection.GetMetadata<IMetadata5>());
|
||||
}
|
||||
|
||||
[Benchmark(OperationsPerInvoke = 5)]
|
||||
public void GetOrderedMetadata()
|
||||
{
|
||||
foreach (var item in _collection.GetOrderedMetadata<IMetadata1>())
|
||||
{
|
||||
GC.KeepAlive(item);
|
||||
}
|
||||
|
||||
foreach (var item in _collection.GetOrderedMetadata<IMetadata2>())
|
||||
{
|
||||
GC.KeepAlive(item);
|
||||
}
|
||||
|
||||
foreach (var item in _collection.GetOrderedMetadata<IMetadata3>())
|
||||
{
|
||||
GC.KeepAlive(item);
|
||||
}
|
||||
|
||||
foreach (var item in _collection.GetOrderedMetadata<IMetadata4>())
|
||||
{
|
||||
GC.KeepAlive(item);
|
||||
}
|
||||
|
||||
foreach (var item in _collection.GetOrderedMetadata<IMetadata5>())
|
||||
{
|
||||
GC.KeepAlive(item);
|
||||
}
|
||||
}
|
||||
|
||||
private interface IMetadata1 { }
|
||||
private interface IMetadata2 { }
|
||||
private interface IMetadata3 { }
|
||||
private interface IMetadata4 { }
|
||||
private interface IMetadata5 { }
|
||||
private class Metadata1 : IMetadata1 { }
|
||||
private class Metadata2 : IMetadata2 { }
|
||||
private class Metadata3 : IMetadata3 { }
|
||||
private class Metadata4 : IMetadata4 { }
|
||||
private class Metadata5 : IMetadata5 { }
|
||||
private class Metadata6 : IMetadata1, IMetadata2 { }
|
||||
private class Metadata7 : IMetadata2, IMetadata3 { }
|
||||
private class Metadata8 : IMetadata4, IMetadata5 { }
|
||||
private class Metadata9 : IMetadata1, IMetadata2 { }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
// 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.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Routing.Matching;
|
||||
using Microsoft.AspNetCore.Routing.Patterns;
|
||||
using Microsoft.AspNetCore.Routing.Template;
|
||||
using Microsoft.AspNetCore.Routing.Tree;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.ObjectPool;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
public abstract class EndpointRoutingBenchmarkBase
|
||||
{
|
||||
private protected RouteEndpoint[] Endpoints;
|
||||
private protected HttpContext[] Requests;
|
||||
|
||||
private protected void SetupEndpoints(params RouteEndpoint[] endpoints)
|
||||
{
|
||||
Endpoints = endpoints;
|
||||
}
|
||||
|
||||
// The older routing implementations retrieve services when they first execute.
|
||||
private protected IServiceProvider CreateServices()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
|
||||
services.AddLogging();
|
||||
services.AddOptions();
|
||||
services.AddRouting();
|
||||
|
||||
services.TryAddEnumerable(
|
||||
ServiceDescriptor.Singleton<EndpointDataSource>(new DefaultEndpointDataSource(Endpoints)));
|
||||
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
private protected DfaMatcherBuilder CreateDfaMatcherBuilder()
|
||||
{
|
||||
return CreateServices().GetRequiredService<DfaMatcherBuilder>();
|
||||
}
|
||||
|
||||
private protected static int[] SampleRequests(int endpointCount, int count)
|
||||
{
|
||||
// This isn't very high tech, but it's at least regular distribution.
|
||||
// We sort the route templates by precedence, so this should result in
|
||||
// an even distribution of the 'complexity' of the routes that are exercised.
|
||||
var frequency = endpointCount / count;
|
||||
if (frequency < 2)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The sample count is too high. This won't produce an accurate sampling" +
|
||||
"of the request data.");
|
||||
}
|
||||
|
||||
var samples = new int[count];
|
||||
for (var i = 0; i < samples.Length; i++)
|
||||
{
|
||||
samples[i] = i * frequency;
|
||||
}
|
||||
|
||||
return samples;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private protected void Validate(HttpContext httpContext, Endpoint expected, Endpoint actual)
|
||||
{
|
||||
if (!object.ReferenceEquals(expected, actual))
|
||||
{
|
||||
var message = new StringBuilder();
|
||||
message.AppendLine($"Validation failed for request {Array.IndexOf(Requests, httpContext)}");
|
||||
message.AppendLine($"{httpContext.Request.Method} {httpContext.Request.Path}");
|
||||
message.AppendLine($"expected: '{((RouteEndpoint)expected)?.DisplayName ?? "null"}'");
|
||||
message.AppendLine($"actual: '{((RouteEndpoint)actual)?.DisplayName ?? "null"}'");
|
||||
throw new InvalidOperationException(message.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
protected void AssertUrl(string expectedUrl, string actualUrl)
|
||||
{
|
||||
AssertUrl(expectedUrl, actualUrl, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
protected void AssertUrl(string expectedUrl, string actualUrl, StringComparison stringComparison)
|
||||
{
|
||||
if (!string.Equals(expectedUrl, actualUrl, stringComparison))
|
||||
{
|
||||
throw new InvalidOperationException($"Expected: {expectedUrl}, Actual: {actualUrl}");
|
||||
}
|
||||
}
|
||||
|
||||
protected RouteEndpoint CreateEndpoint(string template, string httpMethod)
|
||||
{
|
||||
return CreateEndpoint(template, metadata: new object[]
|
||||
{
|
||||
new HttpMethodMetadata(new string[]{ httpMethod, }),
|
||||
});
|
||||
}
|
||||
|
||||
protected RouteEndpoint CreateEndpoint(
|
||||
string template,
|
||||
object defaults = null,
|
||||
object constraints = null,
|
||||
object requiredValues = null,
|
||||
int order = 0,
|
||||
string displayName = null,
|
||||
string routeName = null,
|
||||
params object[] metadata)
|
||||
{
|
||||
var endpointMetadata = new List<object>(metadata ?? Array.Empty<object>());
|
||||
endpointMetadata.Add(new RouteValuesAddressMetadata(routeName, new RouteValueDictionary(requiredValues)));
|
||||
|
||||
return new RouteEndpoint(
|
||||
(context) => Task.CompletedTask,
|
||||
RoutePatternFactory.Parse(template, defaults, constraints),
|
||||
order,
|
||||
new EndpointMetadataCollection(endpointMetadata),
|
||||
displayName);
|
||||
}
|
||||
|
||||
protected (HttpContext httpContext, RouteValueDictionary ambientValues) CreateCurrentRequestContext(
|
||||
object ambientValues = null)
|
||||
{
|
||||
var feature = new EndpointFeature { RouteValues = new RouteValueDictionary(ambientValues) };
|
||||
var context = new DefaultHttpContext();
|
||||
context.Features.Set<IEndpointFeature>(feature);
|
||||
context.Features.Set<IRouteValuesFeature>(feature);
|
||||
|
||||
return (context, feature.RouteValues);
|
||||
}
|
||||
|
||||
protected void CreateOutboundRouteEntry(TreeRouteBuilder treeRouteBuilder, RouteEndpoint endpoint)
|
||||
{
|
||||
var routeValuesAddressMetadata = endpoint.Metadata.GetMetadata<IRouteValuesAddressMetadata>();
|
||||
var requiredValues = routeValuesAddressMetadata?.RequiredValues ?? new RouteValueDictionary();
|
||||
|
||||
treeRouteBuilder.MapOutbound(
|
||||
NullRouter.Instance,
|
||||
new RouteTemplate(RoutePatternFactory.Parse(
|
||||
endpoint.RoutePattern.RawText,
|
||||
defaults: endpoint.RoutePattern.Defaults,
|
||||
parameterPolicies: null)),
|
||||
requiredLinkValues: new RouteValueDictionary(requiredValues),
|
||||
routeName: null,
|
||||
order: 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
// 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 BenchmarkDotNet.Attributes;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing.Tree;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.LinkGeneration
|
||||
{
|
||||
public partial class LinkGenerationGithubBenchmark
|
||||
{
|
||||
private LinkGenerator _linkGenerator;
|
||||
private TreeRouter _treeRouter;
|
||||
private (HttpContext HttpContext, RouteValueDictionary AmbientValues) _requestContext;
|
||||
private RouteValueDictionary _lookUpValues;
|
||||
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
SetupEndpoints();
|
||||
|
||||
var services = CreateServices();
|
||||
_linkGenerator = services.GetRequiredService<LinkGenerator>();
|
||||
|
||||
// Attribute routing related
|
||||
var treeRouteBuilder = services.GetRequiredService<TreeRouteBuilder>();
|
||||
foreach (var endpoint in Endpoints)
|
||||
{
|
||||
CreateOutboundRouteEntry(treeRouteBuilder, endpoint);
|
||||
}
|
||||
_treeRouter = treeRouteBuilder.Build();
|
||||
|
||||
_requestContext = CreateCurrentRequestContext();
|
||||
|
||||
// Get the endpoint to test and pre-populate the lookup values with the defaults
|
||||
// (as they are dynamically generated) and update with other required parameter values.
|
||||
// /repos/{owner}/{repo}/issues/comments/{commentId}
|
||||
var endpointToTest = Endpoints[176];
|
||||
_lookUpValues = new RouteValueDictionary(endpointToTest.RoutePattern.Defaults);
|
||||
_lookUpValues["owner"] = "aspnet";
|
||||
_lookUpValues["repo"] = "routing";
|
||||
_lookUpValues["commentId"] = "20202";
|
||||
}
|
||||
|
||||
[Benchmark(Baseline = true)]
|
||||
public void TreeRouter()
|
||||
{
|
||||
var virtualPathData = _treeRouter.GetVirtualPath(new VirtualPathContext(
|
||||
_requestContext.HttpContext,
|
||||
ambientValues: _requestContext.AmbientValues,
|
||||
values: new RouteValueDictionary(_lookUpValues)));
|
||||
|
||||
AssertUrl("/repos/aspnet/routing/issues/comments/20202", virtualPathData?.VirtualPath);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void EndpointRouting()
|
||||
{
|
||||
var actualUrl = _linkGenerator.GetLink(
|
||||
_requestContext.HttpContext,
|
||||
values: new RouteValueDictionary(_lookUpValues));
|
||||
|
||||
AssertUrl("/repos/aspnet/routing/issues/comments/20202", actualUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,74 @@
|
|||
// 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 BenchmarkDotNet.Attributes;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.LinkGeneration
|
||||
{
|
||||
public class SingleRouteRouteValuesBasedEndpointFinderBenchmark : EndpointRoutingBenchmarkBase
|
||||
{
|
||||
private IEndpointFinder<RouteValuesAddress> _finder;
|
||||
private TestEndpointFinder _baseFinder;
|
||||
private (HttpContext HttpContext, RouteValueDictionary AmbientValues) _requestContext;
|
||||
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
var template = "Products/Details";
|
||||
var defaults = new { controller = "Products", action = "Details" };
|
||||
var requiredValues = new { controller = "Products", action = "Details" };
|
||||
|
||||
SetupEndpoints(CreateEndpoint(template, defaults, requiredValues: requiredValues, routeName: "ProductDetails"));
|
||||
var services = CreateServices();
|
||||
_finder = services.GetRequiredService<IEndpointFinder<RouteValuesAddress>>();
|
||||
_baseFinder = new TestEndpointFinder(Endpoints[0]);
|
||||
|
||||
_requestContext = CreateCurrentRequestContext();
|
||||
}
|
||||
|
||||
[Benchmark(Baseline = true)]
|
||||
public void Baseline()
|
||||
{
|
||||
var actual = _baseFinder.FindEndpoints(address: 0);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void RouteValues()
|
||||
{
|
||||
var actual = _finder.FindEndpoints(new RouteValuesAddress
|
||||
{
|
||||
AmbientValues = _requestContext.AmbientValues,
|
||||
ExplicitValues = new RouteValueDictionary(new { controller = "Products", action = "Details" }),
|
||||
RouteName = null
|
||||
});
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void RouteName()
|
||||
{
|
||||
var actual = _finder.FindEndpoints(new RouteValuesAddress
|
||||
{
|
||||
AmbientValues = _requestContext.AmbientValues,
|
||||
RouteName = "ProductDetails"
|
||||
});
|
||||
}
|
||||
|
||||
private class TestEndpointFinder : IEndpointFinder<int>
|
||||
{
|
||||
private readonly Endpoint _endpoint;
|
||||
|
||||
public TestEndpointFinder(Endpoint endpoint)
|
||||
{
|
||||
_endpoint = endpoint;
|
||||
}
|
||||
|
||||
public IEnumerable<Endpoint> FindEndpoints(int address)
|
||||
{
|
||||
return new[] { _endpoint };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
// 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 BenchmarkDotNet.Attributes;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing.Tree;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.LinkGeneration
|
||||
{
|
||||
public class SingleRouteWithConstraintsBenchmark : EndpointRoutingBenchmarkBase
|
||||
{
|
||||
private TreeRouter _treeRouter;
|
||||
private LinkGenerator _linkGenerator;
|
||||
private (HttpContext HttpContext, RouteValueDictionary AmbientValues) _requestContext;
|
||||
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
var template = "Customers/Details/{category}/{region}/{id:int}";
|
||||
var defaults = new { controller = "Customers", action = "Details" };
|
||||
var requiredValues = new { controller = "Customers", action = "Details" };
|
||||
|
||||
// Endpoint routing related
|
||||
SetupEndpoints(CreateEndpoint(template, defaults, requiredValues: requiredValues));
|
||||
var services = CreateServices();
|
||||
_linkGenerator = services.GetRequiredService<LinkGenerator>();
|
||||
|
||||
// Attribute routing related
|
||||
var treeRouteBuilder = services.GetRequiredService<TreeRouteBuilder>();
|
||||
CreateOutboundRouteEntry(treeRouteBuilder, Endpoints[0]);
|
||||
_treeRouter = treeRouteBuilder.Build();
|
||||
|
||||
_requestContext = CreateCurrentRequestContext();
|
||||
}
|
||||
|
||||
[Benchmark(Baseline = true)]
|
||||
public void TreeRouter()
|
||||
{
|
||||
var virtualPathData = _treeRouter.GetVirtualPath(new VirtualPathContext(
|
||||
_requestContext.HttpContext,
|
||||
ambientValues: _requestContext.AmbientValues,
|
||||
values: new RouteValueDictionary(
|
||||
new
|
||||
{
|
||||
controller = "Customers",
|
||||
action = "Details",
|
||||
category = "Administration",
|
||||
region = "US",
|
||||
id = 10
|
||||
})));
|
||||
|
||||
AssertUrl("/Customers/Details/Administration/US/10", virtualPathData?.VirtualPath);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void EndpointRouting()
|
||||
{
|
||||
var actualUrl = _linkGenerator.GetLink(
|
||||
_requestContext.HttpContext,
|
||||
values: new RouteValueDictionary(
|
||||
new
|
||||
{
|
||||
controller = "Customers",
|
||||
action = "Details",
|
||||
category = "Administration",
|
||||
region = "US",
|
||||
id = 10
|
||||
}));
|
||||
|
||||
AssertUrl("/Customers/Details/Administration/US/10", actualUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
// 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 BenchmarkDotNet.Attributes;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing.Tree;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.LinkGeneration
|
||||
{
|
||||
public class SingleRouteWithNoParametersBenchmark : EndpointRoutingBenchmarkBase
|
||||
{
|
||||
private TreeRouter _treeRouter;
|
||||
private LinkGenerator _linkGenerator;
|
||||
private (HttpContext HttpContext, RouteValueDictionary AmbientValues) _requestContext;
|
||||
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
var template = "Products/Details";
|
||||
var defaults = new { controller = "Products", action = "Details" };
|
||||
var requiredValues = new { controller = "Products", action = "Details" };
|
||||
|
||||
// Endpoint routing related
|
||||
SetupEndpoints(CreateEndpoint(template, defaults, requiredValues: requiredValues));
|
||||
var services = CreateServices();
|
||||
_linkGenerator = services.GetRequiredService<LinkGenerator>();
|
||||
|
||||
// Attribute routing related
|
||||
var treeRouteBuilder = services.GetRequiredService<TreeRouteBuilder>();
|
||||
CreateOutboundRouteEntry(treeRouteBuilder, Endpoints[0]);
|
||||
_treeRouter = treeRouteBuilder.Build();
|
||||
|
||||
_requestContext = CreateCurrentRequestContext();
|
||||
}
|
||||
|
||||
[Benchmark(Baseline = true)]
|
||||
public void TreeRouter()
|
||||
{
|
||||
var virtualPathData = _treeRouter.GetVirtualPath(new VirtualPathContext(
|
||||
_requestContext.HttpContext,
|
||||
ambientValues: _requestContext.AmbientValues,
|
||||
values: new RouteValueDictionary(
|
||||
new
|
||||
{
|
||||
controller = "Products",
|
||||
action = "Details",
|
||||
})));
|
||||
|
||||
AssertUrl("/Products/Details", virtualPathData?.VirtualPath);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void EndpointRouting()
|
||||
{
|
||||
var actualUrl = _linkGenerator.GetLink(
|
||||
_requestContext.HttpContext,
|
||||
values: new RouteValueDictionary(
|
||||
new
|
||||
{
|
||||
controller = "Products",
|
||||
action = "Details",
|
||||
}));
|
||||
|
||||
AssertUrl("/Products/Details", actualUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
// 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 BenchmarkDotNet.Attributes;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing.Tree;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.LinkGeneration
|
||||
{
|
||||
public class SingleRouteWithParametersBenchmark : EndpointRoutingBenchmarkBase
|
||||
{
|
||||
private TreeRouter _treeRouter;
|
||||
private LinkGenerator _linkGenerator;
|
||||
private (HttpContext HttpContext, RouteValueDictionary AmbientValues) _requestContext;
|
||||
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
var template = "Customers/Details/{category}/{region}/{id}";
|
||||
var defaults = new { controller = "Customers", action = "Details" };
|
||||
var requiredValues = new { controller = "Customers", action = "Details" };
|
||||
|
||||
// Endpoint routing related
|
||||
SetupEndpoints(CreateEndpoint(template, defaults, requiredValues: requiredValues));
|
||||
var services = CreateServices();
|
||||
_linkGenerator = services.GetRequiredService<LinkGenerator>();
|
||||
|
||||
// Attribute routing related
|
||||
var treeRouteBuilder = services.GetRequiredService<TreeRouteBuilder>();
|
||||
CreateOutboundRouteEntry(treeRouteBuilder, Endpoints[0]);
|
||||
_treeRouter = treeRouteBuilder.Build();
|
||||
|
||||
_requestContext = CreateCurrentRequestContext();
|
||||
}
|
||||
|
||||
[Benchmark(Baseline = true)]
|
||||
public void TreeRouter()
|
||||
{
|
||||
var virtualPathData = _treeRouter.GetVirtualPath(new VirtualPathContext(
|
||||
_requestContext.HttpContext,
|
||||
ambientValues: _requestContext.AmbientValues,
|
||||
values: new RouteValueDictionary(
|
||||
new
|
||||
{
|
||||
controller = "Customers",
|
||||
action = "Details",
|
||||
category = "Administration",
|
||||
region = "US",
|
||||
id = 10
|
||||
})));
|
||||
|
||||
AssertUrl("/Customers/Details/Administration/US/10", virtualPathData?.VirtualPath);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void EndpointRouting()
|
||||
{
|
||||
var actualUrl = _linkGenerator.GetLink(
|
||||
_requestContext.HttpContext,
|
||||
values: new RouteValueDictionary(
|
||||
new
|
||||
{
|
||||
controller = "Customers",
|
||||
action = "Details",
|
||||
category = "Administration",
|
||||
region = "US",
|
||||
id = 10
|
||||
}));
|
||||
|
||||
AssertUrl("/Customers/Details/Administration/US/10", actualUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,172 +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;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
{
|
||||
// An optimized jump table that trades a small amount of additional memory for
|
||||
// hash-table like performance.
|
||||
//
|
||||
// The optimization here is to use the first character of the known entries
|
||||
// as a 'key' in the hash table in the space of A-Z. This gives us a maximum
|
||||
// of 26 buckets (hence the reduced memory)
|
||||
internal class AsciiKeyedJumpTable : JumpTable
|
||||
{
|
||||
public static bool TryCreate(
|
||||
int defaultDestination,
|
||||
int exitDestination,
|
||||
List<(string text, int destination)> entries,
|
||||
out JumpTable result)
|
||||
{
|
||||
result = null;
|
||||
|
||||
// First we group string by their uppercase letter. If we see a string
|
||||
// that starts with a non-ASCII letter
|
||||
var map = new Dictionary<char, List<(string text, int destination)>>();
|
||||
|
||||
for (var i = 0; i < entries.Count; i++)
|
||||
{
|
||||
if (entries[i].text.Length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!IsAscii(entries[i].text))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var first = ToUpperAscii(entries[i].text[0]);
|
||||
if (first < 'A' || first > 'Z')
|
||||
{
|
||||
// Not a letter
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!map.TryGetValue(first, out var matches))
|
||||
{
|
||||
matches = new List<(string text, int destination)>();
|
||||
map.Add(first, matches);
|
||||
}
|
||||
|
||||
matches.Add(entries[i]);
|
||||
}
|
||||
|
||||
var next = 0;
|
||||
var ordered = new(string text, int destination)[entries.Count];
|
||||
var indexes = new int[26 * 2];
|
||||
for (var i = 0; i < 26; i++)
|
||||
{
|
||||
indexes[i * 2] = next;
|
||||
|
||||
var length = 0;
|
||||
if (map.TryGetValue((char)('A' + i), out var matches))
|
||||
{
|
||||
length += matches.Count;
|
||||
for (var j = 0; j < matches.Count; j++)
|
||||
{
|
||||
ordered[next++] = matches[j];
|
||||
}
|
||||
}
|
||||
|
||||
indexes[i * 2 + 1] = length;
|
||||
}
|
||||
|
||||
result = new AsciiKeyedJumpTable(defaultDestination, exitDestination, ordered, indexes);
|
||||
return true;
|
||||
}
|
||||
|
||||
private readonly int _defaultDestination;
|
||||
private readonly int _exitDestination;
|
||||
private readonly (string text, int destination)[] _entries;
|
||||
private readonly int[] _indexes;
|
||||
|
||||
private AsciiKeyedJumpTable(
|
||||
int defaultDestination,
|
||||
int exitDestination,
|
||||
(string text, int destination)[] entries,
|
||||
int[] indexes)
|
||||
{
|
||||
_defaultDestination = defaultDestination;
|
||||
_exitDestination = exitDestination;
|
||||
_entries = entries;
|
||||
_indexes = indexes;
|
||||
}
|
||||
|
||||
public override int GetDestination(string path, PathSegment segment)
|
||||
{
|
||||
if (segment.Length == 0)
|
||||
{
|
||||
return _exitDestination;
|
||||
}
|
||||
|
||||
var c = path[segment.Start];
|
||||
if (!IsAscii(c))
|
||||
{
|
||||
return _defaultDestination;
|
||||
}
|
||||
|
||||
c = ToUpperAscii(c);
|
||||
if (c < 'A' || c > 'Z')
|
||||
{
|
||||
// Character is non-ASCII or not a letter. Since we know that all of the entries are ASCII
|
||||
// and begin with a letter this is not a match.
|
||||
return _defaultDestination;
|
||||
}
|
||||
|
||||
var offset = (c - 'A') * 2;
|
||||
var start = _indexes[offset];
|
||||
var length = _indexes[offset + 1];
|
||||
|
||||
var entries = _entries;
|
||||
for (var i = start; i < start + length; i++)
|
||||
{
|
||||
var text = entries[i].text;
|
||||
if (segment.Length == text.Length &&
|
||||
string.Compare(
|
||||
path,
|
||||
segment.Start,
|
||||
text,
|
||||
0,
|
||||
segment.Length,
|
||||
StringComparison.OrdinalIgnoreCase) == 0)
|
||||
{
|
||||
return entries[i].destination;
|
||||
}
|
||||
}
|
||||
|
||||
return _defaultDestination;
|
||||
}
|
||||
|
||||
internal static bool IsAscii(char c)
|
||||
{
|
||||
// ~0x7F is a bit mask that checks for bits that won't be set in an ASCII character.
|
||||
// ASCII only uses the lowest 7 bits.
|
||||
return (c & ~0x7F) == 0;
|
||||
}
|
||||
|
||||
internal static bool IsAscii(string text)
|
||||
{
|
||||
for (var i = 0; i < text.Length; i++)
|
||||
{
|
||||
var c = text[i];
|
||||
if (!IsAscii(c))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static char ToUpperAscii(char c)
|
||||
{
|
||||
// 0x5F can be used to convert a character to uppercase ascii (assuming it's a letter).
|
||||
// This works because lowercase ASCII chars are exactly 32 less than their uppercase
|
||||
// counterparts.
|
||||
return (char)(c & 0x5F);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,197 +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.Diagnostics;
|
||||
using System.Linq;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
{
|
||||
internal class CustomHashTableJumpTable : JumpTable
|
||||
{
|
||||
// Similar to HashHelpers list of primes, but truncated. We don't expect
|
||||
// incredibly large numbers to be useful here.
|
||||
private static readonly int[] Primes = new int[]
|
||||
{
|
||||
3, 7, 11, 17, 23, 29, 37, 47, 59,
|
||||
71, 89, 107, 131, 163, 197, 239, 293,
|
||||
353, 431, 521, 631, 761, 919, 1103, 1327,
|
||||
1597, 1931, 2333, 2801, 3371, 4049, 4861, 5839,
|
||||
7013, 8419, 10103,
|
||||
};
|
||||
|
||||
private readonly int _defaultDestination;
|
||||
private readonly int _exitDestination;
|
||||
|
||||
private readonly int _prime;
|
||||
private readonly int[] _buckets;
|
||||
private readonly Entry[] _entries;
|
||||
|
||||
public CustomHashTableJumpTable(
|
||||
int defaultDestination,
|
||||
int exitDestination,
|
||||
(string text, int destination)[] entries)
|
||||
{
|
||||
_defaultDestination = defaultDestination;
|
||||
_exitDestination = exitDestination;
|
||||
|
||||
var map = new Dictionary<int, List<(string text, int destination)>>();
|
||||
|
||||
for (var i = 0; i < entries.Length; i++)
|
||||
{
|
||||
var key = GetKey(entries[i].text, new PathSegment(0, entries[i].text.Length));
|
||||
if (!map.TryGetValue(key, out var matches))
|
||||
{
|
||||
matches = new List<(string text, int destination)>();
|
||||
map.Add(key, matches);
|
||||
}
|
||||
|
||||
matches.Add(entries[i]);
|
||||
}
|
||||
|
||||
_prime = GetPrime(map.Count);
|
||||
_buckets = new int[_prime + 1];
|
||||
_entries = new Entry[map.Sum(kvp => kvp.Value.Count)];
|
||||
|
||||
var next = 0;
|
||||
foreach (var group in map.GroupBy(kvp => kvp.Key % _prime).OrderBy(g => g.Key))
|
||||
{
|
||||
_buckets[group.Key] = next;
|
||||
|
||||
foreach (var array in group)
|
||||
{
|
||||
for (var i = 0; i < array.Value.Count; i++)
|
||||
{
|
||||
_entries[next++] = new Entry(array.Value[i].text, array.Value[i].destination);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Assert(next == _entries.Length);
|
||||
_buckets[_prime] = next;
|
||||
|
||||
var last = 0;
|
||||
for (var i = 0; i < _buckets.Length; i++)
|
||||
{
|
||||
if (_buckets[i] == 0)
|
||||
{
|
||||
_buckets[i] = last;
|
||||
}
|
||||
else
|
||||
{
|
||||
last = _buckets[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int Find(int key)
|
||||
{
|
||||
return key % _prime;
|
||||
}
|
||||
|
||||
private static int GetPrime(int capacity)
|
||||
{
|
||||
for (int i = 0; i < Primes.Length; i++)
|
||||
{
|
||||
int prime = Primes[i];
|
||||
if (prime >= capacity)
|
||||
{
|
||||
return prime;
|
||||
}
|
||||
}
|
||||
|
||||
return Primes[Primes.Length - 1];
|
||||
}
|
||||
|
||||
public override int GetDestination(string path, PathSegment segment)
|
||||
{
|
||||
if (segment.Length == 0)
|
||||
{
|
||||
return _exitDestination;
|
||||
}
|
||||
|
||||
var key = GetKey(path, segment);
|
||||
var index = Find(key);
|
||||
|
||||
var start = _buckets[index];
|
||||
var end = _buckets[index + 1];
|
||||
|
||||
var entries = _entries.AsSpan(start, end - start);
|
||||
for (var i = 0; i < entries.Length; i++)
|
||||
{
|
||||
var text = entries[i].Text;
|
||||
if (text.Length == segment.Length &&
|
||||
string.Compare(
|
||||
path,
|
||||
segment.Start,
|
||||
text,
|
||||
0,
|
||||
segment.Length,
|
||||
StringComparison.OrdinalIgnoreCase) == 0)
|
||||
{
|
||||
return entries[i].Destination;
|
||||
}
|
||||
}
|
||||
|
||||
return _defaultDestination;
|
||||
}
|
||||
|
||||
// Builds a hashcode from segment (first four characters converted to 8bit ASCII)
|
||||
private static unsafe int GetKey(string path, PathSegment segment)
|
||||
{
|
||||
fixed (char* p = path)
|
||||
{
|
||||
switch (path.Length)
|
||||
{
|
||||
case 0:
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
case 1:
|
||||
{
|
||||
return
|
||||
((*(p + segment.Start + 0) & 0x5F) << (0 * 8));
|
||||
}
|
||||
|
||||
case 2:
|
||||
{
|
||||
return
|
||||
((*(p + segment.Start + 0) & 0x5F) << (0 * 8)) |
|
||||
((*(p + segment.Start + 1) & 0x5F) << (1 * 8));
|
||||
}
|
||||
|
||||
case 3:
|
||||
{
|
||||
return
|
||||
((*(p + segment.Start + 0) & 0x5F) << (0 * 8)) |
|
||||
((*(p + segment.Start + 1) & 0x5F) << (1 * 8)) |
|
||||
((*(p + segment.Start + 2) & 0x5F) << (2 * 8));
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
return
|
||||
((*(p + segment.Start + 0) & 0x5F) << (0 * 8)) |
|
||||
((*(p + segment.Start + 1) & 0x5F) << (1 * 8)) |
|
||||
((*(p + segment.Start + 2) & 0x5F) << (2 * 8)) |
|
||||
((*(p + segment.Start + 3) & 0x5F) << (3 * 8));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readonly struct Entry
|
||||
{
|
||||
public readonly string Text;
|
||||
public readonly int Destination;
|
||||
|
||||
public Entry(string text, int destination)
|
||||
{
|
||||
Text = text;
|
||||
Destination = destination;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,112 +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;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
{
|
||||
internal class DictionaryLookupJumpTable : JumpTable
|
||||
{
|
||||
private readonly int _defaultDestination;
|
||||
private readonly int _exitDestination;
|
||||
private readonly Dictionary<int, (string text, int destination)[]> _store;
|
||||
|
||||
public DictionaryLookupJumpTable(
|
||||
int defaultDestination,
|
||||
int exitDestination,
|
||||
(string text, int destination)[] entries)
|
||||
{
|
||||
_defaultDestination = defaultDestination;
|
||||
_exitDestination = exitDestination;
|
||||
|
||||
var map = new Dictionary<int, List<(string text, int destination)>>();
|
||||
|
||||
for (var i = 0; i < entries.Length; i++)
|
||||
{
|
||||
var key = GetKey(entries[i].text.AsSpan());
|
||||
if (!map.TryGetValue(key, out var matches))
|
||||
{
|
||||
matches = new List<(string text, int destination)>();
|
||||
map.Add(key, matches);
|
||||
}
|
||||
|
||||
matches.Add(entries[i]);
|
||||
}
|
||||
|
||||
_store = map.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ToArray());
|
||||
}
|
||||
|
||||
public override int GetDestination(string path, PathSegment segment)
|
||||
{
|
||||
if (segment.Length == 0)
|
||||
{
|
||||
return _exitDestination;
|
||||
}
|
||||
|
||||
var key = GetKey(path.AsSpan(segment.Start, segment.Length));
|
||||
if (_store.TryGetValue(key, out var entries))
|
||||
{
|
||||
for (var i = 0; i < entries.Length; i++)
|
||||
{
|
||||
var text = entries[i].text;
|
||||
if (text.Length == segment.Length &&
|
||||
string.Compare(
|
||||
path,
|
||||
segment.Start,
|
||||
text,
|
||||
0,
|
||||
segment.Length,
|
||||
StringComparison.OrdinalIgnoreCase) == 0)
|
||||
{
|
||||
return entries[i].destination;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _defaultDestination;
|
||||
}
|
||||
|
||||
private static int GetKey(string path, PathSegment segment)
|
||||
{
|
||||
return GetKey(path.AsSpan(segment.Start, segment.Length));
|
||||
}
|
||||
|
||||
/// builds a key from the last byte of length + first 3 characters of text (converted to ascii)
|
||||
private static int GetKey(ReadOnlySpan<char> span)
|
||||
{
|
||||
var length = (byte)(span.Length & 0xFF);
|
||||
|
||||
byte c0, c1, c2;
|
||||
switch (length)
|
||||
{
|
||||
case 0:
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
case 1:
|
||||
{
|
||||
c0 = (byte)(span[0] & 0x5F);
|
||||
return (length << 24) | (c0 << 16);
|
||||
}
|
||||
|
||||
case 2:
|
||||
{
|
||||
c0 = (byte)(span[0] & 0x5F);
|
||||
c1 = (byte)(span[1] & 0x5F);
|
||||
return (length << 24) | (c0 << 16) | (c1 << 8);
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
c0 = (byte)(span[0] & 0x5F);
|
||||
c1 = (byte)(span[1] & 0x5F);
|
||||
c2 = (byte)(span[2] & 0x5F);
|
||||
return (length << 24) | (c0 << 16) | (c1 << 8) | c2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,78 +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 BenchmarkDotNet.Attributes;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
{
|
||||
public class JumpTableSingleEntryBenchmark
|
||||
{
|
||||
private JumpTable _table;
|
||||
private string[] _strings;
|
||||
private PathSegment[] _segments;
|
||||
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
_table = new SingleEntryJumpTable(0, -1, "hello-world", 1);
|
||||
_strings = new string[]
|
||||
{
|
||||
"index/foo/2",
|
||||
"index/hello-world1/2",
|
||||
"index/hello-world/2",
|
||||
"index//2",
|
||||
"index/hillo-goodbye/2",
|
||||
};
|
||||
_segments = new PathSegment[]
|
||||
{
|
||||
new PathSegment(6, 3),
|
||||
new PathSegment(6, 12),
|
||||
new PathSegment(6, 11),
|
||||
new PathSegment(6, 0),
|
||||
new PathSegment(6, 13),
|
||||
};
|
||||
}
|
||||
|
||||
[Benchmark(Baseline = true, OperationsPerInvoke = 5)]
|
||||
public int Baseline()
|
||||
{
|
||||
var strings = _strings;
|
||||
var segments = _segments;
|
||||
|
||||
var destination = 0;
|
||||
for (var i = 0; i < strings.Length; i++)
|
||||
{
|
||||
var @string = strings[i];
|
||||
var segment = segments[i];
|
||||
|
||||
destination = segment.Length == 0 ? -1 :
|
||||
segment.Length != 11 ? 1 :
|
||||
string.Compare(
|
||||
@string,
|
||||
segment.Start,
|
||||
"hello-world",
|
||||
0,
|
||||
segment.Length,
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
return destination;
|
||||
}
|
||||
|
||||
[Benchmark(OperationsPerInvoke = 5)]
|
||||
public int Implementation()
|
||||
{
|
||||
var strings = _strings;
|
||||
var segments = _segments;
|
||||
|
||||
var destination = 0;
|
||||
for (var i = 0; i < strings.Length; i++)
|
||||
{
|
||||
destination = _table.GetDestination(strings[i], segments[i]);
|
||||
}
|
||||
|
||||
return destination;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,90 +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.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing.EndpointConstraints;
|
||||
using Microsoft.AspNetCore.Routing.Metadata;
|
||||
using Microsoft.AspNetCore.Routing.Patterns;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
{
|
||||
public abstract class MatcherBenchmarkBase
|
||||
{
|
||||
private protected MatcherEndpoint[] Endpoints;
|
||||
private protected HttpContext[] Requests;
|
||||
|
||||
// The older routing implementations retrieve services when they first execute.
|
||||
private protected static IServiceProvider CreateServices()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging();
|
||||
services.AddOptions();
|
||||
services.AddRouting();
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
private protected DfaMatcherBuilder CreateDfaMatcherBuilder()
|
||||
{
|
||||
return CreateServices().GetRequiredService<DfaMatcherBuilder>();
|
||||
}
|
||||
|
||||
private protected static MatcherEndpoint CreateEndpoint(string template, string httpMethod = null)
|
||||
{
|
||||
var metadata = new List<object>();
|
||||
if (httpMethod != null)
|
||||
{
|
||||
metadata.Add(new HttpMethodMetadata(new string[] { httpMethod, }));
|
||||
}
|
||||
|
||||
return new MatcherEndpoint(
|
||||
MatcherEndpoint.EmptyInvoker,
|
||||
RoutePatternFactory.Parse(template),
|
||||
new RouteValueDictionary(),
|
||||
0,
|
||||
new EndpointMetadataCollection(metadata),
|
||||
template);
|
||||
}
|
||||
|
||||
private protected static int[] SampleRequests(int endpointCount, int count)
|
||||
{
|
||||
// This isn't very high tech, but it's at least regular distribution.
|
||||
// We sort the route templates by precedence, so this should result in
|
||||
// an even distribution of the 'complexity' of the routes that are exercised.
|
||||
var frequency = endpointCount / count;
|
||||
if (frequency < 2)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The sample count is too high. This won't produce an accurate sampling" +
|
||||
"of the request data.");
|
||||
}
|
||||
|
||||
var samples = new int[count];
|
||||
for (var i = 0; i < samples.Length; i++)
|
||||
{
|
||||
samples[i] = i * frequency;
|
||||
}
|
||||
|
||||
return samples;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private protected void Validate(HttpContext httpContext, Endpoint expected, Endpoint actual)
|
||||
{
|
||||
if (!object.ReferenceEquals(expected, actual))
|
||||
{
|
||||
var message = new StringBuilder();
|
||||
message.AppendLine($"Validation failed for request {Array.IndexOf(Requests, httpContext)}");
|
||||
message.AppendLine($"{httpContext.Request.Method} {httpContext.Request.Path}");
|
||||
message.AppendLine($"expected: '{((MatcherEndpoint)expected)?.DisplayName ?? "null"}'");
|
||||
message.AppendLine($"actual: '{((MatcherEndpoint)actual)?.DisplayName ?? "null"}'");
|
||||
throw new InvalidOperationException(message.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// 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.Routing.Matchers
|
||||
namespace Microsoft.AspNetCore.Routing.Matching
|
||||
{
|
||||
public abstract class FastPathTokenizerBenchmarkBase
|
||||
{
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
using System;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
namespace Microsoft.AspNetCore.Routing.Matching
|
||||
{
|
||||
public class FastPathTokenizerEmptyBenchmark : FastPathTokenizerBenchmarkBase
|
||||
{
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
using System;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
namespace Microsoft.AspNetCore.Routing.Matching
|
||||
{
|
||||
public class FastPathTokenizerLargeBenchmark : FastPathTokenizerBenchmarkBase
|
||||
{
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
using System;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
namespace Microsoft.AspNetCore.Routing.Matching
|
||||
{
|
||||
public class FastPathTokenizerPlaintextBenchmark : FastPathTokenizerBenchmarkBase
|
||||
{
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
using System;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
namespace Microsoft.AspNetCore.Routing.Matching
|
||||
{
|
||||
public class FastPathTokenizerSmallBenchmark : FastPathTokenizerBenchmarkBase
|
||||
{
|
||||
|
|
@ -3,10 +3,9 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
namespace Microsoft.AspNetCore.Routing.Matching
|
||||
{
|
||||
public class JumpTableMultipleEntryBenchmark
|
||||
{
|
||||
|
|
@ -15,12 +14,11 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
|
||||
private JumpTable _linearSearch;
|
||||
private JumpTable _dictionary;
|
||||
private JumpTable _ascii;
|
||||
private JumpTable _dictionaryLookup;
|
||||
private JumpTable _customHashTable;
|
||||
private JumpTable _trie;
|
||||
private JumpTable _vectorTrie;
|
||||
|
||||
// All factors of 100 to support sampling
|
||||
[Params(2, 4, 5, 10, 25)]
|
||||
[Params(2, 5, 10, 25, 50, 100)]
|
||||
public int Count;
|
||||
|
||||
[GlobalSetup]
|
||||
|
|
@ -48,9 +46,8 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
|
||||
_linearSearch = new LinearSearchJumpTable(0, -1, entries.ToArray());
|
||||
_dictionary = new DictionaryJumpTable(0, -1, entries.ToArray());
|
||||
Debug.Assert(AsciiKeyedJumpTable.TryCreate(0, -1, entries, out _ascii));
|
||||
_dictionaryLookup = new DictionaryLookupJumpTable(0, -1, entries.ToArray());
|
||||
_customHashTable = new CustomHashTableJumpTable(0, -1, entries.ToArray());
|
||||
_trie = new ILEmitTrieJumpTable(0, -1, entries.ToArray(), vectorize: false, _dictionary);
|
||||
_vectorTrie = new ILEmitTrieJumpTable(0, -1, entries.ToArray(), vectorize: true, _dictionary);
|
||||
}
|
||||
|
||||
// This baseline is similar to SingleEntryJumpTable. We just want
|
||||
|
|
@ -67,15 +64,24 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
var @string = strings[i];
|
||||
var segment = segments[i];
|
||||
|
||||
destination = segment.Length == 0 ? -1 :
|
||||
segment.Length != @string.Length ? 1 :
|
||||
string.Compare(
|
||||
if (segment.Length == 0)
|
||||
{
|
||||
destination = -1;
|
||||
}
|
||||
else if (segment.Length != @string.Length)
|
||||
{
|
||||
destination = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
destination = string.Compare(
|
||||
@string,
|
||||
segment.Start,
|
||||
@string,
|
||||
0,
|
||||
@string.Length,
|
||||
segment.Length,
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
return destination;
|
||||
|
|
@ -112,7 +118,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
}
|
||||
|
||||
[Benchmark(OperationsPerInvoke = 100)]
|
||||
public int Ascii()
|
||||
public int Trie()
|
||||
{
|
||||
var strings = _strings;
|
||||
var segments = _segments;
|
||||
|
|
@ -120,14 +126,14 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
var destination = 0;
|
||||
for (var i = 0; i < strings.Length; i++)
|
||||
{
|
||||
destination = _ascii.GetDestination(strings[i], segments[i]);
|
||||
destination = _trie.GetDestination(strings[i], segments[i]);
|
||||
}
|
||||
|
||||
return destination;
|
||||
}
|
||||
|
||||
[Benchmark(OperationsPerInvoke = 100)]
|
||||
public int DictionaryLookup()
|
||||
public int VectorTrie()
|
||||
{
|
||||
var strings = _strings;
|
||||
var segments = _segments;
|
||||
|
|
@ -135,22 +141,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
var destination = 0;
|
||||
for (var i = 0; i < strings.Length; i++)
|
||||
{
|
||||
destination = _dictionaryLookup.GetDestination(strings[i], segments[i]);
|
||||
}
|
||||
|
||||
return destination;
|
||||
}
|
||||
|
||||
[Benchmark(OperationsPerInvoke = 100)]
|
||||
public int CustomHashTable()
|
||||
{
|
||||
var strings = _strings;
|
||||
var segments = _segments;
|
||||
|
||||
var destination = 0;
|
||||
for (var i = 0; i < strings.Length; i++)
|
||||
{
|
||||
destination = _customHashTable.GetDestination(strings[i], segments[i]);
|
||||
destination = _vectorTrie.GetDestination(strings[i], segments[i]);
|
||||
}
|
||||
|
||||
return destination;
|
||||
|
|
@ -164,7 +155,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
var guid = Guid.NewGuid().ToString();
|
||||
|
||||
// Between 5 and 36 characters
|
||||
var text = guid.Substring(0, Math.Max(5, Math.Min(count, 36)));
|
||||
var text = guid.Substring(0, Math.Max(5, Math.Min(i, 36)));
|
||||
if (char.IsDigit(text[0]))
|
||||
{
|
||||
// Convert first character to a letter.
|
||||
|
|
@ -0,0 +1,318 @@
|
|||
// 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.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matching
|
||||
{
|
||||
public class JumpTableSingleEntryBenchmark
|
||||
{
|
||||
private JumpTable _implementation;
|
||||
private JumpTable _prototype;
|
||||
private JumpTable _trie;
|
||||
private JumpTable _vectorTrie;
|
||||
|
||||
private string[] _strings;
|
||||
private PathSegment[] _segments;
|
||||
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
_implementation = new SingleEntryJumpTable(0, -1, "hello-world", 1);
|
||||
_prototype = new SingleEntryAsciiVectorizedJumpTable(0, -2, "hello-world", 1);
|
||||
_trie = new ILEmitTrieJumpTable(0, -1, new[] { ("hello-world", 1), }, vectorize: false, _implementation);
|
||||
_vectorTrie = new ILEmitTrieJumpTable(0, -1, new[] { ("hello-world", 1), }, vectorize: true, _implementation);
|
||||
|
||||
_strings = new string[]
|
||||
{
|
||||
"index/foo/2",
|
||||
"index/hello-world1/2",
|
||||
"index/hello-world/2",
|
||||
"index//2",
|
||||
"index/hillo-goodbye/2",
|
||||
};
|
||||
_segments = new PathSegment[]
|
||||
{
|
||||
new PathSegment(6, 3),
|
||||
new PathSegment(6, 12),
|
||||
new PathSegment(6, 11),
|
||||
new PathSegment(6, 0),
|
||||
new PathSegment(6, 13),
|
||||
};
|
||||
}
|
||||
|
||||
[Benchmark(Baseline = true, OperationsPerInvoke = 5)]
|
||||
public int Baseline()
|
||||
{
|
||||
var strings = _strings;
|
||||
var segments = _segments;
|
||||
|
||||
int destination = 0;
|
||||
for (var i = 0; i < strings.Length; i++)
|
||||
{
|
||||
var @string = strings[i];
|
||||
var segment = segments[i];
|
||||
|
||||
if (segment.Length == 0)
|
||||
{
|
||||
destination = -1;
|
||||
}
|
||||
else if (segment.Length != "hello-world".Length)
|
||||
{
|
||||
destination = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
destination = string.Compare(
|
||||
@string,
|
||||
segment.Start,
|
||||
"hello-world",
|
||||
0,
|
||||
segment.Length,
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
return destination;
|
||||
}
|
||||
|
||||
[Benchmark(OperationsPerInvoke = 5)]
|
||||
public int Implementation()
|
||||
{
|
||||
var strings = _strings;
|
||||
var segments = _segments;
|
||||
|
||||
var destination = 0;
|
||||
for (var i = 0; i < strings.Length; i++)
|
||||
{
|
||||
destination = _implementation.GetDestination(strings[i], segments[i]);
|
||||
}
|
||||
|
||||
return destination;
|
||||
}
|
||||
|
||||
[Benchmark(OperationsPerInvoke = 5)]
|
||||
public int Prototype()
|
||||
{
|
||||
var strings = _strings;
|
||||
var segments = _segments;
|
||||
|
||||
var destination = 0;
|
||||
for (var i = 0; i < strings.Length; i++)
|
||||
{
|
||||
destination = _prototype.GetDestination(strings[i], segments[i]);
|
||||
}
|
||||
|
||||
return destination;
|
||||
}
|
||||
|
||||
[Benchmark(OperationsPerInvoke = 5)]
|
||||
public int Trie()
|
||||
{
|
||||
var strings = _strings;
|
||||
var segments = _segments;
|
||||
|
||||
var destination = 0;
|
||||
for (var i = 0; i < strings.Length; i++)
|
||||
{
|
||||
destination = _trie.GetDestination(strings[i], segments[i]);
|
||||
}
|
||||
|
||||
return destination;
|
||||
}
|
||||
|
||||
[Benchmark(OperationsPerInvoke = 5)]
|
||||
public int VectorTrie()
|
||||
{
|
||||
var strings = _strings;
|
||||
var segments = _segments;
|
||||
|
||||
var destination = 0;
|
||||
for (var i = 0; i < strings.Length; i++)
|
||||
{
|
||||
destination = _vectorTrie.GetDestination(strings[i], segments[i]);
|
||||
}
|
||||
|
||||
return destination;
|
||||
}
|
||||
|
||||
private class SingleEntryAsciiVectorizedJumpTable : JumpTable
|
||||
{
|
||||
private readonly int _defaultDestination;
|
||||
private readonly int _exitDestination;
|
||||
private readonly string _text;
|
||||
private readonly int _destination;
|
||||
|
||||
private readonly ulong[] _values;
|
||||
private readonly int _residue0Lower;
|
||||
private readonly int _residue0Upper;
|
||||
private readonly int _residue1Lower;
|
||||
private readonly int _residue1Upper;
|
||||
private readonly int _residue2Lower;
|
||||
private readonly int _residue2Upper;
|
||||
|
||||
public SingleEntryAsciiVectorizedJumpTable(
|
||||
int defaultDestination,
|
||||
int exitDestination,
|
||||
string text,
|
||||
int destination)
|
||||
{
|
||||
_defaultDestination = defaultDestination;
|
||||
_exitDestination = exitDestination;
|
||||
_text = text;
|
||||
_destination = destination;
|
||||
|
||||
var length = text.Length;
|
||||
var span = text.ToLowerInvariant().AsSpan();
|
||||
ref var p = ref Unsafe.As<char, byte>(ref MemoryMarshal.GetReference(span));
|
||||
|
||||
_values = new ulong[length / 4];
|
||||
for (var i = 0; i < length / 4; i++)
|
||||
{
|
||||
_values[i] = Unsafe.ReadUnaligned<ulong>(ref p);
|
||||
p = Unsafe.Add(ref p, 64);
|
||||
}
|
||||
switch (length % 4)
|
||||
{
|
||||
case 1:
|
||||
{
|
||||
var c = Unsafe.ReadUnaligned<char>(ref p);
|
||||
_residue0Lower = char.ToLowerInvariant(c);
|
||||
_residue0Upper = char.ToUpperInvariant(c);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 2:
|
||||
{
|
||||
var c = Unsafe.ReadUnaligned<char>(ref p);
|
||||
_residue0Lower = char.ToLowerInvariant(c);
|
||||
_residue0Upper = char.ToUpperInvariant(c);
|
||||
|
||||
p = Unsafe.Add(ref p, 2);
|
||||
c = Unsafe.ReadUnaligned<char>(ref p);
|
||||
_residue1Lower = char.ToLowerInvariant(c);
|
||||
_residue1Upper = char.ToUpperInvariant(c);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 3:
|
||||
{
|
||||
var c = Unsafe.ReadUnaligned<char>(ref p);
|
||||
_residue0Lower = char.ToLowerInvariant(c);
|
||||
_residue0Upper = char.ToUpperInvariant(c);
|
||||
|
||||
p = Unsafe.Add(ref p, 2);
|
||||
c = Unsafe.ReadUnaligned<char>(ref p);
|
||||
_residue1Lower = char.ToLowerInvariant(c);
|
||||
_residue1Upper = char.ToUpperInvariant(c);
|
||||
|
||||
p = Unsafe.Add(ref p, 2);
|
||||
c = Unsafe.ReadUnaligned<char>(ref p);
|
||||
_residue2Lower = char.ToLowerInvariant(c);
|
||||
_residue2Upper = char.ToUpperInvariant(c);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override int GetDestination(string path, PathSegment segment)
|
||||
{
|
||||
var length = segment.Length;
|
||||
var span = path.AsSpan(segment.Start, length);
|
||||
ref var p = ref Unsafe.As<char, byte>(ref MemoryMarshal.GetReference(span));
|
||||
|
||||
var i = 0;
|
||||
while (length > 3)
|
||||
{
|
||||
var value = Unsafe.ReadUnaligned<ulong>(ref p);
|
||||
|
||||
if ((value & ~0x007F007F007F007FUL) == 0)
|
||||
{
|
||||
return _defaultDestination;
|
||||
}
|
||||
|
||||
var ulongLowerIndicator = value + (0x0080008000800080UL - 0x0041004100410041UL);
|
||||
var ulongUpperIndicator = value + (0x0080008000800080UL - 0x005B005B005B005BUL);
|
||||
var ulongCombinedIndicator = (ulongLowerIndicator ^ ulongUpperIndicator) & 0x0080008000800080UL;
|
||||
var mask = (ulongCombinedIndicator) >> 2;
|
||||
|
||||
value ^= mask;
|
||||
|
||||
if (value != _values[i])
|
||||
{
|
||||
return _defaultDestination;
|
||||
}
|
||||
|
||||
i++;
|
||||
length -= 4;
|
||||
p = ref Unsafe.Add(ref p, 64);
|
||||
}
|
||||
|
||||
switch (length)
|
||||
{
|
||||
case 1:
|
||||
{
|
||||
var c = Unsafe.ReadUnaligned<char>(ref p);
|
||||
if (c != _residue0Lower && c != _residue0Upper)
|
||||
{
|
||||
return _defaultDestination;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 2:
|
||||
{
|
||||
var c = Unsafe.ReadUnaligned<char>(ref p);
|
||||
if (c != _residue0Lower && c != _residue0Upper)
|
||||
{
|
||||
return _defaultDestination;
|
||||
}
|
||||
|
||||
p = ref Unsafe.Add(ref p, 2);
|
||||
c = Unsafe.ReadUnaligned<char>(ref p);
|
||||
if (c != _residue1Lower && c != _residue1Upper)
|
||||
{
|
||||
return _defaultDestination;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 3:
|
||||
{
|
||||
var c = Unsafe.ReadUnaligned<char>(ref p);
|
||||
if (c != _residue0Lower && c != _residue0Upper)
|
||||
{
|
||||
return _defaultDestination;
|
||||
}
|
||||
|
||||
p = ref Unsafe.Add(ref p, 2);
|
||||
c = Unsafe.ReadUnaligned<char>(ref p);
|
||||
if (c != _residue1Lower && c != _residue1Upper)
|
||||
{
|
||||
return _defaultDestination;
|
||||
}
|
||||
|
||||
p = ref Unsafe.Add(ref p, 2);
|
||||
c = Unsafe.ReadUnaligned<char>(ref p);
|
||||
if (c != _residue2Lower && c != _residue2Upper)
|
||||
{
|
||||
return _defaultDestination;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return _destination;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
using BenchmarkDotNet.Attributes;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
namespace Microsoft.AspNetCore.Routing.Matching
|
||||
{
|
||||
public class JumpTableZeroEntryBenchmark
|
||||
{
|
||||
|
|
@ -3,11 +3,12 @@
|
|||
|
||||
using System.Threading.Tasks;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
namespace Microsoft.AspNetCore.Routing.Matching
|
||||
{
|
||||
// Generated from https://github.com/Azure/azure-rest-api-specs
|
||||
public partial class MatcherAzureBenchmark : MatcherBenchmarkBase
|
||||
public partial class MatcherAzureBenchmark : EndpointRoutingBenchmarkBase
|
||||
{
|
||||
private const int SampleCount = 100;
|
||||
|
||||
|
|
@ -4,16 +4,16 @@
|
|||
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
namespace Microsoft.AspNetCore.Routing.Matching
|
||||
{
|
||||
// This code was generated by the Swaggatherer
|
||||
public partial class MatcherAzureBenchmark : MatcherBenchmarkBase
|
||||
public partial class MatcherAzureBenchmark : EndpointRoutingBenchmarkBase
|
||||
{
|
||||
private const int EndpointCount = 5160;
|
||||
|
||||
private void SetupEndpoints()
|
||||
{
|
||||
Endpoints = new MatcherEndpoint[5160];
|
||||
Endpoints = new RouteEndpoint[5160];
|
||||
Endpoints[0] = CreateEndpoint("/account", "GET");
|
||||
Endpoints[1] = CreateEndpoint("/analyze", "POST");
|
||||
Endpoints[2] = CreateEndpoint("/apis", "GET");
|
||||
|
|
@ -4,11 +4,15 @@
|
|||
using System;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
namespace Microsoft.AspNetCore.Routing.Matching
|
||||
{
|
||||
// Generated from https://github.com/Azure/azure-rest-api-specs
|
||||
public partial class MatcherFindCandidateSetAzureBenchmark : MatcherBenchmarkBase
|
||||
public partial class MatcherFindCandidateSetAzureBenchmark : EndpointRoutingBenchmarkBase
|
||||
{
|
||||
// SegmentCount should be max-segments + 1, but we don't have a good way to compute
|
||||
// it here, so using 32 as a safe guess.
|
||||
private const int SegmentCount = 32;
|
||||
|
||||
private const int SampleCount = 100;
|
||||
|
||||
private BarebonesMatcher _baseline;
|
||||
|
|
@ -58,7 +62,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
var httpContext = Requests[sample];
|
||||
|
||||
var path = httpContext.Request.Path.Value;
|
||||
Span<PathSegment> segments = stackalloc PathSegment[FastPathTokenizer.DefaultSegmentCount];
|
||||
Span<PathSegment> segments = stackalloc PathSegment[SegmentCount];
|
||||
var count = FastPathTokenizer.Tokenize(path, segments);
|
||||
|
||||
var candidates = _dfa.FindCandidateSet(httpContext, path, segments.Slice(0, count));
|
||||
|
|
@ -4,16 +4,16 @@
|
|||
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
namespace Microsoft.AspNetCore.Routing.Matching
|
||||
{
|
||||
// This code was generated by the Swaggatherer
|
||||
public partial class MatcherFindCandidateSetAzureBenchmark : MatcherBenchmarkBase
|
||||
public partial class MatcherFindCandidateSetAzureBenchmark : EndpointRoutingBenchmarkBase
|
||||
{
|
||||
private const int EndpointCount = 3517;
|
||||
|
||||
private void SetupEndpoints()
|
||||
{
|
||||
Endpoints = new MatcherEndpoint[3517];
|
||||
Endpoints = new RouteEndpoint[3517];
|
||||
Endpoints[0] = CreateEndpoint("/account");
|
||||
Endpoints[1] = CreateEndpoint("/analyze");
|
||||
Endpoints[2] = CreateEndpoint("/apis");
|
||||
|
|
@ -5,12 +5,16 @@ using System;
|
|||
using System.Threading.Tasks;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
namespace Microsoft.AspNetCore.Routing.Matching
|
||||
{
|
||||
// Generated from https://github.com/APIs-guru/openapi-directory
|
||||
// Use https://editor2.swagger.io/ to convert from yaml to json-
|
||||
public partial class MatcherFindCandidateSetGithubBenchmark : MatcherBenchmarkBase
|
||||
public partial class MatcherFindCandidateSetGithubBenchmark : EndpointRoutingBenchmarkBase
|
||||
{
|
||||
// SegmentCount should be max-segments + 1, but we don't have a good way to compute
|
||||
// it here, so using 32 as a safe guess.
|
||||
private const int SegmentCount = 32;
|
||||
|
||||
private BarebonesMatcher _baseline;
|
||||
private DfaMatcher _dfa;
|
||||
|
||||
|
|
@ -50,7 +54,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
var httpContext = Requests[i];
|
||||
|
||||
var path = httpContext.Request.Path.Value;
|
||||
Span<PathSegment> segments = stackalloc PathSegment[FastPathTokenizer.DefaultSegmentCount];
|
||||
Span<PathSegment> segments = stackalloc PathSegment[SegmentCount];
|
||||
var count = FastPathTokenizer.Tokenize(path, segments);
|
||||
|
||||
var candidates = _dfa.FindCandidateSet(httpContext, path, segments.Slice(0, count));
|
||||
|
|
@ -4,16 +4,16 @@
|
|||
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
namespace Microsoft.AspNetCore.Routing.Matching
|
||||
{
|
||||
// This code was generated by the Swaggatherer
|
||||
public partial class MatcherFindCandidateSetGithubBenchmark : MatcherBenchmarkBase
|
||||
public partial class MatcherFindCandidateSetGithubBenchmark : EndpointRoutingBenchmarkBase
|
||||
{
|
||||
private const int EndpointCount = 155;
|
||||
|
||||
private void SetupEndpoints()
|
||||
{
|
||||
Endpoints = new MatcherEndpoint[155];
|
||||
Endpoints = new RouteEndpoint[155];
|
||||
Endpoints[0] = CreateEndpoint("/emojis");
|
||||
Endpoints[1] = CreateEndpoint("/events");
|
||||
Endpoints[2] = CreateEndpoint("/feeds");
|
||||
|
|
@ -5,24 +5,27 @@ using System;
|
|||
using BenchmarkDotNet.Attributes;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
namespace Microsoft.AspNetCore.Routing.Matching
|
||||
{
|
||||
public class MatcheFindCandidateSetSingleEntryBenchmark : MatcherBenchmarkBase
|
||||
public class MatcherFindCandidateSetSingleEntryBenchmark : EndpointRoutingBenchmarkBase
|
||||
{
|
||||
// SegmentCount should be max-segments + 1
|
||||
private const int SegmentCount = 2;
|
||||
|
||||
private TrivialMatcher _baseline;
|
||||
private DfaMatcher _dfa;
|
||||
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
Endpoints = new MatcherEndpoint[1];
|
||||
Endpoints = new RouteEndpoint[1];
|
||||
Endpoints[0] = CreateEndpoint("/plaintext");
|
||||
|
||||
Requests = new HttpContext[1];
|
||||
Requests[0] = new DefaultHttpContext();
|
||||
Requests[0].RequestServices = CreateServices();
|
||||
Requests[0].Request.Path = "/plaintext";
|
||||
|
||||
|
||||
_baseline = (TrivialMatcher)SetupMatcher(new TrivialMatcherBuilder());
|
||||
_dfa = (DfaMatcher)SetupMatcher(CreateDfaMatcherBuilder());
|
||||
}
|
||||
|
|
@ -51,7 +54,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
{
|
||||
var httpContext = Requests[0];
|
||||
var path = httpContext.Request.Path.Value;
|
||||
Span<PathSegment> segments = stackalloc PathSegment[FastPathTokenizer.DefaultSegmentCount];
|
||||
Span<PathSegment> segments = stackalloc PathSegment[SegmentCount];
|
||||
var count = FastPathTokenizer.Tokenize(path, segments);
|
||||
|
||||
var candidates = _dfa.FindCandidateSet(httpContext, path, segments.Slice(0, count));
|
||||
|
|
@ -4,11 +4,15 @@
|
|||
using System;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
namespace Microsoft.AspNetCore.Routing.Matching
|
||||
{
|
||||
public class MatcherFindCandidateSetSmallEntryCountBenchmark : MatcherBenchmarkBase
|
||||
public class MatcherFindCandidateSetSmallEntryCountBenchmark : EndpointRoutingBenchmarkBase
|
||||
{
|
||||
// SegmentCount should be max-segments + 1
|
||||
private const int SegmentCount = 6;
|
||||
|
||||
private TrivialMatcher _baseline;
|
||||
private DfaMatcher _dfa;
|
||||
|
||||
|
|
@ -29,7 +33,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
|
||||
private void SetupEndpoints()
|
||||
{
|
||||
Endpoints = new MatcherEndpoint[10];
|
||||
Endpoints = new RouteEndpoint[10];
|
||||
Endpoints[0] = CreateEndpoint("/another-really-cool-entry");
|
||||
Endpoints[1] = CreateEndpoint("/Some-Entry");
|
||||
Endpoints[2] = CreateEndpoint("/a/path/with/more/segments");
|
||||
|
|
@ -87,7 +91,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
var httpContext = Requests[0];
|
||||
var path = httpContext.Request.Path.Value;
|
||||
|
||||
Span<PathSegment> segments = stackalloc PathSegment[FastPathTokenizer.DefaultSegmentCount];
|
||||
Span<PathSegment> segments = stackalloc PathSegment[SegmentCount];
|
||||
var count = FastPathTokenizer.Tokenize(path, segments);
|
||||
|
||||
var candidates = _dfa.FindCandidateSet(httpContext, path, segments.Slice(0, count));
|
||||
|
|
@ -3,12 +3,13 @@
|
|||
|
||||
using System.Threading.Tasks;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
namespace Microsoft.AspNetCore.Routing.Matching
|
||||
{
|
||||
// Generated from https://github.com/APIs-guru/openapi-directory
|
||||
// Use https://editor2.swagger.io/ to convert from yaml to json-
|
||||
public partial class MatcherGithubBenchmark : MatcherBenchmarkBase
|
||||
public partial class MatcherGithubBenchmark : EndpointRoutingBenchmarkBase
|
||||
{
|
||||
private BarebonesMatcher _baseline;
|
||||
private Matcher _dfa;
|
||||
|
|
@ -4,16 +4,16 @@
|
|||
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
namespace Microsoft.AspNetCore.Routing.Matching
|
||||
{
|
||||
// This code was generated by the Swaggatherer
|
||||
public partial class MatcherGithubBenchmark : MatcherBenchmarkBase
|
||||
public partial class MatcherGithubBenchmark : EndpointRoutingBenchmarkBase
|
||||
{
|
||||
private const int EndpointCount = 243;
|
||||
|
||||
private void SetupEndpoints()
|
||||
{
|
||||
Endpoints = new MatcherEndpoint[243];
|
||||
Endpoints = new RouteEndpoint[243];
|
||||
Endpoints[0] = CreateEndpoint("/emojis", "GET");
|
||||
Endpoints[1] = CreateEndpoint("/events", "GET");
|
||||
Endpoints[2] = CreateEndpoint("/feeds", "GET");
|
||||
|
|
@ -4,11 +4,12 @@
|
|||
using System.Threading.Tasks;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
namespace Microsoft.AspNetCore.Routing.Matching
|
||||
{
|
||||
// Just like TechEmpower Plaintext
|
||||
public partial class MatcherSingleEntryBenchmark : MatcherBenchmarkBase
|
||||
public partial class MatcherSingleEntryBenchmark : EndpointRoutingBenchmarkBase
|
||||
{
|
||||
private const int SampleCount = 100;
|
||||
|
||||
|
|
@ -22,7 +23,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
Endpoints = new MatcherEndpoint[1];
|
||||
Endpoints = new RouteEndpoint[1];
|
||||
Endpoints[0] = CreateEndpoint("/plaintext");
|
||||
|
||||
Requests = new HttpContext[1];
|
||||
|
|
@ -4,25 +4,26 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
namespace Microsoft.AspNetCore.Routing.Matching
|
||||
{
|
||||
// A test-only matcher implementation - used as a baseline for simpler
|
||||
// perf tests. The idea with this matcher is that we can cheat on the requirements
|
||||
// to establish a lower bound for perf comparisons.
|
||||
internal sealed class TrivialMatcher : Matcher
|
||||
{
|
||||
private readonly MatcherEndpoint _endpoint;
|
||||
private readonly RouteEndpoint _endpoint;
|
||||
private readonly Candidate[] _candidates;
|
||||
|
||||
public TrivialMatcher(MatcherEndpoint endpoint)
|
||||
public TrivialMatcher(RouteEndpoint endpoint)
|
||||
{
|
||||
_endpoint = endpoint;
|
||||
|
||||
_candidates = new Candidate[] { new Candidate(endpoint), };
|
||||
}
|
||||
|
||||
public sealed override Task MatchAsync(HttpContext httpContext, IEndpointFeature feature)
|
||||
public sealed override Task MatchAsync(HttpContext httpContext, EndpointFeature feature)
|
||||
{
|
||||
if (httpContext == null)
|
||||
{
|
||||
|
|
@ -38,7 +39,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
if (string.Equals(_endpoint.RoutePattern.RawText, path, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
feature.Endpoint = _endpoint;
|
||||
feature.Values = new RouteValueDictionary();
|
||||
feature.RouteValues = new RouteValueDictionary();
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
|
|
@ -4,13 +4,13 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
namespace Microsoft.AspNetCore.Routing.Matching
|
||||
{
|
||||
internal class TrivialMatcherBuilder : MatcherBuilder
|
||||
{
|
||||
private readonly List<MatcherEndpoint> _endpoints = new List<MatcherEndpoint>();
|
||||
private readonly List<RouteEndpoint> _endpoints = new List<RouteEndpoint>();
|
||||
|
||||
public override void AddEndpoint(MatcherEndpoint endpoint)
|
||||
public override void AddEndpoint(RouteEndpoint endpoint)
|
||||
{
|
||||
_endpoints.Add(endpoint);
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netcoreapp2.2</TargetFrameworks>
|
||||
<TargetFramework>netcoreapp2.2</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
<ServerGarbageCollection>true</ServerGarbageCollection>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
|
|
@ -18,24 +18,25 @@
|
|||
for perf comparisons.
|
||||
-->
|
||||
<ItemGroup>
|
||||
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matchers\BarebonesMatcher.cs">
|
||||
<Link>Matchers\BarebonesMatcher.cs</Link>
|
||||
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matching\BarebonesMatcher.cs">
|
||||
<Link>Matching\BarebonesMatcher.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matchers\BarebonesMatcherBuilder.cs">
|
||||
<Link>Matchers\BarebonesMatcherBuilder.cs</Link>
|
||||
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matching\BarebonesMatcherBuilder.cs">
|
||||
<Link>Matching\BarebonesMatcherBuilder.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matchers\RouteMatcher.cs">
|
||||
<Link>Matchers\RouteMatcher.cs</Link>
|
||||
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matching\RouteMatcher.cs">
|
||||
<Link>Matching\RouteMatcher.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matchers\RouteMatcherBuilder.cs">
|
||||
<Link>Matchers\RouteMatcherBuilder.cs</Link>
|
||||
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matching\RouteMatcherBuilder.cs">
|
||||
<Link>Matching\RouteMatcherBuilder.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matchers\TreeRouterMatcher.cs">
|
||||
<Link>Matchers\TreeRouterMatcher.cs</Link>
|
||||
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matching\TreeRouterMatcher.cs">
|
||||
<Link>Matching\TreeRouterMatcher.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matchers\TreeRouterMatcherBuilder.cs">
|
||||
<Link>Matchers\TreeRouterMatcherBuilder.cs</Link>
|
||||
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matching\TreeRouterMatcherBuilder.cs">
|
||||
<Link>Matching\TreeRouterMatcherBuilder.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\TestObjects\TestServiceProvider.cs" Link="Matching\TestServiceProvider.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -1,74 +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;
|
||||
|
||||
namespace Swaggatherer
|
||||
{
|
||||
internal static class Template
|
||||
{
|
||||
public static string Execute(IReadOnlyList<RouteEntry> entries)
|
||||
{
|
||||
var setupEndpointsLines = new List<string>();
|
||||
for (var i = 0; i < entries.Count; i++)
|
||||
{
|
||||
var entry = entries[i];
|
||||
var httpMethodText = entry.Method == null ? string.Empty : $", \"{entry.Method.ToUpperInvariant()}\"";
|
||||
setupEndpointsLines.Add($" _endpoints[{i}] = CreateEndpoint(\"{entry.Template.TemplateText}\"{httpMethodText});");
|
||||
}
|
||||
|
||||
var setupRequestsLines = new List<string>();
|
||||
for (var i = 0; i < entries.Count; i++)
|
||||
{
|
||||
setupRequestsLines.Add($" _requests[{i}] = new DefaultHttpContext();");
|
||||
setupRequestsLines.Add($" _requests[{i}].RequestServices = CreateServices();");
|
||||
setupRequestsLines.Add($" _requests[{i}].Request.Method = \"{entries[i].Method.ToUpperInvariant()}\";");
|
||||
setupRequestsLines.Add($" _requests[{i}].Request.Path = \"{entries[i].RequestUrl}\";");
|
||||
}
|
||||
|
||||
var setupMatcherLines = new List<string>();
|
||||
for (var i = 0; i < entries.Count; i++)
|
||||
{
|
||||
setupMatcherLines.Add($" builder.AddEndpoint(_endpoints[{i}]);");
|
||||
}
|
||||
|
||||
return string.Format(@"
|
||||
// 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;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
{{
|
||||
// This code was generated by the Swaggatherer
|
||||
public partial class GeneratedBenchmark : MatcherBenchmarkBase
|
||||
{{
|
||||
private const int EndpointCount = {3};
|
||||
|
||||
private void SetupEndpoints()
|
||||
{{
|
||||
_endpoints = new MatcherEndpoint[{3}];
|
||||
{0}
|
||||
}}
|
||||
|
||||
private void SetupRequests()
|
||||
{{
|
||||
_requests = new HttpContext[{3}];
|
||||
{1}
|
||||
}}
|
||||
|
||||
private Matcher SetupMatcher(MatcherBuilder builder)
|
||||
{{
|
||||
{2}
|
||||
return builder.Build();
|
||||
}}
|
||||
}}
|
||||
}}",
|
||||
string.Join(Environment.NewLine, setupEndpointsLines),
|
||||
string.Join(Environment.NewLine, setupRequestsLines),
|
||||
string.Join(Environment.NewLine, setupMatcherLines),
|
||||
entries.Count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -38,9 +38,12 @@
|
|||
<MoqPackageVersion>4.7.49</MoqPackageVersion>
|
||||
<NETStandardLibrary20PackageVersion>2.0.3</NETStandardLibrary20PackageVersion>
|
||||
<NewtonsoftJsonPackageVersion>11.0.2</NewtonsoftJsonPackageVersion>
|
||||
<XunitAnalyzersPackageVersion>0.9.0</XunitAnalyzersPackageVersion>
|
||||
<SystemReflectionEmitLightweightPackageVersion>4.3.0</SystemReflectionEmitLightweightPackageVersion>
|
||||
<SystemReflectionEmitPackageVersion>4.3.0</SystemReflectionEmitPackageVersion>
|
||||
<XunitAnalyzersPackageVersion>0.10.0</XunitAnalyzersPackageVersion>
|
||||
<XunitPackageVersion>2.3.1</XunitPackageVersion>
|
||||
<XunitRunnerVisualStudioPackageVersion>2.4.0-rc.1.build4038</XunitRunnerVisualStudioPackageVersion>
|
||||
<XunitRunnerVisualStudioPackageVersion>2.4.0</XunitRunnerVisualStudioPackageVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(DotNetPackageVersionPropsPath)" Condition=" '$(DotNetPackageVersionPropsPath)' != '' " />
|
||||
<PropertyGroup Label="Package Versions: Pinned" />
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -12,4 +12,8 @@
|
|||
<DotNetCoreRuntime Include="$(MicrosoftNETCoreApp21PackageVersion)" />
|
||||
<DotNetCoreRuntime Include="$(MicrosoftNETCoreApp22PackageVersion)" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<EnableBenchmarkValidation>true</EnableBenchmarkValidation>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -1,40 +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.Globalization;
|
||||
using Microsoft.AspNetCore.Routing.Matchers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace RoutingSample.Web
|
||||
{
|
||||
internal class EndsWithStringMatchProcessor : MatchProcessorBase
|
||||
{
|
||||
private readonly ILogger<EndsWithStringMatchProcessor> _logger;
|
||||
|
||||
public EndsWithStringMatchProcessor(ILogger<EndsWithStringMatchProcessor> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public override bool Process(object value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var valueString = Convert.ToString(value, CultureInfo.InvariantCulture);
|
||||
|
||||
var endsWith = valueString.EndsWith(ConstraintArgument, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (!endsWith)
|
||||
{
|
||||
_logger.LogDebug(
|
||||
$"Parameter '{ParameterName}' with value '{valueString}' does not end with '{ConstraintArgument}'.");
|
||||
}
|
||||
|
||||
return endsWith;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
// 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.Globalization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
|
||||
namespace RoutingSample.Web
|
||||
{
|
||||
internal class EndsWithStringRouteConstraint : IRouteConstraint
|
||||
{
|
||||
private readonly string _endsWith;
|
||||
|
||||
public EndsWithStringRouteConstraint(string endsWith)
|
||||
{
|
||||
_endsWith = endsWith;
|
||||
}
|
||||
|
||||
public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
|
||||
{
|
||||
var value = values[routeKey];
|
||||
if (value == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var valueString = Convert.ToString(value, CultureInfo.InvariantCulture);
|
||||
var endsWith = valueString.EndsWith(_endsWith, StringComparison.OrdinalIgnoreCase);
|
||||
return endsWith;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,8 +9,8 @@ namespace RoutingSample.Web
|
|||
{
|
||||
public class Program
|
||||
{
|
||||
public static readonly string GlobalRoutingScenario = "globalrouting";
|
||||
public static readonly string RouterScenario = "router";
|
||||
public const string EndpointRoutingScenario = "endpointrouting";
|
||||
public const string RouterScenario = "router";
|
||||
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
|
|
@ -25,7 +25,7 @@ namespace RoutingSample.Web
|
|||
if (args.Length == 0)
|
||||
{
|
||||
Console.WriteLine("Choose a sample to run:");
|
||||
Console.WriteLine($"1. {GlobalRoutingScenario}");
|
||||
Console.WriteLine($"1. {EndpointRoutingScenario}");
|
||||
Console.WriteLine($"2. {RouterScenario}");
|
||||
Console.WriteLine();
|
||||
|
||||
|
|
@ -40,18 +40,18 @@ namespace RoutingSample.Web
|
|||
switch (scenario)
|
||||
{
|
||||
case "1":
|
||||
case "globalrouting":
|
||||
startupType = typeof(UseGlobalRoutingStartup);
|
||||
case EndpointRoutingScenario:
|
||||
startupType = typeof(UseEndpointRoutingStartup);
|
||||
break;
|
||||
|
||||
case "2":
|
||||
case "router":
|
||||
case RouterScenario:
|
||||
startupType = typeof(UseRouterStartup);
|
||||
break;
|
||||
|
||||
default:
|
||||
Console.WriteLine($"unknown scenario {scenario}");
|
||||
Console.WriteLine($"usage: dotnet run -- ({GlobalRoutingScenario}|{RouterScenario})");
|
||||
Console.WriteLine($"usage: dotnet run -- ({EndpointRoutingScenario}|{RouterScenario})");
|
||||
throw new InvalidOperationException();
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,146 @@
|
|||
// 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.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Internal;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.Routing.Internal;
|
||||
using Microsoft.AspNetCore.Routing.Matching;
|
||||
using Microsoft.AspNetCore.Routing.Patterns;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
namespace RoutingSample.Web
|
||||
{
|
||||
public class UseEndpointRoutingStartup
|
||||
{
|
||||
private static readonly byte[] _homePayload = Encoding.UTF8.GetBytes("Endpoint Routing sample endpoints:" + Environment.NewLine + "/plaintext");
|
||||
private static readonly byte[] _helloWorldPayload = Encoding.UTF8.GetBytes("Hello, World!");
|
||||
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddTransient<EndsWithStringRouteConstraint>();
|
||||
|
||||
services.AddRouting(options =>
|
||||
{
|
||||
options.ConstraintMap.Add("endsWith", typeof(EndsWithStringRouteConstraint));
|
||||
});
|
||||
|
||||
var endpointDataSource = new DefaultEndpointDataSource(new[]
|
||||
{
|
||||
new RouteEndpoint((httpContext) =>
|
||||
{
|
||||
var response = httpContext.Response;
|
||||
var payloadLength = _homePayload.Length;
|
||||
response.StatusCode = 200;
|
||||
response.ContentType = "text/plain";
|
||||
response.ContentLength = payloadLength;
|
||||
return response.Body.WriteAsync(_homePayload, 0, payloadLength);
|
||||
},
|
||||
RoutePatternFactory.Parse("/"),
|
||||
0,
|
||||
EndpointMetadataCollection.Empty,
|
||||
"Home"),
|
||||
new RouteEndpoint((httpContext) =>
|
||||
{
|
||||
var response = httpContext.Response;
|
||||
var payloadLength = _helloWorldPayload.Length;
|
||||
response.StatusCode = 200;
|
||||
response.ContentType = "text/plain";
|
||||
response.ContentLength = payloadLength;
|
||||
return response.Body.WriteAsync(_helloWorldPayload, 0, payloadLength);
|
||||
},
|
||||
RoutePatternFactory.Parse("/plaintext"),
|
||||
0,
|
||||
EndpointMetadataCollection.Empty,
|
||||
"Plaintext"),
|
||||
new RouteEndpoint((httpContext) =>
|
||||
{
|
||||
var response = httpContext.Response;
|
||||
response.StatusCode = 200;
|
||||
response.ContentType = "text/plain";
|
||||
return response.WriteAsync("WithConstraints");
|
||||
},
|
||||
RoutePatternFactory.Parse("/withconstraints/{id:endsWith(_001)}"),
|
||||
0,
|
||||
EndpointMetadataCollection.Empty,
|
||||
"withconstraints"),
|
||||
new RouteEndpoint((httpContext) =>
|
||||
{
|
||||
var response = httpContext.Response;
|
||||
response.StatusCode = 200;
|
||||
response.ContentType = "text/plain";
|
||||
return response.WriteAsync("withoptionalconstraints");
|
||||
},
|
||||
RoutePatternFactory.Parse("/withoptionalconstraints/{id:endsWith(_001)?}"),
|
||||
0,
|
||||
EndpointMetadataCollection.Empty,
|
||||
"withoptionalconstraints"),
|
||||
new RouteEndpoint((httpContext) =>
|
||||
{
|
||||
using (var writer = new StreamWriter(httpContext.Response.Body, Encoding.UTF8, 1024, leaveOpen: true))
|
||||
{
|
||||
var graphWriter = httpContext.RequestServices.GetRequiredService<DfaGraphWriter>();
|
||||
var dataSource = httpContext.RequestServices.GetRequiredService<CompositeEndpointDataSource>();
|
||||
graphWriter.Write(dataSource, writer);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
RoutePatternFactory.Parse("/graph"),
|
||||
0,
|
||||
new EndpointMetadataCollection(new HttpMethodMetadata(new[]{ "GET", })),
|
||||
"DFA Graph"),
|
||||
new RouteEndpoint((httpContext) =>
|
||||
{
|
||||
var linkGenerator = httpContext.RequestServices.GetRequiredService<LinkGenerator>();
|
||||
|
||||
var response = httpContext.Response;
|
||||
response.StatusCode = 200;
|
||||
response.ContentType = "text/plain";
|
||||
return response.WriteAsync(
|
||||
"Link: " + linkGenerator.GetLink(httpContext, "WithSingleAsteriskCatchAll", new { }));
|
||||
},
|
||||
RoutePatternFactory.Parse("/WithSingleAsteriskCatchAll/{*path}"),
|
||||
0,
|
||||
new EndpointMetadataCollection(
|
||||
new RouteValuesAddressMetadata(
|
||||
name: "WithSingleAsteriskCatchAll",
|
||||
requiredValues: new RouteValueDictionary())),
|
||||
"WithSingleAsteriskCatchAll"),
|
||||
new RouteEndpoint((httpContext) =>
|
||||
{
|
||||
var linkGenerator = httpContext.RequestServices.GetRequiredService<LinkGenerator>();
|
||||
|
||||
var response = httpContext.Response;
|
||||
response.StatusCode = 200;
|
||||
response.ContentType = "text/plain";
|
||||
return response.WriteAsync(
|
||||
"Link: " + linkGenerator.GetLink(httpContext, "WithDoubleAsteriskCatchAll", new { }));
|
||||
},
|
||||
RoutePatternFactory.Parse("/WithDoubleAsteriskCatchAll/{**path}"),
|
||||
0,
|
||||
new EndpointMetadataCollection(
|
||||
new RouteValuesAddressMetadata(
|
||||
name: "WithDoubleAsteriskCatchAll",
|
||||
requiredValues: new RouteValueDictionary())),
|
||||
"WithDoubleAsteriskCatchAll"),
|
||||
});
|
||||
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<EndpointDataSource>(endpointDataSource));
|
||||
}
|
||||
|
||||
public void Configure(Microsoft.AspNetCore.Builder.IApplicationBuilder app)
|
||||
{
|
||||
app.UseEndpointRouting();
|
||||
|
||||
// Imagine some more stuff here...
|
||||
|
||||
app.UseEndpoint();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,98 +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.Text;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.Routing.Matchers;
|
||||
using Microsoft.AspNetCore.Routing.Patterns;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace RoutingSample.Web
|
||||
{
|
||||
public class UseGlobalRoutingStartup
|
||||
{
|
||||
private static readonly byte[] _homePayload = Encoding.UTF8.GetBytes("Global Routing sample endpoints:" + Environment.NewLine + "/plaintext");
|
||||
private static readonly byte[] _helloWorldPayload = Encoding.UTF8.GetBytes("Hello, World!");
|
||||
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddTransient<EndsWithStringMatchProcessor>();
|
||||
|
||||
services.AddRouting(options =>
|
||||
{
|
||||
options.ConstraintMap.Add("endsWith", typeof(EndsWithStringMatchProcessor));
|
||||
});
|
||||
|
||||
services.Configure<EndpointOptions>(options =>
|
||||
{
|
||||
options.DataSources.Add(new DefaultEndpointDataSource(new[]
|
||||
{
|
||||
new MatcherEndpoint((next) => (httpContext) =>
|
||||
{
|
||||
var response = httpContext.Response;
|
||||
var payloadLength = _homePayload.Length;
|
||||
response.StatusCode = 200;
|
||||
response.ContentType = "text/plain";
|
||||
response.ContentLength = payloadLength;
|
||||
return response.Body.WriteAsync(_homePayload, 0, payloadLength);
|
||||
},
|
||||
RoutePatternFactory.Parse("/"),
|
||||
new RouteValueDictionary(),
|
||||
0,
|
||||
EndpointMetadataCollection.Empty,
|
||||
"Home"),
|
||||
new MatcherEndpoint((next) => (httpContext) =>
|
||||
{
|
||||
var response = httpContext.Response;
|
||||
var payloadLength = _helloWorldPayload.Length;
|
||||
response.StatusCode = 200;
|
||||
response.ContentType = "text/plain";
|
||||
response.ContentLength = payloadLength;
|
||||
return response.Body.WriteAsync(_helloWorldPayload, 0, payloadLength);
|
||||
},
|
||||
RoutePatternFactory.Parse("/plaintext"),
|
||||
new RouteValueDictionary(),
|
||||
0,
|
||||
EndpointMetadataCollection.Empty,
|
||||
"Plaintext"),
|
||||
new MatcherEndpoint((next) => (httpContext) =>
|
||||
{
|
||||
var response = httpContext.Response;
|
||||
response.StatusCode = 200;
|
||||
response.ContentType = "text/plain";
|
||||
return response.WriteAsync("WithConstraints");
|
||||
},
|
||||
RoutePatternFactory.Parse("/withconstraints/{id:endsWith(_001)}"),
|
||||
new RouteValueDictionary(),
|
||||
0,
|
||||
EndpointMetadataCollection.Empty,
|
||||
"withconstraints"),
|
||||
new MatcherEndpoint((next) => (httpContext) =>
|
||||
{
|
||||
var response = httpContext.Response;
|
||||
response.StatusCode = 200;
|
||||
response.ContentType = "text/plain";
|
||||
return response.WriteAsync("withoptionalconstraints");
|
||||
},
|
||||
RoutePatternFactory.Parse("/withoptionalconstraints/{id:endsWith(_001)?}"),
|
||||
new RouteValueDictionary(),
|
||||
0,
|
||||
EndpointMetadataCollection.Empty,
|
||||
"withoptionalconstraints"),
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
public void Configure(IApplicationBuilder app)
|
||||
{
|
||||
app.UseGlobalRouting();
|
||||
|
||||
// Imagine some more stuff here...
|
||||
|
||||
app.UseEndpoint();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +1,49 @@
|
|||
// 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.Diagnostics;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
namespace Microsoft.AspNetCore.Http
|
||||
{
|
||||
[DebuggerDisplay("{DisplayName,nq}")]
|
||||
public abstract class Endpoint
|
||||
/// <summary>
|
||||
/// Respresents a logical endpoint in an application.
|
||||
/// </summary>
|
||||
public class Endpoint
|
||||
{
|
||||
protected Endpoint(
|
||||
/// <summary>
|
||||
/// Creates a new instance of <see cref="Endpoint"/>.
|
||||
/// </summary>
|
||||
/// <param name="requestDelegate">The delegate used to process requests for the endpoint.</param>
|
||||
/// <param name="metadata">
|
||||
/// The endpoint <see cref="EndpointMetadataCollection"/>. May be null.
|
||||
/// </param>
|
||||
/// <param name="displayName">
|
||||
/// The informational display name of the endpoint. May be null.
|
||||
/// </param>
|
||||
public Endpoint(
|
||||
RequestDelegate requestDelegate,
|
||||
EndpointMetadataCollection metadata,
|
||||
string displayName)
|
||||
{
|
||||
// All are allowed to be null
|
||||
RequestDelegate = requestDelegate;
|
||||
Metadata = metadata ?? EndpointMetadataCollection.Empty;
|
||||
DisplayName = displayName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the informational display name of this endpoint.
|
||||
/// </summary>
|
||||
public string DisplayName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of metadata associated with this endpoint.
|
||||
/// </summary>
|
||||
public EndpointMetadataCollection Metadata { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the delegate used to process requests for the endpoint.
|
||||
/// </summary>
|
||||
public RequestDelegate RequestDelegate { get; }
|
||||
|
||||
public override string ToString() => DisplayName ?? base.ToString();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,17 +3,35 @@
|
|||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
namespace Microsoft.AspNetCore.Http
|
||||
{
|
||||
public class EndpointMetadataCollection : IReadOnlyList<object>
|
||||
/// <summary>
|
||||
/// A collection of arbitrary metadata associated with an endpoint.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <see cref="EndpointMetadataCollection"/> instances contain a list of metadata items
|
||||
/// of arbitrary types. The metadata items are stored as an ordered collection with
|
||||
/// items arranged in ascending order of precedence.
|
||||
/// </remarks>
|
||||
public sealed class EndpointMetadataCollection : IReadOnlyList<object>
|
||||
{
|
||||
/// <summary>
|
||||
/// An empty <see cref="EndpointMetadataCollection"/>.
|
||||
/// </summary>
|
||||
public static readonly EndpointMetadataCollection Empty = new EndpointMetadataCollection(Array.Empty<object>());
|
||||
|
||||
private readonly object[] _items;
|
||||
private readonly ConcurrentDictionary<Type, object[]> _cache;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="EndpointMetadataCollection"/>.
|
||||
/// </summary>
|
||||
/// <param name="items">The metadata items.</param>
|
||||
public EndpointMetadataCollection(IEnumerable<object> items)
|
||||
{
|
||||
if (items == null)
|
||||
|
|
@ -22,80 +40,161 @@ namespace Microsoft.AspNetCore.Routing
|
|||
}
|
||||
|
||||
_items = items.ToArray();
|
||||
_cache = new ConcurrentDictionary<Type, object[]>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="EndpointMetadataCollection"/>.
|
||||
/// </summary>
|
||||
/// <param name="items">The metadata items.</param>
|
||||
public EndpointMetadataCollection(params object[] items)
|
||||
: this((IEnumerable<object>)items)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the item at <paramref name="index"/>.
|
||||
/// </summary>
|
||||
/// <param name="index">The index of the item to retrieve.</param>
|
||||
/// <returns>The item at <paramref name="index"/>.</returns>
|
||||
public object this[int index] => _items[index];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the count of metadata items.
|
||||
/// </summary>
|
||||
public int Count => _items.Length;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the most significant metadata item of type <typeparamref name="T"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of metadata to retrieve.</typeparam>
|
||||
/// <returns>
|
||||
/// The most significant metadata of type <typeparamref name="T"/> or <c>null</c>.
|
||||
/// </returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public T GetMetadata<T>() where T : class
|
||||
{
|
||||
for (var i = _items.Length - 1; i >= 0; i--)
|
||||
if (_cache.TryGetValue(typeof(T), out var result))
|
||||
{
|
||||
var item = _items[i] as T;
|
||||
if (item != null)
|
||||
{
|
||||
return item;
|
||||
}
|
||||
var length = result.Length;
|
||||
return length > 0 ? (T)result[length - 1] : default;
|
||||
}
|
||||
|
||||
return default;
|
||||
return GetMetadataSlow<T>();
|
||||
}
|
||||
|
||||
private T GetMetadataSlow<T>() where T : class
|
||||
{
|
||||
var array = GetOrderedMetadataSlow<T>();
|
||||
var length = array.Length;
|
||||
return length > 0 ? array[length - 1] : default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the metadata items of type <typeparamref name="T"/> in ascending
|
||||
/// order of precedence.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of metadata.</typeparam>
|
||||
/// <returns>A sequence of metadata items of <typeparamref name="T"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public IEnumerable<T> GetOrderedMetadata<T>() where T : class
|
||||
{
|
||||
for (var i = 0; i < _items.Length; i++)
|
||||
if (_cache.TryGetValue(typeof(T), out var result))
|
||||
{
|
||||
var item = _items[i] as T;
|
||||
if (item != null)
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
return (T[])result;
|
||||
}
|
||||
|
||||
return GetOrderedMetadataSlow<T>();
|
||||
}
|
||||
|
||||
private T[] GetOrderedMetadataSlow<T>() where T : class
|
||||
{
|
||||
var items = new List<T>();
|
||||
for (var i = 0; i < _items.Length; i++)
|
||||
{
|
||||
if (_items[i] is T item)
|
||||
{
|
||||
items.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
var array = items.ToArray();
|
||||
_cache.TryAdd(typeof(T), array);
|
||||
return array;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an <see cref="IEnumerator"/> of all metadata items.
|
||||
/// </summary>
|
||||
/// <returns>An <see cref="IEnumerator"/> of all metadata items.</returns>
|
||||
public Enumerator GetEnumerator() => new Enumerator(this);
|
||||
|
||||
/// <summary>
|
||||
/// Gets an <see cref="IEnumerator{Object}"/> of all metadata items.
|
||||
/// </summary>
|
||||
/// <returns>An <see cref="IEnumerator{Object}"/> of all metadata items.</returns>
|
||||
IEnumerator<object> IEnumerable<object>.GetEnumerator() => GetEnumerator();
|
||||
|
||||
/// <summary>
|
||||
/// Gets an <see cref="IEnumerator"/> of all metadata items.
|
||||
/// </summary>
|
||||
/// <returns>An <see cref="IEnumerator"/> of all metadata items.</returns>
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates the elements of an <see cref="EndpointMetadataCollection"/>.
|
||||
/// </summary>
|
||||
public struct Enumerator : IEnumerator<object>
|
||||
{
|
||||
// Intentionally not readonly to prevent defensive struct copies
|
||||
private object[] _items;
|
||||
private int _index;
|
||||
private object _current;
|
||||
|
||||
internal Enumerator(EndpointMetadataCollection collection)
|
||||
{
|
||||
_items = collection._items;
|
||||
_index = 0;
|
||||
_current = null;
|
||||
Current = null;
|
||||
}
|
||||
|
||||
public object Current => _current;
|
||||
/// <summary>
|
||||
/// Gets the element at the current position of the enumerator
|
||||
/// </summary>
|
||||
public object Current { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Releases all resources used by the <see cref="Enumerator"/>.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Advances the enumerator to the next element of the <see cref="Enumerator"/>.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the enumerator was successfully advanced to the next element;
|
||||
/// <c>false</c> if the enumerator has passed the end of the collection.
|
||||
/// </returns>
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (_index < _items.Length)
|
||||
{
|
||||
_current = _items[_index++];
|
||||
Current = _items[_index++];
|
||||
return true;
|
||||
}
|
||||
|
||||
_current = null;
|
||||
Current = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the enumerator to its initial position, which is before the first element in the collection.
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
_index = 0;
|
||||
_current = null;
|
||||
Current = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,18 @@
|
|||
// 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.Routing
|
||||
namespace Microsoft.AspNetCore.Http.Features
|
||||
{
|
||||
/// <summary>
|
||||
/// A feature interface for endpoint routing. Use <see cref="HttpContext.Features"/>
|
||||
/// to access an instance associated with the current request.
|
||||
/// </summary>
|
||||
public interface IEndpointFeature
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the selected <see cref="Http.Endpoint"/> for the current
|
||||
/// request.
|
||||
/// </summary>
|
||||
Endpoint Endpoint { get; set; }
|
||||
|
||||
Func<RequestDelegate, RequestDelegate> Invoker { get; set; }
|
||||
|
||||
RouteValueDictionary Values { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +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.
|
||||
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
public interface INameMetadata
|
||||
/// <summary>
|
||||
/// A marker interface for types that are associated with route parameters.
|
||||
/// </summary>
|
||||
public interface IParameterPolicy
|
||||
{
|
||||
string Name { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Routing
|
|||
/// Defines the contract that a class must implement in order to check whether a URL parameter
|
||||
/// value is valid for a constraint.
|
||||
/// </summary>
|
||||
public interface IRouteConstraint
|
||||
public interface IRouteConstraint : IParameterPolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines whether the URL parameter contains a valid value for this constraint.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
// 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.Routing;
|
||||
|
||||
namespace Microsoft.AspNetCore.Http.Features
|
||||
{
|
||||
public interface IRouteValuesFeature
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="RouteValueDictionary"/> associated with the currrent
|
||||
/// request.
|
||||
/// </summary>
|
||||
RouteValueDictionary RouteValues { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
// 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.Routing
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines a contract to generate a URL from a template.
|
||||
/// </summary>
|
||||
public abstract class LinkGenerationTemplate
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a URL with an absolute path from the specified route values.
|
||||
/// </summary>
|
||||
/// <param name="values">An object that contains route values.</param>
|
||||
/// <returns>The generated URL.</returns>
|
||||
public string MakeUrl(object values)
|
||||
{
|
||||
return MakeUrl(values, options: null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a URL with an absolute path from the specified route values and link options.
|
||||
/// </summary>
|
||||
/// <param name="values">An object that contains route values.</param>
|
||||
/// <param name="options">The <see cref="LinkOptions"/>.</param>
|
||||
/// <returns>The generated URL.</returns>
|
||||
public abstract string MakeUrl(object values, LinkOptions options);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,463 @@
|
|||
// 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.Routing
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines a contract to generate URLs to endpoints.
|
||||
/// </summary>
|
||||
public abstract class LinkGenerator
|
||||
{
|
||||
public abstract bool TryGetLink(LinkGeneratorContext context, out string link);
|
||||
/// <summary>
|
||||
/// Generates a URL with an absolute path from the specified route values.
|
||||
/// </summary>
|
||||
/// <param name="values">An object that contains route values.</param>
|
||||
/// <returns>The generated URL.</returns>
|
||||
public string GetLink(object values)
|
||||
{
|
||||
return GetLink(httpContext: null, routeName: null, values, options: null);
|
||||
}
|
||||
|
||||
public abstract string GetLink(LinkGeneratorContext context);
|
||||
/// <summary>
|
||||
/// Generates a URL with an absolute path from the specified route values and link options.
|
||||
/// </summary>
|
||||
/// <param name="values">An object that contains route values.</param>
|
||||
/// <param name="options">The <see cref="LinkOptions"/>.</param>
|
||||
/// <returns>The generated URL.</returns>
|
||||
public string GetLink(object values, LinkOptions options)
|
||||
{
|
||||
return GetLink(httpContext: null, routeName: null, values, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a URL with an absolute path from the specified route values.
|
||||
/// A return value indicates whether the operation succeeded.
|
||||
/// </summary>
|
||||
/// <param name="values">An object that contains route values.</param>
|
||||
/// <param name="link">The generated URL.</param>
|
||||
/// <returns><c>true</c> if a URL was generated successfully; otherwise, <c>false</c>.</returns>
|
||||
public bool TryGetLink(object values, out string link)
|
||||
{
|
||||
return TryGetLink(httpContext: null, routeName: null, values, options: null, out link);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a URL with an absolute path from the specified route values and link options.
|
||||
/// A return value indicates whether the operation succeeded.
|
||||
/// </summary>
|
||||
/// <param name="values">An object that contains route values.</param>
|
||||
/// <param name="options">The <see cref="LinkOptions"/>.</param>
|
||||
/// <param name="link">The generated URL.</param>
|
||||
/// <returns><c>true</c> if a URL was generated successfully; otherwise, <c>false</c>.</returns>
|
||||
public bool TryGetLink(object values, LinkOptions options, out string link)
|
||||
{
|
||||
return TryGetLink(httpContext: null, routeName: null, values, options, out link);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a URL with an absolute path from the specified route values.
|
||||
/// </summary>
|
||||
/// <param name="httpContext">The <see cref="HttpContext"/> associated with current request.</param>
|
||||
/// <param name="values">An object that contains route values.</param>
|
||||
/// <returns>The generated URL.</returns>
|
||||
public string GetLink(HttpContext httpContext, object values)
|
||||
{
|
||||
return GetLink(httpContext, routeName: null, values, options: null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a URL with an absolute path from the specified route values.
|
||||
/// A return value indicates whether the operation succeeded.
|
||||
/// </summary>
|
||||
/// <param name="httpContext">The <see cref="HttpContext"/> associated with current request.</param>
|
||||
/// <param name="values">An object that contains route values.</param>
|
||||
/// <param name="link">The generated URL.</param>
|
||||
/// <returns><c>true</c> if a URL was generated successfully; otherwise, <c>false</c>.</returns>
|
||||
public bool TryGetLink(HttpContext httpContext, object values, out string link)
|
||||
{
|
||||
return TryGetLink(httpContext, routeName: null, values, options: null, out link);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a URL with an absolute path from the specified route values and link options.
|
||||
/// </summary>
|
||||
/// <param name="httpContext">The <see cref="HttpContext"/> associated with current request.</param>
|
||||
/// <param name="values">An object that contains route values.</param>
|
||||
/// <param name="options">The <see cref="LinkOptions"/>.</param>
|
||||
/// <returns>The generated URL.</returns>
|
||||
public string GetLink(HttpContext httpContext, object values, LinkOptions options)
|
||||
{
|
||||
return GetLink(httpContext, routeName: null, values, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a URL with an absolute path from the specified route values and link options.
|
||||
/// A return value indicates whether the operation succeeded.
|
||||
/// </summary>
|
||||
/// <param name="httpContext">The <see cref="HttpContext"/> associated with current request.</param>
|
||||
/// <param name="values">An object that contains route values.</param>
|
||||
/// <param name="options">The <see cref="LinkOptions"/>.</param>
|
||||
/// <param name="link">The generated URL.</param>
|
||||
/// <returns><c>true</c> if a URL was generated successfully; otherwise, <c>false</c>.</returns>
|
||||
public bool TryGetLink(HttpContext httpContext, object values, LinkOptions options, out string link)
|
||||
{
|
||||
return TryGetLink(httpContext, routeName: null, values, options, out link);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a URL with an absolute path from the specified route name and route values.
|
||||
/// </summary>
|
||||
/// <param name="routeName">The name of the route to generate the URL to.</param>
|
||||
/// <param name="values">An object that contains route values.</param>
|
||||
/// <returns>The generated URL.</returns>
|
||||
public string GetLink(string routeName, object values)
|
||||
{
|
||||
return GetLink(httpContext: null, routeName, values, options: null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a URL with an absolute path from the specified route name and route values.
|
||||
/// A return value indicates whether the operation succeeded.
|
||||
/// </summary>
|
||||
/// <param name="routeName">The name of the route to generate the URL to.</param>
|
||||
/// <param name="values">An object that contains route values.</param>
|
||||
/// <param name="link">The generated URL.</param>
|
||||
/// <returns><c>true</c> if a URL was generated successfully; otherwise, <c>false</c>.</returns>
|
||||
public bool TryGetLink(string routeName, object values, out string link)
|
||||
{
|
||||
return TryGetLink(httpContext: null, routeName, values, options: null, out link);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a URL with an absolute path from the specified route name and route values.
|
||||
/// </summary>
|
||||
/// <param name="routeName">The name of the route to generate the URL to.</param>
|
||||
/// <param name="values">An object that contains route values.</param>
|
||||
/// <param name="options">The <see cref="LinkOptions"/>.</param>
|
||||
/// <returns>The generated URL.</returns>
|
||||
public string GetLink(string routeName, object values, LinkOptions options)
|
||||
{
|
||||
return GetLink(httpContext: null, routeName, values, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a URL with an absolute path from the specified route name, route values and link options.
|
||||
/// A return value indicates whether the operation succeeded.
|
||||
/// </summary>
|
||||
/// <param name="routeName">The name of the route to generate the URL to.</param>
|
||||
/// <param name="values">An object that contains route values.</param>
|
||||
/// <param name="options">The <see cref="LinkOptions"/>.</param>
|
||||
/// <param name="link">The generated URL.</param>
|
||||
/// <returns><c>true</c> if a URL was generated successfully; otherwise, <c>false</c>.</returns>
|
||||
public bool TryGetLink(string routeName, object values, LinkOptions options, out string link)
|
||||
{
|
||||
return TryGetLink(httpContext: null, routeName, values, options, out link);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a URL with an absolute path from the specified route name and route values.
|
||||
/// </summary>
|
||||
/// <param name="routeName">The name of the route to generate the URL to.</param>
|
||||
/// <param name="httpContext">The <see cref="HttpContext"/> associated with current request.</param>
|
||||
/// <param name="values">An object that contains route values.</param>
|
||||
/// <returns>The generated URL.</returns>
|
||||
public string GetLink(HttpContext httpContext, string routeName, object values)
|
||||
{
|
||||
return GetLink(httpContext, routeName, values, options: null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a URL with an absolute path from the specified route name and route values.
|
||||
/// A return value indicates whether the operation succeeded.
|
||||
/// </summary>
|
||||
/// <param name="httpContext">The <see cref="HttpContext"/> associated with current request.</param>
|
||||
/// <param name="routeName">The name of the route to generate the URL to.</param>
|
||||
/// <param name="values">An object that contains route values.</param>
|
||||
/// <param name="link">The generated URL.</param>
|
||||
/// <returns><c>true</c> if a URL was generated successfully; otherwise, <c>false</c>.</returns>
|
||||
public bool TryGetLink(HttpContext httpContext, string routeName, object values, out string link)
|
||||
{
|
||||
return TryGetLink(httpContext, routeName, values, options: null, out link);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a URL with an absolute path from the specified route name, route values and link options.
|
||||
/// </summary>
|
||||
/// <param name="routeName">The name of the route to generate the URL to.</param>
|
||||
/// <param name="httpContext">The <see cref="HttpContext"/> associated with current request.</param>
|
||||
/// <param name="values">An object that contains route values.</param>
|
||||
/// <param name="options">The <see cref="LinkOptions"/>.</param>
|
||||
/// <returns>The generated URL.</returns>
|
||||
public string GetLink(HttpContext httpContext, string routeName, object values, LinkOptions options)
|
||||
{
|
||||
if (TryGetLink(httpContext, routeName, values, options, out var link))
|
||||
{
|
||||
return link;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Could not find a matching endpoint to generate a link.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a URL with an absolute path from the specified route name, route values and link options.
|
||||
/// A return value indicates whether the operation succeeded.
|
||||
/// </summary>
|
||||
/// <param name="httpContext">The <see cref="HttpContext"/> associated with current request.</param>
|
||||
/// <param name="routeName">The name of the route to generate the URL to.</param>
|
||||
/// <param name="values">An object that contains route values.</param>
|
||||
/// <param name="options">The <see cref="LinkOptions"/>.</param>
|
||||
/// <param name="link">The generated URL.</param>
|
||||
/// <returns><c>true</c> if a URL was generated successfully; otherwise, <c>false</c>.</returns>
|
||||
public abstract bool TryGetLink(
|
||||
HttpContext httpContext,
|
||||
string routeName,
|
||||
object values,
|
||||
LinkOptions options,
|
||||
out string link);
|
||||
|
||||
/// <summary>
|
||||
/// Generates a URL with an absolute path from the specified lookup information and route values.
|
||||
/// This lookup information is used to find endpoints using a registered 'IEndpointFinder<TAddress>'.
|
||||
/// </summary>
|
||||
/// <typeparam name="TAddress">The address type to look up endpoints.</typeparam>
|
||||
/// <param name="address">The information used to look up endpoints for generating a URL.</param>
|
||||
/// <param name="values">An object that contains route values.</param>
|
||||
/// <returns>The generated URL.</returns>
|
||||
public string GetLinkByAddress<TAddress>(TAddress address, object values)
|
||||
{
|
||||
return GetLinkByAddress(httpContext: null, address, values, options: null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a URL with an absolute path from the specified lookup information and route values.
|
||||
/// This lookup information is used to find endpoints using a registered 'IEndpointFinder<TAddress>'.
|
||||
/// A return value indicates whether the operation succeeded.
|
||||
/// </summary>
|
||||
/// <typeparam name="TAddress">The address type to look up endpoints.</typeparam>
|
||||
/// <param name="address">The information used to look up endpoints for generating a URL.</param>
|
||||
/// <param name="values">An object that contains route values.</param>
|
||||
/// <param name="link">The generated URL.</param>
|
||||
/// <returns><c>true</c> if a URL was generated successfully; otherwise, <c>false</c>.</returns>
|
||||
public bool TryGetLinkByAddress<TAddress>(TAddress address, object values, out string link)
|
||||
{
|
||||
return TryGetLinkByAddress(address, values, options: null, out link);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a URL with an absolute path from the specified lookup information, route values and link options.
|
||||
/// This lookup information is used to find endpoints using a registered 'IEndpointFinder<TAddress>'.
|
||||
/// </summary>
|
||||
/// <typeparam name="TAddress">The address type to look up endpoints.</typeparam>
|
||||
/// <param name="address">The information used to look up endpoints for generating a URL.</param>
|
||||
/// <param name="values">An object that contains route values.</param>
|
||||
/// <param name="options">The <see cref="LinkOptions"/>.</param>
|
||||
/// <returns>The generated URL.</returns>
|
||||
public string GetLinkByAddress<TAddress>(TAddress address, object values, LinkOptions options)
|
||||
{
|
||||
return GetLinkByAddress(httpContext: null, address, values, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a URL with an absolute path from the specified lookup information, route values and link options.
|
||||
/// This lookup information is used to find endpoints using a registered 'IEndpointFinder<TAddress>'.
|
||||
/// A return value indicates whether the operation succeeded.
|
||||
/// </summary>
|
||||
/// <typeparam name="TAddress">The address type to look up endpoints.</typeparam>
|
||||
/// <param name="address">The information used to look up endpoints for generating a URL.</param>
|
||||
/// <param name="values">An object that contains route values.</param>
|
||||
/// <param name="options">The <see cref="LinkOptions"/>.</param>
|
||||
/// <param name="link">The generated URL.</param>
|
||||
/// <returns><c>true</c> if a URL was generated successfully; otherwise, <c>false</c>.</returns>
|
||||
public bool TryGetLinkByAddress<TAddress>(
|
||||
TAddress address,
|
||||
object values,
|
||||
LinkOptions options,
|
||||
out string link)
|
||||
{
|
||||
return TryGetLinkByAddress(httpContext: null, address, values, options, out link);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a URL with an absolute path from the specified lookup information, route values and link options.
|
||||
/// This lookup information is used to find endpoints using a registered 'IEndpointFinder<TAddress>'.
|
||||
/// </summary>
|
||||
/// <typeparam name="TAddress">The address type to look up endpoints.</typeparam>
|
||||
/// <param name="address">The information used to look up endpoints for generating a URL.</param>
|
||||
/// <param name="httpContext">The <see cref="HttpContext"/> associated with current request.</param>
|
||||
/// <param name="values">An object that contains route values.</param>
|
||||
/// <returns>The generated URL.</returns>
|
||||
public string GetLinkByAddress<TAddress>(HttpContext httpContext, TAddress address, object values)
|
||||
{
|
||||
return GetLinkByAddress(httpContext, address, values, options: null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a URL with an absolute path from the specified lookup information and route values.
|
||||
/// This lookup information is used to find endpoints using a registered 'IEndpointFinder<TAddress>'.
|
||||
/// A return value indicates whether the operation succeeded.
|
||||
/// </summary>
|
||||
/// <typeparam name="TAddress">The address type to look up endpoints.</typeparam>
|
||||
/// <param name="address">The information used to look up endpoints for generating a URL.</param>
|
||||
/// <param name="httpContext">The <see cref="HttpContext"/> associated with current request.</param>
|
||||
/// <param name="values">An object that contains route values.</param>
|
||||
/// <param name="link">The generated URL.</param>
|
||||
/// <returns><c>true</c> if a URL was generated successfully; otherwise, <c>false</c>.</returns>
|
||||
public bool TryGetLinkByAddress<TAddress>(
|
||||
HttpContext httpContext,
|
||||
TAddress address,
|
||||
object values,
|
||||
out string link)
|
||||
{
|
||||
return TryGetLinkByAddress(httpContext, address, values, options: null, out link);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a URL with an absolute path from the specified lookup information, route values and link options.
|
||||
/// This lookup information is used to find endpoints using a registered 'IEndpointFinder<TAddress>'.
|
||||
/// </summary>
|
||||
/// <typeparam name="TAddress">The address type to look up endpoints.</typeparam>
|
||||
/// <param name="address">The information used to look up endpoints for generating a URL.</param>
|
||||
/// <param name="httpContext">The <see cref="HttpContext"/> associated with current request.</param>
|
||||
/// <param name="values">An object that contains route values.</param>
|
||||
/// <param name="options">The <see cref="LinkOptions"/>.</param>
|
||||
/// <returns>The generated URL.</returns>
|
||||
public string GetLinkByAddress<TAddress>(
|
||||
HttpContext httpContext,
|
||||
TAddress address,
|
||||
object values,
|
||||
LinkOptions options)
|
||||
{
|
||||
if (TryGetLinkByAddress(httpContext, address, values, options, out var link))
|
||||
{
|
||||
return link;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Could not find a matching endpoint to generate a link.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a URL with an absolute path from the specified lookup information, route values and link options.
|
||||
/// This lookup information is used to find endpoints using a registered 'IEndpointFinder<TAddress>'.
|
||||
/// A return value indicates whether the operation succeeded.
|
||||
/// </summary>
|
||||
/// <typeparam name="TAddress">The address type to look up endpoints.</typeparam>
|
||||
/// <param name="address">The information used to look up endpoints for generating a URL.</param>
|
||||
/// <param name="httpContext">The <see cref="HttpContext"/> associated with current request.</param>
|
||||
/// <param name="values">An object that contains route values.</param>
|
||||
/// <param name="options">The <see cref="LinkOptions"/>.</param>
|
||||
/// <param name="link">The generated URL.</param>
|
||||
/// <returns><c>true</c> if a URL was generated successfully; otherwise, <c>false</c>.</returns>
|
||||
public abstract bool TryGetLinkByAddress<TAddress>(
|
||||
HttpContext httpContext,
|
||||
TAddress address,
|
||||
object values,
|
||||
LinkOptions options,
|
||||
out string link);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="LinkGenerationTemplate"/> to generate a URL from the specified route values.
|
||||
/// This template object holds information of the endpoint(s) that were found and which can later be used to
|
||||
/// generate a URL using the <see cref="LinkGenerationTemplate.MakeUrl(object, LinkOptions)"/> api.
|
||||
/// </summary>
|
||||
/// <param name="values">
|
||||
/// An object that contains route values. These values are used to lookup endpoint(s).
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// If an endpoint(s) was found successfully, then this returns a template object representing that,
|
||||
/// <c>null</c> otherwise.
|
||||
/// </returns>
|
||||
public LinkGenerationTemplate GetTemplate(object values)
|
||||
{
|
||||
return GetTemplate(httpContext: null, routeName: null, values);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="LinkGenerationTemplate"/> to generate a URL from the specified route name and route values.
|
||||
/// This template object holds information of the endpoint(s) that were found and which can later be used to
|
||||
/// generate a URL using the <see cref="LinkGenerationTemplate.MakeUrl(object, LinkOptions)"/> api.
|
||||
/// </summary>
|
||||
/// <param name="routeName">The name of the route to generate the URL to.</param>
|
||||
/// <param name="values">
|
||||
/// An object that contains route values. These values are used to lookup for endpoint(s).
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// If an endpoint(s) was found successfully, then this returns a template object representing that,
|
||||
/// <c>null</c> otherwise.
|
||||
/// </returns>
|
||||
public LinkGenerationTemplate GetTemplate(string routeName, object values)
|
||||
{
|
||||
return GetTemplate(httpContext: null, routeName, values);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="LinkGenerationTemplate"/> to generate a URL from the specified route values.
|
||||
/// This template object holds information of the endpoint(s) that were found and which can later be used to
|
||||
/// generate a URL using the <see cref="LinkGenerationTemplate.MakeUrl(object, LinkOptions)"/> api.
|
||||
/// </summary>
|
||||
/// <param name="httpContext">The <see cref="HttpContext"/> associated with current request.</param>
|
||||
/// <param name="values">
|
||||
/// An object that contains route values. These values are used to lookup for endpoint(s).
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// If an endpoint(s) was found successfully, then this returns a template object representing that,
|
||||
/// <c>null</c> otherwise.
|
||||
/// </returns>
|
||||
public LinkGenerationTemplate GetTemplate(HttpContext httpContext, object values)
|
||||
{
|
||||
return GetTemplate(httpContext, routeName: null, values);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="LinkGenerationTemplate"/> to generate a URL from the specified route name and route values.
|
||||
/// This template object holds information of the endpoint(s) that were found and which can later be used to
|
||||
/// generate a URL using the <see cref="LinkGenerationTemplate.MakeUrl(object, LinkOptions)"/> api.
|
||||
/// </summary>
|
||||
/// <param name="httpContext">The <see cref="HttpContext"/> associated with current request.</param>
|
||||
/// <param name="routeName">The name of the route to generate the URL to.</param>
|
||||
/// <param name="values">
|
||||
/// An object that contains route values. These values are used to lookup for endpoint(s).
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// If an endpoint(s) was found successfully, then this returns a template object representing that,
|
||||
/// <c>null</c> otherwise.
|
||||
/// </returns>
|
||||
public abstract LinkGenerationTemplate GetTemplate(HttpContext httpContext, string routeName, object values);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="LinkGenerationTemplate"/> to generate a URL from the specified lookup information.
|
||||
/// This template object holds information of the endpoint(s) that were found and which can later be used to
|
||||
/// generate a URL using the <see cref="LinkGenerationTemplate.MakeUrl(object, LinkOptions)"/> api.
|
||||
/// The lookup information is used to find endpoints using a registered 'IEndpointFinder<TAddress>'.
|
||||
/// </summary>
|
||||
/// <typeparam name="TAddress">The address type to look up endpoints.</typeparam>
|
||||
/// <param name="address">The information used to look up endpoints for creating a template.</param>
|
||||
/// <returns>
|
||||
/// If an endpoint(s) was found successfully, then this returns a template object representing that,
|
||||
/// <c>null</c> otherwise.
|
||||
/// </returns>
|
||||
public LinkGenerationTemplate GetTemplateByAddress<TAddress>(TAddress address)
|
||||
{
|
||||
return GetTemplateByAddress(httpContext: null, address);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="LinkGenerationTemplate"/> to generate a URL from the specified lookup information.
|
||||
/// This template object holds information of the endpoint(s) that were found and which can later be used to
|
||||
/// generate a URL using the <see cref="LinkGenerationTemplate.MakeUrl(object, LinkOptions)"/> api.
|
||||
/// The lookup information is used to find endpoints using a registered 'IEndpointFinder<TAddress>'.
|
||||
/// </summary>
|
||||
/// <typeparam name="TAddress">The address type to look up endpoints.</typeparam>
|
||||
/// <param name="address">The information used to look up endpoints for creating a template.</param>
|
||||
/// <param name="httpContext">The <see cref="HttpContext"/> associated with current request.</param>
|
||||
/// <returns>
|
||||
/// If an endpoint(s) was found successfully, then this returns a template object representing that,
|
||||
/// <c>null</c> otherwise.
|
||||
/// </returns>
|
||||
public abstract LinkGenerationTemplate GetTemplateByAddress<TAddress>(
|
||||
HttpContext httpContext,
|
||||
TAddress address);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +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.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
public class LinkGeneratorContext
|
||||
public class LinkOptions
|
||||
{
|
||||
public HttpContext HttpContext { get; set; }
|
||||
|
||||
public IEnumerable<Endpoint> Endpoints { get; set; }
|
||||
|
||||
public RouteValueDictionary ExplicitValues { get; set; }
|
||||
|
||||
public RouteValueDictionary AmbientValues { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether all generated paths URLs are lower-case.
|
||||
/// Use <see cref="LowercaseQueryStrings" /> to configure the behavior for query strings.
|
||||
|
|
@ -222,13 +222,7 @@ namespace Microsoft.AspNetCore.Routing
|
|||
}
|
||||
}
|
||||
|
||||
IEnumerable<string> IReadOnlyDictionary<string, object>.Keys
|
||||
{
|
||||
get
|
||||
{
|
||||
return Keys;
|
||||
}
|
||||
}
|
||||
IEnumerable<string> IReadOnlyDictionary<string, object>.Keys => Keys;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ICollection<object> Values
|
||||
|
|
@ -248,13 +242,7 @@ namespace Microsoft.AspNetCore.Routing
|
|||
}
|
||||
}
|
||||
|
||||
IEnumerable<object> IReadOnlyDictionary<string, object>.Values
|
||||
{
|
||||
get
|
||||
{
|
||||
return Values;
|
||||
}
|
||||
}
|
||||
IEnumerable<object> IReadOnlyDictionary<string, object>.Values => Values;
|
||||
|
||||
/// <inheritdoc />
|
||||
void ICollection<KeyValuePair<string, object>>.Add(KeyValuePair<string, object> item)
|
||||
|
|
@ -565,7 +553,7 @@ namespace Microsoft.AspNetCore.Routing
|
|||
|
||||
internal class PropertyStorage
|
||||
{
|
||||
private static readonly PropertyCache _propertyCache = new PropertyCache();
|
||||
private static readonly ConcurrentDictionary<Type, PropertyHelper[]> _propertyCache = new ConcurrentDictionary<Type, PropertyHelper[]>();
|
||||
|
||||
public readonly object Value;
|
||||
public readonly PropertyHelper[] Properties;
|
||||
|
|
@ -606,9 +594,5 @@ namespace Microsoft.AspNetCore.Routing
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class PropertyCache : ConcurrentDictionary<Type, PropertyHelper[]>
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,15 +7,16 @@ using System.Diagnostics;
|
|||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using Microsoft.AspNetCore.Routing.EndpointConstraints;
|
||||
using Microsoft.AspNetCore.Routing.Matchers;
|
||||
using Microsoft.AspNetCore.Routing.Metadata;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an <see cref="EndpointDataSource"/> whose values come from a collection of <see cref="EndpointDataSource"/> instances.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("{DebuggerDisplayString,nq}")]
|
||||
public class CompositeEndpointDataSource : EndpointDataSource
|
||||
public sealed class CompositeEndpointDataSource : EndpointDataSource
|
||||
{
|
||||
private readonly EndpointDataSource[] _dataSources;
|
||||
private readonly object _lock;
|
||||
|
|
@ -35,12 +36,20 @@ namespace Microsoft.AspNetCore.Routing
|
|||
_lock = new object();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="IChangeToken"/> used to signal invalidation of cached <see cref="Endpoint"/>
|
||||
/// instances.
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="IChangeToken"/>.</returns>
|
||||
public override IChangeToken GetChangeToken()
|
||||
{
|
||||
EnsureInitialized();
|
||||
return _consumerChangeToken;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a read-only collection of <see cref="Endpoint"/> instances.
|
||||
/// </summary>
|
||||
public override IReadOnlyList<Endpoint> Endpoints
|
||||
{
|
||||
get
|
||||
|
|
@ -123,37 +132,57 @@ namespace Microsoft.AspNetCore.Routing
|
|||
var sb = new StringBuilder();
|
||||
foreach (var endpoint in _endpoints)
|
||||
{
|
||||
if (endpoint is MatcherEndpoint matcherEndpoint)
|
||||
if (endpoint is RouteEndpoint routeEndpoint)
|
||||
{
|
||||
var template = matcherEndpoint.RoutePattern.RawText;
|
||||
var template = routeEndpoint.RoutePattern.RawText;
|
||||
template = string.IsNullOrEmpty(template) ? "\"\"" : template;
|
||||
sb.Append(template);
|
||||
var requiredValues = matcherEndpoint.RequiredValues.Select(kvp => $"{kvp.Key} = \"{kvp.Value ?? "null"}\"");
|
||||
sb.Append(", Required Values: new { ");
|
||||
sb.Append(string.Join(", ", requiredValues));
|
||||
sb.Append(", Defaults: new { ");
|
||||
sb.Append(string.Join(", ", FormatValues(routeEndpoint.RoutePattern.Defaults)));
|
||||
sb.Append(" }");
|
||||
sb.Append(", Order:");
|
||||
sb.Append(matcherEndpoint.Order);
|
||||
var routeValuesAddressMetadata = routeEndpoint.Metadata.GetMetadata<IRouteValuesAddressMetadata>();
|
||||
sb.Append(", Route Name: ");
|
||||
sb.Append(routeValuesAddressMetadata?.Name);
|
||||
if (routeValuesAddressMetadata?.RequiredValues != null)
|
||||
{
|
||||
sb.Append(", Required Values: new { ");
|
||||
sb.Append(string.Join(", ", FormatValues(routeValuesAddressMetadata.RequiredValues)));
|
||||
sb.Append(" }");
|
||||
}
|
||||
sb.Append(", Order: ");
|
||||
sb.Append(routeEndpoint.Order);
|
||||
|
||||
var httpMethodMetadata = matcherEndpoint.Metadata.GetMetadata<IHttpMethodMetadata>();
|
||||
var httpMethodMetadata = routeEndpoint.Metadata.GetMetadata<IHttpMethodMetadata>();
|
||||
if (httpMethodMetadata != null)
|
||||
{
|
||||
foreach (var httpMethod in httpMethodMetadata.HttpMethods)
|
||||
{
|
||||
sb.Append(", Http Methods: ");
|
||||
sb.Append(string.Join(", ", httpMethod));
|
||||
}
|
||||
sb.Append(", Http Methods: ");
|
||||
sb.Append(string.Join(", ", httpMethodMetadata.HttpMethods));
|
||||
}
|
||||
|
||||
sb.Append(", Display Name: ");
|
||||
sb.Append(routeEndpoint.DisplayName);
|
||||
sb.AppendLine();
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append("Non-MatcherEndpoint. DisplayName:");
|
||||
sb.Append("Non-RouteEndpoint. DisplayName:");
|
||||
sb.AppendLine(endpoint.DisplayName);
|
||||
}
|
||||
}
|
||||
return sb.ToString();
|
||||
|
||||
IEnumerable<string> FormatValues(IEnumerable<KeyValuePair<string, object>> values)
|
||||
{
|
||||
return values.Select(
|
||||
kvp =>
|
||||
{
|
||||
var value = "null";
|
||||
if (kvp.Value != null)
|
||||
{
|
||||
value = "\"" + kvp.Value.ToString() + "\"";
|
||||
}
|
||||
return kvp.Key + " = " + value;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,16 +20,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
|
|||
RouteValueDictionary values,
|
||||
RouteDirection routeDirection)
|
||||
{
|
||||
if (httpContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(httpContext));
|
||||
}
|
||||
|
||||
if (route == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(route));
|
||||
}
|
||||
|
||||
if (routeKey == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(routeKey));
|
||||
|
|
|
|||
|
|
@ -39,16 +39,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
|
|||
RouteValueDictionary values,
|
||||
RouteDirection routeDirection)
|
||||
{
|
||||
if (httpContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(httpContext));
|
||||
}
|
||||
|
||||
if (route == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(route));
|
||||
}
|
||||
|
||||
if (routeKey == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(routeKey));
|
||||
|
|
|
|||
|
|
@ -26,16 +26,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
|
|||
RouteValueDictionary values,
|
||||
RouteDirection routeDirection)
|
||||
{
|
||||
if (httpContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(httpContext));
|
||||
}
|
||||
|
||||
if (route == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(route));
|
||||
}
|
||||
|
||||
if (routeKey == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(routeKey));
|
||||
|
|
|
|||
|
|
@ -20,16 +20,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
|
|||
RouteValueDictionary values,
|
||||
RouteDirection routeDirection)
|
||||
{
|
||||
if (httpContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(httpContext));
|
||||
}
|
||||
|
||||
if (route == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(route));
|
||||
}
|
||||
|
||||
if (routeKey == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(routeKey));
|
||||
|
|
|
|||
|
|
@ -20,16 +20,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
|
|||
RouteValueDictionary values,
|
||||
RouteDirection routeDirection)
|
||||
{
|
||||
if (httpContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(httpContext));
|
||||
}
|
||||
|
||||
if (route == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(route));
|
||||
}
|
||||
|
||||
if (routeKey == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(routeKey));
|
||||
|
|
|
|||
|
|
@ -20,16 +20,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
|
|||
RouteValueDictionary values,
|
||||
RouteDirection routeDirection)
|
||||
{
|
||||
if (httpContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(httpContext));
|
||||
}
|
||||
|
||||
if (route == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(route));
|
||||
}
|
||||
|
||||
if (routeKey == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(routeKey));
|
||||
|
|
|
|||
|
|
@ -22,16 +22,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
|
|||
RouteValueDictionary values,
|
||||
RouteDirection routeDirection)
|
||||
{
|
||||
if (httpContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(httpContext));
|
||||
}
|
||||
|
||||
if (route == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(route));
|
||||
}
|
||||
|
||||
if (routeKey == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(routeKey));
|
||||
|
|
|
|||
|
|
@ -41,16 +41,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
|
|||
RouteValueDictionary values,
|
||||
RouteDirection routeDirection)
|
||||
{
|
||||
if (httpContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(httpContext));
|
||||
}
|
||||
|
||||
if (route == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(route));
|
||||
}
|
||||
|
||||
if (routeKey == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(routeKey));
|
||||
|
|
@ -64,6 +54,12 @@ namespace Microsoft.AspNetCore.Routing.Constraints
|
|||
switch (routeDirection)
|
||||
{
|
||||
case RouteDirection.IncomingRequest:
|
||||
// Only required for constraining incoming requests
|
||||
if (httpContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(httpContext));
|
||||
}
|
||||
|
||||
return AllowedMethods.Contains(httpContext.Request.Method, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
case RouteDirection.UrlGeneration:
|
||||
|
|
|
|||
|
|
@ -20,16 +20,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
|
|||
RouteValueDictionary values,
|
||||
RouteDirection routeDirection)
|
||||
{
|
||||
if (httpContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(httpContext));
|
||||
}
|
||||
|
||||
if (route == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(route));
|
||||
}
|
||||
|
||||
if (routeKey == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(routeKey));
|
||||
|
|
|
|||
|
|
@ -77,16 +77,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
|
|||
RouteValueDictionary values,
|
||||
RouteDirection routeDirection)
|
||||
{
|
||||
if (httpContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(httpContext));
|
||||
}
|
||||
|
||||
if (route == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(route));
|
||||
}
|
||||
|
||||
if (routeKey == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(routeKey));
|
||||
|
|
|
|||
|
|
@ -20,16 +20,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
|
|||
RouteValueDictionary values,
|
||||
RouteDirection routeDirection)
|
||||
{
|
||||
if (httpContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(httpContext));
|
||||
}
|
||||
|
||||
if (route == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(route));
|
||||
}
|
||||
|
||||
if (routeKey == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(routeKey));
|
||||
|
|
|
|||
|
|
@ -40,16 +40,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
|
|||
RouteValueDictionary values,
|
||||
RouteDirection routeDirection)
|
||||
{
|
||||
if (httpContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(httpContext));
|
||||
}
|
||||
|
||||
if (route == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(route));
|
||||
}
|
||||
|
||||
if (routeKey == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(routeKey));
|
||||
|
|
|
|||
|
|
@ -34,16 +34,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
|
|||
RouteValueDictionary values,
|
||||
RouteDirection routeDirection)
|
||||
{
|
||||
if (httpContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(httpContext));
|
||||
}
|
||||
|
||||
if (route == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(route));
|
||||
}
|
||||
|
||||
if (routeKey == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(routeKey));
|
||||
|
|
|
|||
|
|
@ -40,16 +40,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
|
|||
RouteValueDictionary values,
|
||||
RouteDirection routeDirection)
|
||||
{
|
||||
if (httpContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(httpContext));
|
||||
}
|
||||
|
||||
if (route == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(route));
|
||||
}
|
||||
|
||||
if (routeKey == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(routeKey));
|
||||
|
|
|
|||
|
|
@ -34,16 +34,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
|
|||
RouteValueDictionary values,
|
||||
RouteDirection routeDirection)
|
||||
{
|
||||
if (httpContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(httpContext));
|
||||
}
|
||||
|
||||
if (route == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(route));
|
||||
}
|
||||
|
||||
if (routeKey == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(routeKey));
|
||||
|
|
|
|||
|
|
@ -30,16 +30,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
|
|||
RouteValueDictionary values,
|
||||
RouteDirection routeDirection)
|
||||
{
|
||||
if (httpContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(httpContext));
|
||||
}
|
||||
|
||||
if (route == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(route));
|
||||
}
|
||||
|
||||
if (routeKey == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(routeKey));
|
||||
|
|
|
|||
|
|
@ -48,16 +48,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
|
|||
RouteValueDictionary values,
|
||||
RouteDirection routeDirection)
|
||||
{
|
||||
if (httpContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(httpContext));
|
||||
}
|
||||
|
||||
if (route == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(route));
|
||||
}
|
||||
|
||||
if (routeKey == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(routeKey));
|
||||
|
|
|
|||
|
|
@ -44,16 +44,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
|
|||
RouteValueDictionary values,
|
||||
RouteDirection routeDirection)
|
||||
{
|
||||
if (httpContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(httpContext));
|
||||
}
|
||||
|
||||
if (route == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(route));
|
||||
}
|
||||
|
||||
if (routeKey == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(routeKey));
|
||||
|
|
|
|||
|
|
@ -24,16 +24,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
|
|||
RouteValueDictionary values,
|
||||
RouteDirection routeDirection)
|
||||
{
|
||||
if (httpContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(httpContext));
|
||||
}
|
||||
|
||||
if (route == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(route));
|
||||
}
|
||||
|
||||
if (routeKey == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(routeKey));
|
||||
|
|
|
|||
|
|
@ -31,16 +31,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
|
|||
/// <inheritdoc />
|
||||
public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
|
||||
{
|
||||
if (httpContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(httpContext));
|
||||
}
|
||||
|
||||
if (route == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(route));
|
||||
}
|
||||
|
||||
if (routeKey == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(routeKey));
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
// 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 Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
/// <summary>
|
||||
/// Metadata that defines data tokens for an <see cref="Endpoint"/>. This metadata
|
||||
/// type provides data tokens value for <see cref="RouteData.DataTokens"/> associated
|
||||
/// with an endpoint.
|
||||
/// </summary>
|
||||
public sealed class DataTokensMetadata : IDataTokensMetadata
|
||||
{
|
||||
public DataTokensMetadata(IReadOnlyDictionary<string, object> dataTokens)
|
||||
{
|
||||
if (dataTokens == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(dataTokens));
|
||||
}
|
||||
|
||||
DataTokens = dataTokens;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the data tokens.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, object> DataTokens { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -3,15 +3,32 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
public class DefaultEndpointDataSource : EndpointDataSource
|
||||
/// <summary>
|
||||
/// Provides a collection of <see cref="Endpoint"/> instances.
|
||||
/// </summary>
|
||||
public sealed class DefaultEndpointDataSource : EndpointDataSource
|
||||
{
|
||||
private readonly List<Endpoint> _endpoints;
|
||||
private readonly List<Endpoint> _endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DefaultEndpointDataSource" /> class.
|
||||
/// </summary>
|
||||
/// <param name="endpoints">The <see cref="Endpoint"/> instances that the data source will return.</param>
|
||||
public DefaultEndpointDataSource(params Endpoint[] endpoints)
|
||||
: this((IEnumerable<Endpoint>) endpoints)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DefaultEndpointDataSource" /> class.
|
||||
/// </summary>
|
||||
/// <param name="endpoints">The <see cref="Endpoint"/> instances that the data source will return.</param>
|
||||
public DefaultEndpointDataSource(IEnumerable<Endpoint> endpoints)
|
||||
{
|
||||
if (endpoints == null)
|
||||
|
|
@ -23,8 +40,16 @@ namespace Microsoft.AspNetCore.Routing
|
|||
_endpoints.AddRange(endpoints);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="IChangeToken"/> used to signal invalidation of cached <see cref="Endpoint"/>
|
||||
/// instances.
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="IChangeToken"/>.</returns>
|
||||
public override IChangeToken GetChangeToken() => NullChangeToken.Singleton;
|
||||
|
||||
/// <summary>
|
||||
/// Returns a read-only collection of <see cref="Endpoint"/> instances.
|
||||
/// </summary>
|
||||
public override IReadOnlyList<Endpoint> Endpoints => _endpoints;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,7 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Routing.Internal;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
|
|
@ -18,6 +16,7 @@ namespace Microsoft.AspNetCore.Routing
|
|||
public class DefaultInlineConstraintResolver : IInlineConstraintResolver
|
||||
{
|
||||
private readonly IDictionary<string, Type> _inlineConstraintMap;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DefaultInlineConstraintResolver"/> class.
|
||||
|
|
@ -25,11 +24,23 @@ namespace Microsoft.AspNetCore.Routing
|
|||
/// <param name="routeOptions">
|
||||
/// Accessor for <see cref="RouteOptions"/> containing the constraints of interest.
|
||||
/// </param>
|
||||
[Obsolete("This constructor is obsolete. Use DefaultInlineConstraintResolver.ctor(IOptions<RouteOptions>, IServiceProvider) instead.")]
|
||||
public DefaultInlineConstraintResolver(IOptions<RouteOptions> routeOptions)
|
||||
{
|
||||
_inlineConstraintMap = routeOptions.Value.ConstraintMap;
|
||||
}
|
||||
|
||||
public DefaultInlineConstraintResolver(IOptions<RouteOptions> routeOptions, IServiceProvider serviceProvider)
|
||||
{
|
||||
if (serviceProvider == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(serviceProvider));
|
||||
}
|
||||
|
||||
_inlineConstraintMap = routeOptions.Value.ConstraintMap;
|
||||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <example>
|
||||
/// A typical constraint looks like the following
|
||||
|
|
@ -45,112 +56,7 @@ namespace Microsoft.AspNetCore.Routing
|
|||
throw new ArgumentNullException(nameof(inlineConstraint));
|
||||
}
|
||||
|
||||
string constraintKey;
|
||||
string argumentString;
|
||||
var indexOfFirstOpenParens = inlineConstraint.IndexOf('(');
|
||||
if (indexOfFirstOpenParens >= 0 && inlineConstraint.EndsWith(")", StringComparison.Ordinal))
|
||||
{
|
||||
constraintKey = inlineConstraint.Substring(0, indexOfFirstOpenParens);
|
||||
argumentString = inlineConstraint.Substring(indexOfFirstOpenParens + 1,
|
||||
inlineConstraint.Length - indexOfFirstOpenParens - 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
constraintKey = inlineConstraint;
|
||||
argumentString = null;
|
||||
}
|
||||
|
||||
Type constraintType;
|
||||
if (!_inlineConstraintMap.TryGetValue(constraintKey, out constraintType))
|
||||
{
|
||||
// Cannot resolve the constraint key
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!typeof(IRouteConstraint).GetTypeInfo().IsAssignableFrom(constraintType.GetTypeInfo()))
|
||||
{
|
||||
throw new RouteCreationException(
|
||||
Resources.FormatDefaultInlineConstraintResolver_TypeNotConstraint(
|
||||
constraintType, constraintKey, typeof(IRouteConstraint).Name));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return CreateConstraint(constraintType, argumentString);
|
||||
}
|
||||
catch (RouteCreationException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
throw new RouteCreationException(
|
||||
$"An error occurred while trying to create an instance of route constraint '{constraintType.FullName}'.",
|
||||
exception);
|
||||
}
|
||||
}
|
||||
|
||||
internal static IRouteConstraint CreateConstraint(Type constraintType, string argumentString)
|
||||
{
|
||||
// No arguments - call the default constructor
|
||||
if (argumentString == null)
|
||||
{
|
||||
return (IRouteConstraint)Activator.CreateInstance(constraintType);
|
||||
}
|
||||
|
||||
var constraintTypeInfo = constraintType.GetTypeInfo();
|
||||
ConstructorInfo activationConstructor = null;
|
||||
object[] parameters = null;
|
||||
var constructors = constraintTypeInfo.DeclaredConstructors.ToArray();
|
||||
|
||||
// If there is only one constructor and it has a single parameter, pass the argument string directly
|
||||
// This is necessary for the Regex RouteConstraint to ensure that patterns are not split on commas.
|
||||
if (constructors.Length == 1 && constructors[0].GetParameters().Length == 1)
|
||||
{
|
||||
activationConstructor = constructors[0];
|
||||
parameters = ConvertArguments(activationConstructor.GetParameters(), new string[] { argumentString });
|
||||
}
|
||||
else
|
||||
{
|
||||
var arguments = argumentString.Split(',').Select(argument => argument.Trim()).ToArray();
|
||||
|
||||
var matchingConstructors = constructors.Where(ci => ci.GetParameters().Length == arguments.Length)
|
||||
.ToArray();
|
||||
var constructorMatches = matchingConstructors.Length;
|
||||
|
||||
if (constructorMatches == 0)
|
||||
{
|
||||
throw new RouteCreationException(
|
||||
Resources.FormatDefaultInlineConstraintResolver_CouldNotFindCtor(
|
||||
constraintTypeInfo.Name, arguments.Length));
|
||||
}
|
||||
else if (constructorMatches == 1)
|
||||
{
|
||||
activationConstructor = matchingConstructors[0];
|
||||
parameters = ConvertArguments(activationConstructor.GetParameters(), arguments);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new RouteCreationException(
|
||||
Resources.FormatDefaultInlineConstraintResolver_AmbiguousCtors(
|
||||
constraintTypeInfo.Name, arguments.Length));
|
||||
}
|
||||
}
|
||||
|
||||
return (IRouteConstraint)activationConstructor.Invoke(parameters);
|
||||
}
|
||||
|
||||
private static object[] ConvertArguments(ParameterInfo[] parameterInfos, string[] arguments)
|
||||
{
|
||||
var parameters = new object[parameterInfos.Length];
|
||||
for (var i = 0; i < parameterInfos.Length; i++)
|
||||
{
|
||||
var parameter = parameterInfos[i];
|
||||
var parameterType = parameter.ParameterType;
|
||||
parameters[i] = Convert.ChangeType(arguments[i], parameterType, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
return parameters;
|
||||
return ParameterPolicyActivator.ResolveParameterPolicy<IRouteConstraint>(_inlineConstraintMap, _serviceProvider, inlineConstraint, out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
// 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.Http;
|
||||
using Microsoft.AspNetCore.Routing.Matching;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
internal class DefaultLinkGenerationTemplate : LinkGenerationTemplate
|
||||
{
|
||||
public DefaultLinkGenerationTemplate(
|
||||
DefaultLinkGenerator linkGenerator,
|
||||
IEnumerable<RouteEndpoint> endpoints,
|
||||
HttpContext httpContext,
|
||||
RouteValueDictionary explicitValues,
|
||||
RouteValueDictionary ambientValues)
|
||||
{
|
||||
LinkGenerator = linkGenerator;
|
||||
Endpoints = endpoints;
|
||||
HttpContext = httpContext;
|
||||
EarlierExplicitValues = explicitValues;
|
||||
AmbientValues = ambientValues;
|
||||
}
|
||||
|
||||
internal DefaultLinkGenerator LinkGenerator { get; }
|
||||
|
||||
internal IEnumerable<RouteEndpoint> Endpoints { get; }
|
||||
|
||||
internal HttpContext HttpContext { get; }
|
||||
|
||||
internal RouteValueDictionary EarlierExplicitValues { get; }
|
||||
|
||||
internal RouteValueDictionary AmbientValues { get; }
|
||||
|
||||
public override string MakeUrl(object values, LinkOptions options)
|
||||
{
|
||||
var currentValues = new RouteValueDictionary(values);
|
||||
var mergedValuesDictionary = new RouteValueDictionary(EarlierExplicitValues);
|
||||
|
||||
foreach (var kvp in currentValues)
|
||||
{
|
||||
mergedValuesDictionary[kvp.Key] = kvp.Value;
|
||||
}
|
||||
|
||||
foreach (var endpoint in Endpoints)
|
||||
{
|
||||
var link = LinkGenerator.MakeLink(
|
||||
HttpContext,
|
||||
endpoint,
|
||||
AmbientValues,
|
||||
mergedValuesDictionary,
|
||||
options);
|
||||
if (link != null)
|
||||
{
|
||||
return link;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,12 +2,15 @@
|
|||
// 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.Text.Encodings.Web;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Routing.Internal;
|
||||
using Microsoft.AspNetCore.Routing.Matchers;
|
||||
using Microsoft.AspNetCore.Routing.Matching;
|
||||
using Microsoft.AspNetCore.Routing.Template;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.ObjectPool;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
|
@ -17,62 +20,165 @@ namespace Microsoft.AspNetCore.Routing
|
|||
internal class DefaultLinkGenerator : LinkGenerator
|
||||
{
|
||||
private readonly static char[] UrlQueryDelimiters = new char[] { '?', '#' };
|
||||
private readonly MatchProcessorFactory _matchProcessorFactory;
|
||||
private readonly ParameterPolicyFactory _parameterPolicyFactory;
|
||||
private readonly ObjectPool<UriBuildingContext> _uriBuildingContextPool;
|
||||
private readonly RouteOptions _options;
|
||||
private readonly ILogger<DefaultLinkGenerator> _logger;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly RouteOptions _options;
|
||||
|
||||
public DefaultLinkGenerator(
|
||||
MatchProcessorFactory matchProcessorFactory,
|
||||
ParameterPolicyFactory parameterPolicyFactory,
|
||||
ObjectPool<UriBuildingContext> uriBuildingContextPool,
|
||||
IOptions<RouteOptions> routeOptions,
|
||||
ILogger<DefaultLinkGenerator> logger)
|
||||
ILogger<DefaultLinkGenerator> logger,
|
||||
IServiceProvider serviceProvider)
|
||||
{
|
||||
_matchProcessorFactory = matchProcessorFactory;
|
||||
_parameterPolicyFactory = parameterPolicyFactory;
|
||||
_uriBuildingContextPool = uriBuildingContextPool;
|
||||
_options = routeOptions.Value;
|
||||
_logger = logger;
|
||||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public override string GetLink(LinkGeneratorContext context)
|
||||
public override bool TryGetLink(
|
||||
HttpContext httpContext,
|
||||
string routeName,
|
||||
object values,
|
||||
LinkOptions options,
|
||||
out string link)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (TryGetLink(context, out var link))
|
||||
{
|
||||
return link;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Could not find a matching endpoint to generate a link.");
|
||||
return TryGetLinkByRouteValues(
|
||||
httpContext,
|
||||
routeName,
|
||||
values,
|
||||
options,
|
||||
out link);
|
||||
}
|
||||
|
||||
public override bool TryGetLink(LinkGeneratorContext context, out string link)
|
||||
public override bool TryGetLinkByAddress<TAddress>(
|
||||
HttpContext httpContext,
|
||||
TAddress address,
|
||||
object values,
|
||||
LinkOptions options,
|
||||
out string link)
|
||||
{
|
||||
if (context == null)
|
||||
return TryGetLinkByAddressInternal(
|
||||
httpContext,
|
||||
address,
|
||||
explicitValues: values,
|
||||
ambientValues: GetAmbientValues(httpContext),
|
||||
options,
|
||||
out link);
|
||||
}
|
||||
|
||||
public override LinkGenerationTemplate GetTemplate(HttpContext httpContext, string routeName, object values)
|
||||
{
|
||||
var ambientValues = GetAmbientValues(httpContext);
|
||||
var explicitValues = new RouteValueDictionary(values);
|
||||
|
||||
return GetTemplateInternal(
|
||||
httpContext,
|
||||
new RouteValuesAddress
|
||||
{
|
||||
RouteName = routeName,
|
||||
ExplicitValues = explicitValues,
|
||||
AmbientValues = ambientValues
|
||||
},
|
||||
ambientValues,
|
||||
explicitValues,
|
||||
values);
|
||||
}
|
||||
|
||||
public override LinkGenerationTemplate GetTemplateByAddress<TAddress>(
|
||||
HttpContext httpContext,
|
||||
TAddress address)
|
||||
{
|
||||
return GetTemplateInternal(httpContext, address, values: null);
|
||||
}
|
||||
|
||||
internal string MakeLink(
|
||||
HttpContext httpContext,
|
||||
RouteEndpoint endpoint,
|
||||
RouteValueDictionary ambientValues,
|
||||
RouteValueDictionary explicitValues,
|
||||
LinkOptions options)
|
||||
{
|
||||
var templateBinder = new TemplateBinder(
|
||||
UrlEncoder.Default,
|
||||
_uriBuildingContextPool,
|
||||
endpoint.RoutePattern,
|
||||
new RouteValueDictionary(endpoint.RoutePattern.Defaults));
|
||||
|
||||
var routeValuesAddressMetadata = endpoint.Metadata.GetMetadata<IRouteValuesAddressMetadata>();
|
||||
var templateValuesResult = templateBinder.GetValues(
|
||||
ambientValues: ambientValues,
|
||||
explicitValues: explicitValues,
|
||||
requiredKeys: routeValuesAddressMetadata?.RequiredValues.Keys);
|
||||
if (templateValuesResult == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
// We're missing one of the required values for this route.
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!MatchesConstraints(httpContext, endpoint, templateValuesResult.CombinedValues))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var url = templateBinder.BindValues(templateValuesResult.AcceptedValues);
|
||||
return Normalize(url, options);
|
||||
}
|
||||
|
||||
private bool TryGetLinkByRouteValues(
|
||||
HttpContext httpContext,
|
||||
string routeName,
|
||||
object values,
|
||||
LinkOptions options,
|
||||
out string link)
|
||||
{
|
||||
var ambientValues = GetAmbientValues(httpContext);
|
||||
|
||||
var address = new RouteValuesAddress
|
||||
{
|
||||
RouteName = routeName,
|
||||
ExplicitValues = new RouteValueDictionary(values),
|
||||
AmbientValues = ambientValues
|
||||
};
|
||||
|
||||
return TryGetLinkByAddressInternal(
|
||||
httpContext,
|
||||
address,
|
||||
explicitValues: values,
|
||||
ambientValues: ambientValues,
|
||||
options,
|
||||
out link);
|
||||
}
|
||||
|
||||
private bool TryGetLinkByAddressInternal<TAddress>(
|
||||
HttpContext httpContext,
|
||||
TAddress address,
|
||||
object explicitValues,
|
||||
RouteValueDictionary ambientValues,
|
||||
LinkOptions options,
|
||||
out string link)
|
||||
{
|
||||
link = null;
|
||||
|
||||
if (context.Endpoints == null)
|
||||
var endpoints = FindEndpoints(address);
|
||||
if (endpoints == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var matcherEndpoints = context.Endpoints.OfType<MatcherEndpoint>();
|
||||
if (!matcherEndpoints.Any())
|
||||
foreach (var endpoint in endpoints)
|
||||
{
|
||||
//todo:log here
|
||||
return false;
|
||||
}
|
||||
link = MakeLink(
|
||||
httpContext,
|
||||
endpoint,
|
||||
ambientValues,
|
||||
new RouteValueDictionary(explicitValues),
|
||||
options);
|
||||
|
||||
foreach (var endpoint in matcherEndpoints)
|
||||
{
|
||||
link = GetLink(endpoint, context);
|
||||
if (link != null)
|
||||
{
|
||||
return true;
|
||||
|
|
@ -82,35 +188,52 @@ namespace Microsoft.AspNetCore.Routing
|
|||
return false;
|
||||
}
|
||||
|
||||
private string GetLink(MatcherEndpoint endpoint, LinkGeneratorContext context)
|
||||
private LinkGenerationTemplate GetTemplateInternal<TAddress>(
|
||||
HttpContext httpContext,
|
||||
TAddress address,
|
||||
object values)
|
||||
{
|
||||
var templateBinder = new TemplateBinder(
|
||||
UrlEncoder.Default,
|
||||
_uriBuildingContextPool,
|
||||
new RouteTemplate(endpoint.RoutePattern),
|
||||
new RouteValueDictionary(endpoint.RoutePattern.Defaults));
|
||||
|
||||
var templateValuesResult = templateBinder.GetValues(
|
||||
ambientValues: context.AmbientValues,
|
||||
values: context.ExplicitValues);
|
||||
if (templateValuesResult == null)
|
||||
{
|
||||
// We're missing one of the required values for this route.
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!MatchesConstraints(context.HttpContext, endpoint, templateValuesResult.CombinedValues))
|
||||
var endpoints = FindEndpoints(address);
|
||||
if (endpoints == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var url = templateBinder.BindValues(templateValuesResult.AcceptedValues);
|
||||
return Normalize(context, url);
|
||||
var ambientValues = GetAmbientValues(httpContext);
|
||||
var explicitValues = new RouteValueDictionary(values);
|
||||
|
||||
return new DefaultLinkGenerationTemplate(
|
||||
this,
|
||||
endpoints,
|
||||
httpContext,
|
||||
explicitValues,
|
||||
ambientValues);
|
||||
}
|
||||
|
||||
private LinkGenerationTemplate GetTemplateInternal<TAddress>(
|
||||
HttpContext httpContext,
|
||||
TAddress address,
|
||||
RouteValueDictionary ambientValues,
|
||||
RouteValueDictionary explicitValues,
|
||||
object values)
|
||||
{
|
||||
var endpoints = FindEndpoints(address);
|
||||
if (endpoints == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new DefaultLinkGenerationTemplate(
|
||||
this,
|
||||
endpoints,
|
||||
httpContext,
|
||||
explicitValues,
|
||||
ambientValues);
|
||||
}
|
||||
|
||||
private bool MatchesConstraints(
|
||||
HttpContext httpContext,
|
||||
MatcherEndpoint endpoint,
|
||||
RouteEndpoint endpoint,
|
||||
RouteValueDictionary routeValues)
|
||||
{
|
||||
if (routeValues == null)
|
||||
|
|
@ -118,15 +241,16 @@ namespace Microsoft.AspNetCore.Routing
|
|||
throw new ArgumentNullException(nameof(routeValues));
|
||||
}
|
||||
|
||||
foreach (var kvp in endpoint.RoutePattern.Constraints)
|
||||
foreach (var kvp in endpoint.RoutePattern.ParameterPolicies)
|
||||
{
|
||||
var parameter = endpoint.RoutePattern.GetParameter(kvp.Key); // may be null, that's ok
|
||||
var constraintReferences = kvp.Value;
|
||||
for (var i = 0; i < constraintReferences.Count; i++)
|
||||
{
|
||||
var constraintReference = constraintReferences[i];
|
||||
var matchProcessor = _matchProcessorFactory.Create(parameter, constraintReference);
|
||||
if (!matchProcessor.ProcessOutbound(httpContext, routeValues))
|
||||
var parameterPolicy = _parameterPolicyFactory.Create(parameter, constraintReference);
|
||||
if (parameterPolicy is IRouteConstraint routeConstraint
|
||||
&& !routeConstraint.Match(httpContext, NullRouter.Instance, kvp.Key, routeValues, RouteDirection.UrlGeneration))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
@ -136,13 +260,11 @@ namespace Microsoft.AspNetCore.Routing
|
|||
return true;
|
||||
}
|
||||
|
||||
private string Normalize(LinkGeneratorContext context, string url)
|
||||
private string Normalize(string url, LinkOptions options)
|
||||
{
|
||||
var lowercaseUrls = context.LowercaseUrls.HasValue ? context.LowercaseUrls.Value : _options.LowercaseUrls;
|
||||
var lowercaseQueryStrings = context.LowercaseQueryStrings.HasValue ?
|
||||
context.LowercaseQueryStrings.Value : _options.LowercaseQueryStrings;
|
||||
var appendTrailingSlash = context.AppendTrailingSlash.HasValue ?
|
||||
context.AppendTrailingSlash.Value : _options.AppendTrailingSlash;
|
||||
var lowercaseUrls = options?.LowercaseUrls ?? _options.LowercaseUrls;
|
||||
var lowercaseQueryStrings = options?.LowercaseQueryStrings ?? _options.LowercaseQueryStrings;
|
||||
var appendTrailingSlash = options?.AppendTrailingSlash ?? _options.AppendTrailingSlash;
|
||||
|
||||
if (!string.IsNullOrEmpty(url) && (lowercaseUrls || appendTrailingSlash))
|
||||
{
|
||||
|
|
@ -177,5 +299,36 @@ namespace Microsoft.AspNetCore.Routing
|
|||
|
||||
return url;
|
||||
}
|
||||
|
||||
private RouteValueDictionary GetAmbientValues(HttpContext httpContext)
|
||||
{
|
||||
if (httpContext != null)
|
||||
{
|
||||
var feature = httpContext.Features.Get<IRouteValuesFeature>();
|
||||
if (feature != null)
|
||||
{
|
||||
return feature.RouteValues;
|
||||
}
|
||||
}
|
||||
return new RouteValueDictionary();
|
||||
}
|
||||
|
||||
private IEnumerable<RouteEndpoint> FindEndpoints<TAddress>(TAddress address)
|
||||
{
|
||||
var finder = _serviceProvider.GetRequiredService<IEndpointFinder<TAddress>>();
|
||||
var endpoints = finder.FindEndpoints(address);
|
||||
if (endpoints == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var routeEndpoints = endpoints.OfType<RouteEndpoint>();
|
||||
if (!routeEndpoints.Any())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return routeEndpoints;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,76 @@
|
|||
// 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.Constraints;
|
||||
using Microsoft.AspNetCore.Routing.Internal;
|
||||
using Microsoft.AspNetCore.Routing.Patterns;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
internal class DefaultParameterPolicyFactory : ParameterPolicyFactory
|
||||
{
|
||||
private readonly RouteOptions _options;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
public DefaultParameterPolicyFactory(
|
||||
IOptions<RouteOptions> options,
|
||||
IServiceProvider serviceProvider)
|
||||
{
|
||||
_options = options.Value;
|
||||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public override IParameterPolicy Create(RoutePatternParameterPart parameter, IParameterPolicy parameterPolicy)
|
||||
{
|
||||
if (parameterPolicy == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(parameterPolicy));
|
||||
}
|
||||
|
||||
if (parameterPolicy is IRouteConstraint routeConstraint)
|
||||
{
|
||||
return InitializeRouteConstraint(parameter?.IsOptional ?? false, routeConstraint);
|
||||
}
|
||||
|
||||
return parameterPolicy;
|
||||
}
|
||||
|
||||
public override IParameterPolicy Create(RoutePatternParameterPart parameter, string inlineText)
|
||||
{
|
||||
if (inlineText == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(inlineText));
|
||||
}
|
||||
|
||||
var parameterPolicy = ParameterPolicyActivator.ResolveParameterPolicy<IParameterPolicy>(_options.ConstraintMap, _serviceProvider, inlineText, out var parameterPolicyKey);
|
||||
if (parameterPolicy == null)
|
||||
{
|
||||
throw new InvalidOperationException(Resources.FormatRoutePattern_ConstraintReferenceNotFound(
|
||||
parameterPolicyKey,
|
||||
typeof(RouteOptions),
|
||||
nameof(RouteOptions.ConstraintMap)));
|
||||
}
|
||||
|
||||
if (parameterPolicy is IRouteConstraint constraint)
|
||||
{
|
||||
return InitializeRouteConstraint(parameter?.IsOptional ?? false, constraint);
|
||||
}
|
||||
|
||||
return parameterPolicy;
|
||||
}
|
||||
|
||||
private IParameterPolicy InitializeRouteConstraint(
|
||||
bool optional,
|
||||
IRouteConstraint routeConstraint)
|
||||
{
|
||||
if (optional)
|
||||
{
|
||||
routeConstraint = new OptionalRouteConstraint(routeConstraint);
|
||||
}
|
||||
|
||||
return routeConstraint;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,11 +3,8 @@
|
|||
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.Routing.Constraints;
|
||||
using Microsoft.AspNetCore.Routing.EndpointConstraints;
|
||||
using Microsoft.AspNetCore.Routing.EndpointFinders;
|
||||
using Microsoft.AspNetCore.Routing.Internal;
|
||||
using Microsoft.AspNetCore.Routing.Matchers;
|
||||
using Microsoft.AspNetCore.Routing.Matching;
|
||||
using Microsoft.AspNetCore.Routing.Tree;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
|
@ -65,26 +62,21 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
//
|
||||
// Default matcher implementation
|
||||
//
|
||||
services.TryAddSingleton<MatchProcessorFactory, DefaultMatchProcessorFactory>();
|
||||
services.TryAddSingleton<ParameterPolicyFactory, DefaultParameterPolicyFactory>();
|
||||
services.TryAddSingleton<MatcherFactory, DfaMatcherFactory>();
|
||||
services.TryAddTransient<DfaMatcherBuilder>();
|
||||
services.TryAddSingleton<DfaGraphWriter>();
|
||||
|
||||
// Link generation related services
|
||||
services.TryAddSingleton<IEndpointFinder<string>, NameBasedEndpointFinder>();
|
||||
services.TryAddSingleton<IEndpointFinder<RouteValuesBasedEndpointFinderContext>, RouteValuesBasedEndpointFinder>();
|
||||
services.TryAddSingleton<IEndpointFinder<RouteValuesAddress>, RouteValuesBasedEndpointFinder>();
|
||||
services.TryAddSingleton<LinkGenerator, DefaultLinkGenerator>();
|
||||
|
||||
|
||||
//
|
||||
// Endpoint Selection
|
||||
//
|
||||
services.TryAddSingleton<EndpointSelector, EndpointConstraintEndpointSelector>();
|
||||
services.TryAddSingleton<EndpointConstraintCache>();
|
||||
services.TryAddSingleton<EndpointSelector, DefaultEndpointSelector>();
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, HttpMethodMatcherPolicy>());
|
||||
|
||||
// Will be cached by the EndpointSelector
|
||||
services.TryAddEnumerable(
|
||||
ServiceDescriptor.Transient<IEndpointConstraintProvider, DefaultEndpointConstraintProvider>());
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,208 +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 System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.EndpointConstraints
|
||||
{
|
||||
internal class EndpointConstraintCache
|
||||
{
|
||||
private readonly CompositeEndpointDataSource _dataSource;
|
||||
private readonly IEndpointConstraintProvider[] _endpointConstraintProviders;
|
||||
|
||||
private volatile InnerCache _currentCache;
|
||||
|
||||
public EndpointConstraintCache(
|
||||
CompositeEndpointDataSource dataSource,
|
||||
IEnumerable<IEndpointConstraintProvider> endpointConstraintProviders)
|
||||
{
|
||||
_dataSource = dataSource;
|
||||
_endpointConstraintProviders = endpointConstraintProviders.OrderBy(item => item.Order).ToArray();
|
||||
}
|
||||
|
||||
private InnerCache CurrentCache
|
||||
{
|
||||
get
|
||||
{
|
||||
var current = _currentCache;
|
||||
var endpointDescriptors = _dataSource.Endpoints;
|
||||
|
||||
if (current == null)
|
||||
{
|
||||
current = new InnerCache();
|
||||
_currentCache = current;
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<IEndpointConstraint> GetEndpointConstraints(HttpContext httpContext, Endpoint endpoint)
|
||||
{
|
||||
var cache = CurrentCache;
|
||||
|
||||
if (cache.Entries.TryGetValue(endpoint, out var entry))
|
||||
{
|
||||
return GetEndpointConstraintsFromEntry(entry, httpContext, endpoint);
|
||||
}
|
||||
|
||||
List<EndpointConstraintItem> items = null;
|
||||
|
||||
if (endpoint.Metadata != null && endpoint.Metadata.Count > 0)
|
||||
{
|
||||
items = endpoint.Metadata
|
||||
.OfType<IEndpointConstraintMetadata>()
|
||||
.Select(m => new EndpointConstraintItem(m))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
IReadOnlyList<IEndpointConstraint> endpointConstraints = null;
|
||||
|
||||
if (items != null && items.Count > 0)
|
||||
{
|
||||
ExecuteProviders(httpContext, endpoint, items);
|
||||
|
||||
endpointConstraints = ExtractEndpointConstraints(items);
|
||||
|
||||
var allEndpointConstraintsCached = true;
|
||||
for (var i = 0; i < items.Count; i++)
|
||||
{
|
||||
var item = items[i];
|
||||
if (!item.IsReusable)
|
||||
{
|
||||
item.Constraint = null;
|
||||
allEndpointConstraintsCached = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (allEndpointConstraintsCached)
|
||||
{
|
||||
entry = new CacheEntry(endpointConstraints);
|
||||
}
|
||||
else
|
||||
{
|
||||
entry = new CacheEntry(items);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// No constraints
|
||||
entry = new CacheEntry();
|
||||
}
|
||||
|
||||
cache.Entries.TryAdd(endpoint, entry);
|
||||
return endpointConstraints;
|
||||
}
|
||||
|
||||
private IReadOnlyList<IEndpointConstraint> GetEndpointConstraintsFromEntry(CacheEntry entry, HttpContext httpContext, Endpoint endpoint)
|
||||
{
|
||||
if (entry.EndpointConstraints != null)
|
||||
{
|
||||
return entry.EndpointConstraints;
|
||||
}
|
||||
|
||||
if (entry.Items == null)
|
||||
{
|
||||
// Endpoint has no constraints
|
||||
return null;
|
||||
}
|
||||
|
||||
var items = new List<EndpointConstraintItem>(entry.Items.Count);
|
||||
for (var i = 0; i < entry.Items.Count; i++)
|
||||
{
|
||||
var item = entry.Items[i];
|
||||
if (item.IsReusable)
|
||||
{
|
||||
items.Add(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
items.Add(new EndpointConstraintItem(item.Metadata));
|
||||
}
|
||||
}
|
||||
|
||||
ExecuteProviders(httpContext, endpoint, items);
|
||||
|
||||
return ExtractEndpointConstraints(items);
|
||||
}
|
||||
|
||||
private void ExecuteProviders(HttpContext httpContext, Endpoint endpoint, List<EndpointConstraintItem> items)
|
||||
{
|
||||
var context = new EndpointConstraintProviderContext(httpContext, endpoint, items);
|
||||
|
||||
for (var i = 0; i < _endpointConstraintProviders.Length; i++)
|
||||
{
|
||||
_endpointConstraintProviders[i].OnProvidersExecuting(context);
|
||||
}
|
||||
|
||||
for (var i = _endpointConstraintProviders.Length - 1; i >= 0; i--)
|
||||
{
|
||||
_endpointConstraintProviders[i].OnProvidersExecuted(context);
|
||||
}
|
||||
}
|
||||
|
||||
private IReadOnlyList<IEndpointConstraint> ExtractEndpointConstraints(List<EndpointConstraintItem> items)
|
||||
{
|
||||
var count = 0;
|
||||
for (var i = 0; i < items.Count; i++)
|
||||
{
|
||||
if (items[i].Constraint != null)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
if (count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var endpointConstraints = new IEndpointConstraint[count];
|
||||
var endpointConstraintIndex = 0;
|
||||
for (int i = 0; i < items.Count; i++)
|
||||
{
|
||||
var endpointConstraint = items[i].Constraint;
|
||||
if (endpointConstraint != null)
|
||||
{
|
||||
endpointConstraints[endpointConstraintIndex++] = endpointConstraint;
|
||||
}
|
||||
}
|
||||
|
||||
return endpointConstraints;
|
||||
}
|
||||
|
||||
private class InnerCache
|
||||
{
|
||||
public InnerCache()
|
||||
{
|
||||
}
|
||||
|
||||
public ConcurrentDictionary<Endpoint, CacheEntry> Entries { get; } =
|
||||
new ConcurrentDictionary<Endpoint, CacheEntry>();
|
||||
}
|
||||
|
||||
private struct CacheEntry
|
||||
{
|
||||
public CacheEntry(IReadOnlyList<IEndpointConstraint> endpointConstraints)
|
||||
{
|
||||
EndpointConstraints = endpointConstraints;
|
||||
Items = null;
|
||||
}
|
||||
|
||||
public CacheEntry(List<EndpointConstraintItem> items)
|
||||
{
|
||||
Items = items;
|
||||
EndpointConstraints = null;
|
||||
}
|
||||
|
||||
public IReadOnlyList<IEndpointConstraint> EndpointConstraints { get; }
|
||||
|
||||
public List<EndpointConstraintItem> Items { get; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,265 +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.Http;
|
||||
using Microsoft.AspNetCore.Routing.Matchers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.EndpointConstraints
|
||||
{
|
||||
internal class EndpointConstraintEndpointSelector : EndpointSelector
|
||||
{
|
||||
private static readonly IReadOnlyList<Endpoint> EmptyEndpoints = Array.Empty<Endpoint>();
|
||||
|
||||
private readonly CompositeEndpointDataSource _dataSource;
|
||||
private readonly EndpointConstraintCache _endpointConstraintCache;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public EndpointConstraintEndpointSelector(
|
||||
CompositeEndpointDataSource dataSource,
|
||||
EndpointConstraintCache endpointConstraintCache,
|
||||
ILoggerFactory loggerFactory)
|
||||
{
|
||||
_dataSource = dataSource;
|
||||
_logger = loggerFactory.CreateLogger<EndpointSelector>();
|
||||
_endpointConstraintCache = endpointConstraintCache;
|
||||
}
|
||||
|
||||
public override Task SelectAsync(
|
||||
HttpContext httpContext,
|
||||
IEndpointFeature feature,
|
||||
CandidateSet candidates)
|
||||
{
|
||||
if (httpContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(httpContext));
|
||||
}
|
||||
|
||||
if (feature == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(feature));
|
||||
}
|
||||
|
||||
if (candidates == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(candidates));
|
||||
}
|
||||
|
||||
var finalMatches = EvaluateEndpointConstraints(httpContext, candidates);
|
||||
|
||||
if (finalMatches == null || finalMatches.Count == 0)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
else if (finalMatches.Count == 1)
|
||||
{
|
||||
var endpoint = finalMatches[0].Endpoint;
|
||||
var values = finalMatches[0].Values;
|
||||
|
||||
feature.Endpoint = endpoint;
|
||||
feature.Invoker = (endpoint as MatcherEndpoint)?.Invoker;
|
||||
feature.Values = values;
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
else
|
||||
{
|
||||
var endpointNames = string.Join(
|
||||
Environment.NewLine,
|
||||
finalMatches.Select(a => a.Endpoint.DisplayName));
|
||||
|
||||
Log.MatchAmbiguous(_logger, httpContext, finalMatches);
|
||||
|
||||
var message = Resources.FormatAmbiguousEndpoints(
|
||||
Environment.NewLine,
|
||||
string.Join(Environment.NewLine, endpointNames));
|
||||
|
||||
throw new AmbiguousMatchException(message);
|
||||
}
|
||||
}
|
||||
|
||||
private IReadOnlyList<EndpointSelectorCandidate> EvaluateEndpointConstraints(
|
||||
HttpContext context,
|
||||
CandidateSet candidateSet)
|
||||
{
|
||||
var candidates = new List<EndpointSelectorCandidate>();
|
||||
|
||||
// Perf: Avoid allocations
|
||||
for (var i = 0; i < candidateSet.Count; i++)
|
||||
{
|
||||
ref var candidate = ref candidateSet[i];
|
||||
if (candidate.IsValidCandidate)
|
||||
{
|
||||
var endpoint = candidate.Endpoint;
|
||||
var constraints = _endpointConstraintCache.GetEndpointConstraints(context, endpoint);
|
||||
candidates.Add(new EndpointSelectorCandidate(
|
||||
endpoint,
|
||||
candidate.Score,
|
||||
candidate.Values,
|
||||
constraints));
|
||||
}
|
||||
}
|
||||
|
||||
var matches = EvaluateEndpointConstraintsCore(context, candidates, startingOrder: null);
|
||||
|
||||
List<EndpointSelectorCandidate> results = null;
|
||||
if (matches != null)
|
||||
{
|
||||
results = new List<EndpointSelectorCandidate>(matches.Count);
|
||||
|
||||
// We need to disambiguate based on 'score' - take the first value of 'score'
|
||||
// and then we only copy matches while they have the same score. This accounts
|
||||
// for a difference in behavior between new routing and old.
|
||||
switch (matches.Count)
|
||||
{
|
||||
case 0:
|
||||
break;
|
||||
|
||||
case 1:
|
||||
results.Add(matches[0]);
|
||||
break;
|
||||
|
||||
default:
|
||||
var score = matches[0].Score;
|
||||
for (var i = 0; i < matches.Count; i++)
|
||||
{
|
||||
if (matches[i].Score != score)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
results.Add(matches[i]);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private IReadOnlyList<EndpointSelectorCandidate> EvaluateEndpointConstraintsCore(
|
||||
HttpContext context,
|
||||
IReadOnlyList<EndpointSelectorCandidate> candidates,
|
||||
int? startingOrder)
|
||||
{
|
||||
// Find the next group of constraints to process. This will be the lowest value of
|
||||
// order that is higher than startingOrder.
|
||||
int? order = null;
|
||||
|
||||
// Perf: Avoid allocations
|
||||
for (var i = 0; i < candidates.Count; i++)
|
||||
{
|
||||
var candidate = candidates[i];
|
||||
if (candidate.Constraints != null)
|
||||
{
|
||||
for (var j = 0; j < candidate.Constraints.Count; j++)
|
||||
{
|
||||
var constraint = candidate.Constraints[j];
|
||||
if ((startingOrder == null || constraint.Order > startingOrder) &&
|
||||
(order == null || constraint.Order < order))
|
||||
{
|
||||
order = constraint.Order;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we don't find a next then there's nothing left to do.
|
||||
if (order == null)
|
||||
{
|
||||
return candidates;
|
||||
}
|
||||
|
||||
// Since we have a constraint to process, bisect the set of endpoints into those with and without a
|
||||
// constraint for the current order.
|
||||
var endpointsWithConstraint = new List<EndpointSelectorCandidate>();
|
||||
var endpointsWithoutConstraint = new List<EndpointSelectorCandidate>();
|
||||
|
||||
var constraintContext = new EndpointConstraintContext();
|
||||
constraintContext.Candidates = candidates;
|
||||
constraintContext.HttpContext = context;
|
||||
|
||||
// Perf: Avoid allocations
|
||||
for (var i = 0; i < candidates.Count; i++)
|
||||
{
|
||||
var candidate = candidates[i];
|
||||
var isMatch = true;
|
||||
var foundMatchingConstraint = false;
|
||||
|
||||
if (candidate.Constraints != null)
|
||||
{
|
||||
constraintContext.CurrentCandidate = candidate;
|
||||
for (var j = 0; j < candidate.Constraints.Count; j++)
|
||||
{
|
||||
var constraint = candidate.Constraints[j];
|
||||
if (constraint.Order == order)
|
||||
{
|
||||
foundMatchingConstraint = true;
|
||||
|
||||
if (!constraint.Accept(constraintContext))
|
||||
{
|
||||
isMatch = false;
|
||||
//_logger.ConstraintMismatch(
|
||||
// candidate.Endpoint.DisplayName,
|
||||
// candidate.Endpoint.Id,
|
||||
// constraint);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isMatch && foundMatchingConstraint)
|
||||
{
|
||||
endpointsWithConstraint.Add(candidate);
|
||||
}
|
||||
else if (isMatch)
|
||||
{
|
||||
endpointsWithoutConstraint.Add(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
// If we have matches with constraints, those are better so try to keep processing those
|
||||
if (endpointsWithConstraint.Count > 0)
|
||||
{
|
||||
var matches = EvaluateEndpointConstraintsCore(context, endpointsWithConstraint, order);
|
||||
if (matches?.Count > 0)
|
||||
{
|
||||
return matches;
|
||||
}
|
||||
}
|
||||
|
||||
// If the set of matches with constraints can't work, then process the set without constraints.
|
||||
if (endpointsWithoutConstraint.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
return EvaluateEndpointConstraintsCore(context, endpointsWithoutConstraint, order);
|
||||
}
|
||||
}
|
||||
|
||||
private static class Log
|
||||
{
|
||||
private static readonly Action<ILogger, PathString, IEnumerable<string>, Exception> _matchAmbiguous = LoggerMessage.Define<PathString, IEnumerable<string>>(
|
||||
LogLevel.Error,
|
||||
new EventId(1, "MatchAmbiguous"),
|
||||
"Request matched multiple endpoints for request path '{Path}'. Matching endpoints: {AmbiguousEndpoints}");
|
||||
|
||||
public static void MatchAmbiguous(ILogger logger, HttpContext httpContext, IEnumerable<EndpointSelectorCandidate> endpoints)
|
||||
{
|
||||
if (logger.IsEnabled(LogLevel.Error))
|
||||
{
|
||||
_matchAmbiguous(logger, httpContext.Request.Path, endpoints.Select(e => e.Endpoint.DisplayName), null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,75 +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.Collections.ObjectModel;
|
||||
using Microsoft.AspNetCore.Routing.Metadata;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.EndpointConstraints
|
||||
{
|
||||
public class HttpMethodEndpointConstraint : IEndpointConstraint, IHttpMethodMetadata
|
||||
{
|
||||
public static readonly int HttpMethodConstraintOrder = 100;
|
||||
|
||||
private readonly IReadOnlyList<string> _httpMethods;
|
||||
|
||||
// Empty collection means any method will be accepted.
|
||||
public HttpMethodEndpointConstraint(IEnumerable<string> httpMethods)
|
||||
{
|
||||
if (httpMethods == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(httpMethods));
|
||||
}
|
||||
|
||||
var methods = new List<string>();
|
||||
|
||||
foreach (var method in httpMethods)
|
||||
{
|
||||
if (string.IsNullOrEmpty(method))
|
||||
{
|
||||
throw new ArgumentException("httpMethod cannot be null or empty");
|
||||
}
|
||||
|
||||
methods.Add(method);
|
||||
}
|
||||
|
||||
_httpMethods = new ReadOnlyCollection<string>(methods);
|
||||
}
|
||||
|
||||
public IEnumerable<string> HttpMethods => _httpMethods;
|
||||
|
||||
public int Order => HttpMethodConstraintOrder;
|
||||
|
||||
IReadOnlyList<string> IHttpMethodMetadata.HttpMethods => _httpMethods;
|
||||
|
||||
bool IHttpMethodMetadata.AcceptCorsPreflight => false;
|
||||
|
||||
public virtual bool Accept(EndpointConstraintContext context)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (_httpMethods.Count == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var request = context.HttpContext.Request;
|
||||
var method = request.Method;
|
||||
|
||||
for (var i = 0; i < _httpMethods.Count; i++)
|
||||
{
|
||||
var supportedMethod = _httpMethods[i];
|
||||
if (string.Equals(supportedMethod, method, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,190 +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 Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.EndpointConstraints
|
||||
{
|
||||
public class EndpointConstraintContext
|
||||
{
|
||||
public IReadOnlyList<EndpointSelectorCandidate> Candidates { get; set; }
|
||||
|
||||
public EndpointSelectorCandidate CurrentCandidate { get; set; }
|
||||
|
||||
public HttpContext HttpContext { get; set; }
|
||||
}
|
||||
|
||||
public interface IEndpointConstraint : IEndpointConstraintMetadata
|
||||
{
|
||||
int Order { get; }
|
||||
|
||||
bool Accept(EndpointConstraintContext context);
|
||||
}
|
||||
|
||||
public interface IEndpointConstraintMetadata
|
||||
{
|
||||
}
|
||||
|
||||
public readonly struct EndpointSelectorCandidate
|
||||
{
|
||||
public EndpointSelectorCandidate(
|
||||
Endpoint endpoint,
|
||||
int score,
|
||||
RouteValueDictionary values,
|
||||
IReadOnlyList<IEndpointConstraint> constraints)
|
||||
{
|
||||
if (endpoint == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(endpoint));
|
||||
}
|
||||
|
||||
Endpoint = endpoint;
|
||||
Score = score;
|
||||
Values = values;
|
||||
Constraints = constraints;
|
||||
}
|
||||
|
||||
// Temporarily added to not break MVC build
|
||||
public EndpointSelectorCandidate(
|
||||
Endpoint endpoint,
|
||||
IReadOnlyList<IEndpointConstraint> constraints)
|
||||
{
|
||||
if (endpoint == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(endpoint));
|
||||
}
|
||||
|
||||
Endpoint = endpoint;
|
||||
Score = 0;
|
||||
Values = null;
|
||||
Constraints = constraints;
|
||||
}
|
||||
|
||||
public Endpoint Endpoint { get; }
|
||||
|
||||
public int Score { get; }
|
||||
|
||||
public RouteValueDictionary Values { get; }
|
||||
|
||||
public IReadOnlyList<IEndpointConstraint> Constraints { get; }
|
||||
}
|
||||
|
||||
public class EndpointConstraintItem
|
||||
{
|
||||
public EndpointConstraintItem(IEndpointConstraintMetadata metadata)
|
||||
{
|
||||
if (metadata == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(metadata));
|
||||
}
|
||||
|
||||
Metadata = metadata;
|
||||
}
|
||||
|
||||
public IEndpointConstraint Constraint { get; set; }
|
||||
|
||||
public IEndpointConstraintMetadata Metadata { get; }
|
||||
|
||||
public bool IsReusable { get; set; }
|
||||
}
|
||||
|
||||
public interface IEndpointConstraintProvider
|
||||
{
|
||||
int Order { get; }
|
||||
|
||||
void OnProvidersExecuting(EndpointConstraintProviderContext context);
|
||||
|
||||
void OnProvidersExecuted(EndpointConstraintProviderContext context);
|
||||
}
|
||||
|
||||
public class EndpointConstraintProviderContext
|
||||
{
|
||||
public EndpointConstraintProviderContext(
|
||||
HttpContext context,
|
||||
Endpoint endpoint,
|
||||
IList<EndpointConstraintItem> items)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (endpoint == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(endpoint));
|
||||
}
|
||||
|
||||
if (items == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(items));
|
||||
}
|
||||
|
||||
HttpContext = context;
|
||||
Endpoint = endpoint;
|
||||
Results = items;
|
||||
}
|
||||
|
||||
public HttpContext HttpContext { get; }
|
||||
|
||||
public Endpoint Endpoint { get; }
|
||||
|
||||
public IList<EndpointConstraintItem> Results { get; }
|
||||
}
|
||||
|
||||
public class DefaultEndpointConstraintProvider : IEndpointConstraintProvider
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public int Order => -1000;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void OnProvidersExecuting(EndpointConstraintProviderContext context)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
for (var i = 0; i < context.Results.Count; i++)
|
||||
{
|
||||
ProvideConstraint(context.Results[i], context.HttpContext.RequestServices);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void OnProvidersExecuted(EndpointConstraintProviderContext context)
|
||||
{
|
||||
}
|
||||
|
||||
private void ProvideConstraint(EndpointConstraintItem item, IServiceProvider services)
|
||||
{
|
||||
// Don't overwrite anything that was done by a previous provider.
|
||||
if (item.Constraint != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.Metadata is IEndpointConstraint constraint)
|
||||
{
|
||||
item.Constraint = constraint;
|
||||
item.IsReusable = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.Metadata is IEndpointConstraintFactory factory)
|
||||
{
|
||||
item.Constraint = factory.CreateInstance(services);
|
||||
item.IsReusable = factory.IsReusable;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface IEndpointConstraintFactory : IEndpointConstraintMetadata
|
||||
{
|
||||
bool IsReusable { get; }
|
||||
|
||||
IEndpointConstraint CreateInstance(IServiceProvider services);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,14 +2,26 @@
|
|||
// 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.Http;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a collection of <see cref="Endpoint"/> instances.
|
||||
/// </summary>
|
||||
public abstract class EndpointDataSource
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a <see cref="IChangeToken"/> used to signal invalidation of cached <see cref="Endpoint"/>
|
||||
/// instances.
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="IChangeToken"/>.</returns>
|
||||
public abstract IChangeToken GetChangeToken();
|
||||
|
||||
/// <summary>
|
||||
/// Returns a read-only collection of <see cref="Endpoint"/> instances.
|
||||
/// </summary>
|
||||
public abstract IReadOnlyList<Endpoint> Endpoints { get; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,37 +3,63 @@
|
|||
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
public sealed class EndpointFeature : IEndpointFeature, IRoutingFeature
|
||||
public sealed class EndpointFeature : IEndpointFeature, IRouteValuesFeature, IRoutingFeature
|
||||
{
|
||||
private RouteData _routeData;
|
||||
private RouteValueDictionary _values;
|
||||
private RouteValueDictionary _routeValues;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the selected <see cref="Http.Endpoint"/> for the current
|
||||
/// request.
|
||||
/// </summary>
|
||||
public Endpoint Endpoint { get; set; }
|
||||
|
||||
public Func<RequestDelegate, RequestDelegate> Invoker { get; set; }
|
||||
|
||||
public RouteValueDictionary Values
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="RouteValueDictionary"/> associated with the currrent
|
||||
/// request.
|
||||
/// </summary>
|
||||
public RouteValueDictionary RouteValues
|
||||
{
|
||||
get => _values;
|
||||
get => _routeValues;
|
||||
set
|
||||
{
|
||||
_values = value;
|
||||
_routeValues = value;
|
||||
|
||||
// RouteData will be created next get with new Values
|
||||
_routeData = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="RouteData"/> for the current request.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The setter is not implemented. Use <see cref="RouteValues"/> to set the route values.
|
||||
/// </remarks>
|
||||
RouteData IRoutingFeature.RouteData
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_routeData == null)
|
||||
{
|
||||
_routeData = new RouteData(_values);
|
||||
_routeData = _routeValues == null ? new RouteData() : new RouteData(_routeValues);
|
||||
|
||||
// Note: DataTokens won't update if someone else overwrites the Endpoint
|
||||
// after route values has been set. This seems find since endpoints are a new
|
||||
// feature and DataTokens are for back-compat.
|
||||
var dataTokensMetadata = Endpoint?.Metadata.GetMetadata<IDataTokensMetadata>();
|
||||
if (dataTokensMetadata != null)
|
||||
{
|
||||
var dataTokens = _routeData.DataTokens;
|
||||
foreach (var kvp in dataTokensMetadata.DataTokens)
|
||||
{
|
||||
_routeData.DataTokens.Add(kvp.Key, kvp.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _routeData;
|
||||
|
|
@ -41,4 +67,4 @@ namespace Microsoft.AspNetCore.Routing
|
|||
set => throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
|
|
@ -31,26 +32,18 @@ namespace Microsoft.AspNetCore.Routing
|
|||
|
||||
public async Task Invoke(HttpContext httpContext)
|
||||
{
|
||||
var feature = httpContext.Features.Get<IEndpointFeature>();
|
||||
if (feature == null)
|
||||
var endpoint = httpContext.Features.Get<IEndpointFeature>()?.Endpoint;
|
||||
if (endpoint?.RequestDelegate != null)
|
||||
{
|
||||
var message = $"Unable to execute an endpoint because the {nameof(GlobalRoutingMiddleware)} was not run for this request. " +
|
||||
$"Ensure {nameof(GlobalRoutingMiddleware)} is added to the request execution pipeline before {nameof(EndpointMiddleware)} in application startup code.";
|
||||
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
|
||||
if (feature.Invoker != null)
|
||||
{
|
||||
Log.ExecutingEndpoint(_logger, feature.Endpoint);
|
||||
Log.ExecutingEndpoint(_logger, endpoint);
|
||||
|
||||
try
|
||||
{
|
||||
await feature.Invoker(_next)(httpContext);
|
||||
await endpoint.RequestDelegate(httpContext);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Log.ExecutedEndpoint(_logger, feature.Endpoint);
|
||||
Log.ExecutedEndpoint(_logger, endpoint);
|
||||
}
|
||||
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ using System.Collections.Generic;
|
|||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
public class EndpointOptions
|
||||
// Internal for 2.2. Public API for configuring endpoints will be added in 3.0
|
||||
internal class EndpointOptions
|
||||
{
|
||||
public IList<EndpointDataSource> DataSources { get; } = new List<EndpointDataSource>();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,13 +5,14 @@ using System;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing.Matchers;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Routing.Matching;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
internal sealed class GlobalRoutingMiddleware
|
||||
internal sealed class EndpointRoutingMiddleware
|
||||
{
|
||||
private readonly MatcherFactory _matcherFactory;
|
||||
private readonly ILogger _logger;
|
||||
|
|
@ -20,10 +21,10 @@ namespace Microsoft.AspNetCore.Routing
|
|||
|
||||
private Task<Matcher> _initializationTask;
|
||||
|
||||
public GlobalRoutingMiddleware(
|
||||
public EndpointRoutingMiddleware(
|
||||
MatcherFactory matcherFactory,
|
||||
CompositeEndpointDataSource endpointDataSource,
|
||||
ILogger<GlobalRoutingMiddleware> logger,
|
||||
ILogger<EndpointRoutingMiddleware> logger,
|
||||
RequestDelegate next)
|
||||
{
|
||||
if (matcherFactory == null)
|
||||
|
|
@ -55,10 +56,6 @@ namespace Microsoft.AspNetCore.Routing
|
|||
public async Task Invoke(HttpContext httpContext)
|
||||
{
|
||||
var feature = new EndpointFeature();
|
||||
httpContext.Features.Set<IEndpointFeature>(feature);
|
||||
|
||||
// Back compat support for users of IRoutingFeature
|
||||
httpContext.Features.Set<IRoutingFeature>(feature);
|
||||
|
||||
// There's an inherent race condition between waiting for init and accessing the matcher
|
||||
// this is OK because once `_matcher` is initialized, it will not be set to null again.
|
||||
|
|
@ -67,6 +64,10 @@ namespace Microsoft.AspNetCore.Routing
|
|||
await matcher.MatchAsync(httpContext, feature);
|
||||
if (feature.Endpoint != null)
|
||||
{
|
||||
// Set the endpoint feature only on success. This means we won't overwrite any
|
||||
// existing state for related features unless we did something.
|
||||
SetEndpointFeature(httpContext, feature);
|
||||
|
||||
Log.MatchSuccess(_logger, feature);
|
||||
}
|
||||
else
|
||||
|
|
@ -77,6 +78,15 @@ namespace Microsoft.AspNetCore.Routing
|
|||
await _next(httpContext);
|
||||
}
|
||||
|
||||
private static void SetEndpointFeature(HttpContext httpContext, EndpointFeature feature)
|
||||
{
|
||||
// For back-compat EndpointRouteValuesFeature implements IEndpointFeature,
|
||||
// IRouteValuesFeature and IRoutingFeature
|
||||
httpContext.Features.Set<IRoutingFeature>(feature);
|
||||
httpContext.Features.Set<IRouteValuesFeature>(feature);
|
||||
httpContext.Features.Set<IEndpointFeature>(feature);
|
||||
}
|
||||
|
||||
// Initialization is async to avoid blocking threads while reflection and things
|
||||
// of that nature take place.
|
||||
//
|
||||
|
|
@ -127,4 +137,4 @@ namespace Microsoft.AspNetCore.Routing
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
// 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.Diagnostics;
|
||||
using System.Linq;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents HTTP method metadata used during routing.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("{DebuggerToString(),nq}")]
|
||||
public sealed class HttpMethodMetadata : IHttpMethodMetadata
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HttpMethodMetadata" /> class.
|
||||
/// </summary>
|
||||
/// <param name="httpMethods">
|
||||
/// The HTTP methods used during routing.
|
||||
/// An empty collection means any HTTP method will be accepted.
|
||||
/// </param>
|
||||
public HttpMethodMetadata(IEnumerable<string> httpMethods)
|
||||
: this(httpMethods, acceptCorsPreflight: false)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HttpMethodMetadata" /> class.
|
||||
/// </summary>
|
||||
/// <param name="httpMethods">
|
||||
/// The HTTP methods used during routing.
|
||||
/// An empty collection means any HTTP method will be accepted.
|
||||
/// </param>
|
||||
/// <param name="acceptCorsPreflight">A value indicating whether routing accepts CORS preflight requests.</param>
|
||||
public HttpMethodMetadata(IEnumerable<string> httpMethods, bool acceptCorsPreflight)
|
||||
{
|
||||
if (httpMethods == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(httpMethods));
|
||||
}
|
||||
|
||||
HttpMethods = httpMethods.ToArray();
|
||||
AcceptCorsPreflight = acceptCorsPreflight;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a value indicating whether the associated endpoint should accept CORS preflight requests.
|
||||
/// </summary>
|
||||
public bool AcceptCorsPreflight { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns a read-only collection of HTTP methods used during routing.
|
||||
/// An empty collection means any HTTP method will be accepted.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> HttpMethods { get; }
|
||||
|
||||
private string DebuggerToString()
|
||||
{
|
||||
return $"HttpMethods: {string.Join(",", HttpMethods)} - Cors: {AcceptCorsPreflight}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
/// <summary>
|
||||
/// Metadata that defines data tokens for an <see cref="Endpoint"/>. This metadata
|
||||
/// type provides data tokens value for <see cref="RouteData.DataTokens"/> associated
|
||||
/// with an endpoint.
|
||||
/// </summary>
|
||||
public interface IDataTokensMetadata
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the data tokens.
|
||||
/// </summary>
|
||||
IReadOnlyDictionary<string, object> DataTokens { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
// 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.Http;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines a contract to find endpoints based on the supplied lookup information.
|
||||
/// </summary>
|
||||
/// <typeparam name="TAddress">The address type to look up endpoints.</typeparam>
|
||||
public interface IEndpointFinder<TAddress>
|
||||
{
|
||||
/// <summary>
|
||||
/// Finds endpoints based on the supplied lookup information.
|
||||
/// </summary>
|
||||
/// <param name="address">The information used to look up endpoints.</param>
|
||||
/// <returns>A collection of <see cref="Endpoint"/>.</returns>
|
||||
IEnumerable<Endpoint> FindEndpoints(TAddress address);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents HTTP method metadata used during routing.
|
||||
/// </summary>
|
||||
public interface IHttpMethodMetadata
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a value indicating whether the associated endpoint should accept CORS preflight requests.
|
||||
/// </summary>
|
||||
bool AcceptCorsPreflight { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns a read-only collection of HTTP methods used during routing.
|
||||
/// An empty collection means any HTTP method will be accepted.
|
||||
/// </summary>
|
||||
IReadOnlyList<string> HttpMethods { get; }
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue