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>
|
<PackageSigningCertName>MicrosoftNuGet</PackageSigningCertName>
|
||||||
<PublicSign Condition="'$(OS)' != 'Windows_NT'">true</PublicSign>
|
<PublicSign Condition="'$(OS)' != 'Windows_NT'">true</PublicSign>
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
<LangVersion>7.2</LangVersion>
|
<LangVersion>7.3</LangVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
30
Routing.sln
30
Routing.sln
|
|
@ -51,7 +51,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarkapps", "benchmarka
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmarks", "benchmarkapps\Benchmarks\Benchmarks.csproj", "{91F47A60-9A78-4968-B10D-157D9BFAC37F}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmarks", "benchmarkapps\Benchmarks\Benchmarks.csproj", "{91F47A60-9A78-4968-B10D-157D9BFAC37F}"
|
||||||
EndProject
|
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
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
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|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.ActiveCfg = Release|Any CPU
|
||||||
{91F47A60-9A78-4968-B10D-157D9BFAC37F}.Release|x86.Build.0 = 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
|
{B8516771-E850-4724-BEC3-63FC00C2AE57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{B8516771-E850-4724-BEC3-63FC00C2AE57}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
|
{B8516771-E850-4724-BEC3-63FC00C2AE57}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
|
||||||
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
|
{B8516771-E850-4724-BEC3-63FC00C2AE57}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
|
||||||
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}.Debug|x86.ActiveCfg = Debug|Any CPU
|
{B8516771-E850-4724-BEC3-63FC00C2AE57}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}.Debug|x86.Build.0 = Debug|Any CPU
|
{B8516771-E850-4724-BEC3-63FC00C2AE57}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{B8516771-E850-4724-BEC3-63FC00C2AE57}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}.Release|Any CPU.Build.0 = Release|Any CPU
|
{B8516771-E850-4724-BEC3-63FC00C2AE57}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
|
{B8516771-E850-4724-BEC3-63FC00C2AE57}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
|
||||||
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}.Release|Mixed Platforms.Build.0 = Release|Any CPU
|
{B8516771-E850-4724-BEC3-63FC00C2AE57}.Release|Mixed Platforms.Build.0 = Release|Any CPU
|
||||||
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}.Release|x86.ActiveCfg = Release|Any CPU
|
{B8516771-E850-4724-BEC3-63FC00C2AE57}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}.Release|x86.Build.0 = Release|Any CPU
|
{B8516771-E850-4724-BEC3-63FC00C2AE57}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
@ -191,7 +193,7 @@ Global
|
||||||
{5C73140B-41F3-466F-A07B-3614E4D80DF9} = {95359B4B-4C85-4B44-A75B-0621905C4CF6}
|
{5C73140B-41F3-466F-A07B-3614E4D80DF9} = {95359B4B-4C85-4B44-A75B-0621905C4CF6}
|
||||||
{F3D86714-4E64-41A6-9B36-A47B3683CF5D} = {D5F39F59-5725-4127-82E7-67028D006185}
|
{F3D86714-4E64-41A6-9B36-A47B3683CF5D} = {D5F39F59-5725-4127-82E7-67028D006185}
|
||||||
{91F47A60-9A78-4968-B10D-157D9BFAC37F} = {7F5914E2-C63F-4759-898E-462804357C90}
|
{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
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {36C8D815-B7F1-479D-894B-E606FB8DECDA}
|
SolutionGuid = {36C8D815-B7F1-479D-894B-E606FB8DECDA}
|
||||||
|
|
|
||||||
|
|
@ -29,11 +29,11 @@ namespace Benchmarks
|
||||||
.UseKestrel();
|
.UseKestrel();
|
||||||
|
|
||||||
var scenario = config["scenarios"]?.ToLower();
|
var scenario = config["scenarios"]?.ToLower();
|
||||||
if (scenario == "plaintextdispatcher" || scenario == "plaintextglobalrouting")
|
if (scenario == "plaintextdispatcher" || scenario == "plaintextendpointrouting")
|
||||||
{
|
{
|
||||||
webHostBuilder.UseStartup<StartupUsingGlobalRouting>();
|
webHostBuilder.UseStartup<StartupUsingEndpointRouting>();
|
||||||
// for testing
|
// for testing
|
||||||
webHostBuilder.UseSetting("Startup", nameof(StartupUsingGlobalRouting));
|
webHostBuilder.UseSetting("Startup", nameof(StartupUsingEndpointRouting));
|
||||||
}
|
}
|
||||||
else if (scenario == "plaintextrouting" || scenario == "plaintextrouter")
|
else if (scenario == "plaintextrouting" || scenario == "plaintextrouter")
|
||||||
{
|
{
|
||||||
|
|
@ -44,7 +44,7 @@ namespace Benchmarks
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
$"Invalid scenario '{scenario}'. Allowed scenarios are PlaintextGlobalRouting and PlaintextRouter");
|
$"Invalid scenario '{scenario}'. Allowed scenarios are PlaintextEndpointRouting and PlaintextRouter");
|
||||||
}
|
}
|
||||||
|
|
||||||
return webHostBuilder;
|
return webHostBuilder;
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,16 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Internal;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
using Microsoft.AspNetCore.Routing.Matchers;
|
|
||||||
using Microsoft.AspNetCore.Routing.Patterns;
|
using Microsoft.AspNetCore.Routing.Patterns;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
|
||||||
namespace Benchmarks
|
namespace Benchmarks
|
||||||
{
|
{
|
||||||
public class StartupUsingGlobalRouting
|
public class StartupUsingEndpointRouting
|
||||||
{
|
{
|
||||||
private static readonly byte[] _helloWorldPayload = Encoding.UTF8.GetBytes("Hello, World!");
|
private static readonly byte[] _helloWorldPayload = Encoding.UTF8.GetBytes("Hello, World!");
|
||||||
|
|
||||||
|
|
@ -18,12 +19,10 @@ namespace Benchmarks
|
||||||
{
|
{
|
||||||
services.AddRouting();
|
services.AddRouting();
|
||||||
|
|
||||||
services.Configure<EndpointOptions>(options =>
|
var endpointDataSource = new DefaultEndpointDataSource(new[]
|
||||||
{
|
|
||||||
options.DataSources.Add(new DefaultEndpointDataSource(new[]
|
|
||||||
{
|
{
|
||||||
new MatcherEndpoint(
|
new RouteEndpoint(
|
||||||
invoker: (next) => (httpContext) =>
|
requestDelegate: (httpContext) =>
|
||||||
{
|
{
|
||||||
var response = httpContext.Response;
|
var response = httpContext.Response;
|
||||||
var payloadLength = _helloWorldPayload.Length;
|
var payloadLength = _helloWorldPayload.Length;
|
||||||
|
|
@ -33,17 +32,17 @@ namespace Benchmarks
|
||||||
return response.Body.WriteAsync(_helloWorldPayload, 0, payloadLength);
|
return response.Body.WriteAsync(_helloWorldPayload, 0, payloadLength);
|
||||||
},
|
},
|
||||||
routePattern: RoutePatternFactory.Parse("/plaintext"),
|
routePattern: RoutePatternFactory.Parse("/plaintext"),
|
||||||
requiredValues: new RouteValueDictionary(),
|
|
||||||
order: 0,
|
order: 0,
|
||||||
metadata: EndpointMetadataCollection.Empty,
|
metadata: EndpointMetadataCollection.Empty,
|
||||||
displayName: "Plaintext"),
|
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();
|
app.UseEndpoint();
|
||||||
}
|
}
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
"PlaintextDispatcher": {
|
"PlaintextDispatcher": {
|
||||||
"Path": "/plaintext"
|
"Path": "/plaintext"
|
||||||
},
|
},
|
||||||
"PlaintextGlobalRouting": {
|
"PlaintextEndpointRouting": {
|
||||||
"Path": "/plaintext"
|
"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.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
{
|
{
|
||||||
public abstract class FastPathTokenizerBenchmarkBase
|
public abstract class FastPathTokenizerBenchmarkBase
|
||||||
{
|
{
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
using System;
|
using System;
|
||||||
using BenchmarkDotNet.Attributes;
|
using BenchmarkDotNet.Attributes;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
{
|
{
|
||||||
public class FastPathTokenizerEmptyBenchmark : FastPathTokenizerBenchmarkBase
|
public class FastPathTokenizerEmptyBenchmark : FastPathTokenizerBenchmarkBase
|
||||||
{
|
{
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
using System;
|
using System;
|
||||||
using BenchmarkDotNet.Attributes;
|
using BenchmarkDotNet.Attributes;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
{
|
{
|
||||||
public class FastPathTokenizerLargeBenchmark : FastPathTokenizerBenchmarkBase
|
public class FastPathTokenizerLargeBenchmark : FastPathTokenizerBenchmarkBase
|
||||||
{
|
{
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
using System;
|
using System;
|
||||||
using BenchmarkDotNet.Attributes;
|
using BenchmarkDotNet.Attributes;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
{
|
{
|
||||||
public class FastPathTokenizerPlaintextBenchmark : FastPathTokenizerBenchmarkBase
|
public class FastPathTokenizerPlaintextBenchmark : FastPathTokenizerBenchmarkBase
|
||||||
{
|
{
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
using System;
|
using System;
|
||||||
using BenchmarkDotNet.Attributes;
|
using BenchmarkDotNet.Attributes;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
{
|
{
|
||||||
public class FastPathTokenizerSmallBenchmark : FastPathTokenizerBenchmarkBase
|
public class FastPathTokenizerSmallBenchmark : FastPathTokenizerBenchmarkBase
|
||||||
{
|
{
|
||||||
|
|
@ -3,10 +3,9 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
|
||||||
using BenchmarkDotNet.Attributes;
|
using BenchmarkDotNet.Attributes;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
{
|
{
|
||||||
public class JumpTableMultipleEntryBenchmark
|
public class JumpTableMultipleEntryBenchmark
|
||||||
{
|
{
|
||||||
|
|
@ -15,12 +14,11 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
|
|
||||||
private JumpTable _linearSearch;
|
private JumpTable _linearSearch;
|
||||||
private JumpTable _dictionary;
|
private JumpTable _dictionary;
|
||||||
private JumpTable _ascii;
|
private JumpTable _trie;
|
||||||
private JumpTable _dictionaryLookup;
|
private JumpTable _vectorTrie;
|
||||||
private JumpTable _customHashTable;
|
|
||||||
|
|
||||||
// All factors of 100 to support sampling
|
// All factors of 100 to support sampling
|
||||||
[Params(2, 4, 5, 10, 25)]
|
[Params(2, 5, 10, 25, 50, 100)]
|
||||||
public int Count;
|
public int Count;
|
||||||
|
|
||||||
[GlobalSetup]
|
[GlobalSetup]
|
||||||
|
|
@ -48,9 +46,8 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
|
|
||||||
_linearSearch = new LinearSearchJumpTable(0, -1, entries.ToArray());
|
_linearSearch = new LinearSearchJumpTable(0, -1, entries.ToArray());
|
||||||
_dictionary = new DictionaryJumpTable(0, -1, entries.ToArray());
|
_dictionary = new DictionaryJumpTable(0, -1, entries.ToArray());
|
||||||
Debug.Assert(AsciiKeyedJumpTable.TryCreate(0, -1, entries, out _ascii));
|
_trie = new ILEmitTrieJumpTable(0, -1, entries.ToArray(), vectorize: false, _dictionary);
|
||||||
_dictionaryLookup = new DictionaryLookupJumpTable(0, -1, entries.ToArray());
|
_vectorTrie = new ILEmitTrieJumpTable(0, -1, entries.ToArray(), vectorize: true, _dictionary);
|
||||||
_customHashTable = new CustomHashTableJumpTable(0, -1, entries.ToArray());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// This baseline is similar to SingleEntryJumpTable. We just want
|
// This baseline is similar to SingleEntryJumpTable. We just want
|
||||||
|
|
@ -67,15 +64,24 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
var @string = strings[i];
|
var @string = strings[i];
|
||||||
var segment = segments[i];
|
var segment = segments[i];
|
||||||
|
|
||||||
destination = segment.Length == 0 ? -1 :
|
if (segment.Length == 0)
|
||||||
segment.Length != @string.Length ? 1 :
|
{
|
||||||
string.Compare(
|
destination = -1;
|
||||||
|
}
|
||||||
|
else if (segment.Length != @string.Length)
|
||||||
|
{
|
||||||
|
destination = 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
destination = string.Compare(
|
||||||
@string,
|
@string,
|
||||||
segment.Start,
|
segment.Start,
|
||||||
@string,
|
@string,
|
||||||
0,
|
0,
|
||||||
@string.Length,
|
segment.Length,
|
||||||
StringComparison.OrdinalIgnoreCase);
|
StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return destination;
|
return destination;
|
||||||
|
|
@ -112,7 +118,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
}
|
}
|
||||||
|
|
||||||
[Benchmark(OperationsPerInvoke = 100)]
|
[Benchmark(OperationsPerInvoke = 100)]
|
||||||
public int Ascii()
|
public int Trie()
|
||||||
{
|
{
|
||||||
var strings = _strings;
|
var strings = _strings;
|
||||||
var segments = _segments;
|
var segments = _segments;
|
||||||
|
|
@ -120,14 +126,14 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
var destination = 0;
|
var destination = 0;
|
||||||
for (var i = 0; i < strings.Length; i++)
|
for (var i = 0; i < strings.Length; i++)
|
||||||
{
|
{
|
||||||
destination = _ascii.GetDestination(strings[i], segments[i]);
|
destination = _trie.GetDestination(strings[i], segments[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return destination;
|
return destination;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Benchmark(OperationsPerInvoke = 100)]
|
[Benchmark(OperationsPerInvoke = 100)]
|
||||||
public int DictionaryLookup()
|
public int VectorTrie()
|
||||||
{
|
{
|
||||||
var strings = _strings;
|
var strings = _strings;
|
||||||
var segments = _segments;
|
var segments = _segments;
|
||||||
|
|
@ -135,22 +141,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
var destination = 0;
|
var destination = 0;
|
||||||
for (var i = 0; i < strings.Length; i++)
|
for (var i = 0; i < strings.Length; i++)
|
||||||
{
|
{
|
||||||
destination = _dictionaryLookup.GetDestination(strings[i], segments[i]);
|
destination = _vectorTrie.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]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return destination;
|
return destination;
|
||||||
|
|
@ -164,7 +155,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
var guid = Guid.NewGuid().ToString();
|
var guid = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
// Between 5 and 36 characters
|
// 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]))
|
if (char.IsDigit(text[0]))
|
||||||
{
|
{
|
||||||
// Convert first character to a letter.
|
// 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;
|
using BenchmarkDotNet.Attributes;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
{
|
{
|
||||||
public class JumpTableZeroEntryBenchmark
|
public class JumpTableZeroEntryBenchmark
|
||||||
{
|
{
|
||||||
|
|
@ -3,11 +3,12 @@
|
||||||
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BenchmarkDotNet.Attributes;
|
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
|
// 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;
|
private const int SampleCount = 100;
|
||||||
|
|
||||||
|
|
@ -4,16 +4,16 @@
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
{
|
{
|
||||||
// This code was generated by the Swaggatherer
|
// This code was generated by the Swaggatherer
|
||||||
public partial class MatcherAzureBenchmark : MatcherBenchmarkBase
|
public partial class MatcherAzureBenchmark : EndpointRoutingBenchmarkBase
|
||||||
{
|
{
|
||||||
private const int EndpointCount = 5160;
|
private const int EndpointCount = 5160;
|
||||||
|
|
||||||
private void SetupEndpoints()
|
private void SetupEndpoints()
|
||||||
{
|
{
|
||||||
Endpoints = new MatcherEndpoint[5160];
|
Endpoints = new RouteEndpoint[5160];
|
||||||
Endpoints[0] = CreateEndpoint("/account", "GET");
|
Endpoints[0] = CreateEndpoint("/account", "GET");
|
||||||
Endpoints[1] = CreateEndpoint("/analyze", "POST");
|
Endpoints[1] = CreateEndpoint("/analyze", "POST");
|
||||||
Endpoints[2] = CreateEndpoint("/apis", "GET");
|
Endpoints[2] = CreateEndpoint("/apis", "GET");
|
||||||
|
|
@ -4,11 +4,15 @@
|
||||||
using System;
|
using System;
|
||||||
using BenchmarkDotNet.Attributes;
|
using BenchmarkDotNet.Attributes;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
{
|
{
|
||||||
// Generated from https://github.com/Azure/azure-rest-api-specs
|
// 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 const int SampleCount = 100;
|
||||||
|
|
||||||
private BarebonesMatcher _baseline;
|
private BarebonesMatcher _baseline;
|
||||||
|
|
@ -58,7 +62,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
var httpContext = Requests[sample];
|
var httpContext = Requests[sample];
|
||||||
|
|
||||||
var path = httpContext.Request.Path.Value;
|
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 count = FastPathTokenizer.Tokenize(path, segments);
|
||||||
|
|
||||||
var candidates = _dfa.FindCandidateSet(httpContext, path, segments.Slice(0, count));
|
var candidates = _dfa.FindCandidateSet(httpContext, path, segments.Slice(0, count));
|
||||||
|
|
@ -4,16 +4,16 @@
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
{
|
{
|
||||||
// This code was generated by the Swaggatherer
|
// This code was generated by the Swaggatherer
|
||||||
public partial class MatcherFindCandidateSetAzureBenchmark : MatcherBenchmarkBase
|
public partial class MatcherFindCandidateSetAzureBenchmark : EndpointRoutingBenchmarkBase
|
||||||
{
|
{
|
||||||
private const int EndpointCount = 3517;
|
private const int EndpointCount = 3517;
|
||||||
|
|
||||||
private void SetupEndpoints()
|
private void SetupEndpoints()
|
||||||
{
|
{
|
||||||
Endpoints = new MatcherEndpoint[3517];
|
Endpoints = new RouteEndpoint[3517];
|
||||||
Endpoints[0] = CreateEndpoint("/account");
|
Endpoints[0] = CreateEndpoint("/account");
|
||||||
Endpoints[1] = CreateEndpoint("/analyze");
|
Endpoints[1] = CreateEndpoint("/analyze");
|
||||||
Endpoints[2] = CreateEndpoint("/apis");
|
Endpoints[2] = CreateEndpoint("/apis");
|
||||||
|
|
@ -5,12 +5,16 @@ using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BenchmarkDotNet.Attributes;
|
using BenchmarkDotNet.Attributes;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
{
|
{
|
||||||
// Generated from https://github.com/APIs-guru/openapi-directory
|
// Generated from https://github.com/APIs-guru/openapi-directory
|
||||||
// Use https://editor2.swagger.io/ to convert from yaml to json-
|
// 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 BarebonesMatcher _baseline;
|
||||||
private DfaMatcher _dfa;
|
private DfaMatcher _dfa;
|
||||||
|
|
||||||
|
|
@ -50,7 +54,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
var httpContext = Requests[i];
|
var httpContext = Requests[i];
|
||||||
|
|
||||||
var path = httpContext.Request.Path.Value;
|
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 count = FastPathTokenizer.Tokenize(path, segments);
|
||||||
|
|
||||||
var candidates = _dfa.FindCandidateSet(httpContext, path, segments.Slice(0, count));
|
var candidates = _dfa.FindCandidateSet(httpContext, path, segments.Slice(0, count));
|
||||||
|
|
@ -4,16 +4,16 @@
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
{
|
{
|
||||||
// This code was generated by the Swaggatherer
|
// This code was generated by the Swaggatherer
|
||||||
public partial class MatcherFindCandidateSetGithubBenchmark : MatcherBenchmarkBase
|
public partial class MatcherFindCandidateSetGithubBenchmark : EndpointRoutingBenchmarkBase
|
||||||
{
|
{
|
||||||
private const int EndpointCount = 155;
|
private const int EndpointCount = 155;
|
||||||
|
|
||||||
private void SetupEndpoints()
|
private void SetupEndpoints()
|
||||||
{
|
{
|
||||||
Endpoints = new MatcherEndpoint[155];
|
Endpoints = new RouteEndpoint[155];
|
||||||
Endpoints[0] = CreateEndpoint("/emojis");
|
Endpoints[0] = CreateEndpoint("/emojis");
|
||||||
Endpoints[1] = CreateEndpoint("/events");
|
Endpoints[1] = CreateEndpoint("/events");
|
||||||
Endpoints[2] = CreateEndpoint("/feeds");
|
Endpoints[2] = CreateEndpoint("/feeds");
|
||||||
|
|
@ -5,17 +5,20 @@ using System;
|
||||||
using BenchmarkDotNet.Attributes;
|
using BenchmarkDotNet.Attributes;
|
||||||
using Microsoft.AspNetCore.Http;
|
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 TrivialMatcher _baseline;
|
||||||
private DfaMatcher _dfa;
|
private DfaMatcher _dfa;
|
||||||
|
|
||||||
[GlobalSetup]
|
[GlobalSetup]
|
||||||
public void Setup()
|
public void Setup()
|
||||||
{
|
{
|
||||||
Endpoints = new MatcherEndpoint[1];
|
Endpoints = new RouteEndpoint[1];
|
||||||
Endpoints[0] = CreateEndpoint("/plaintext");
|
Endpoints[0] = CreateEndpoint("/plaintext");
|
||||||
|
|
||||||
Requests = new HttpContext[1];
|
Requests = new HttpContext[1];
|
||||||
|
|
@ -51,7 +54,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
{
|
{
|
||||||
var httpContext = Requests[0];
|
var httpContext = Requests[0];
|
||||||
var path = httpContext.Request.Path.Value;
|
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 count = FastPathTokenizer.Tokenize(path, segments);
|
||||||
|
|
||||||
var candidates = _dfa.FindCandidateSet(httpContext, path, segments.Slice(0, count));
|
var candidates = _dfa.FindCandidateSet(httpContext, path, segments.Slice(0, count));
|
||||||
|
|
@ -4,11 +4,15 @@
|
||||||
using System;
|
using System;
|
||||||
using BenchmarkDotNet.Attributes;
|
using BenchmarkDotNet.Attributes;
|
||||||
using Microsoft.AspNetCore.Http;
|
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 TrivialMatcher _baseline;
|
||||||
private DfaMatcher _dfa;
|
private DfaMatcher _dfa;
|
||||||
|
|
||||||
|
|
@ -29,7 +33,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
|
|
||||||
private void SetupEndpoints()
|
private void SetupEndpoints()
|
||||||
{
|
{
|
||||||
Endpoints = new MatcherEndpoint[10];
|
Endpoints = new RouteEndpoint[10];
|
||||||
Endpoints[0] = CreateEndpoint("/another-really-cool-entry");
|
Endpoints[0] = CreateEndpoint("/another-really-cool-entry");
|
||||||
Endpoints[1] = CreateEndpoint("/Some-Entry");
|
Endpoints[1] = CreateEndpoint("/Some-Entry");
|
||||||
Endpoints[2] = CreateEndpoint("/a/path/with/more/segments");
|
Endpoints[2] = CreateEndpoint("/a/path/with/more/segments");
|
||||||
|
|
@ -87,7 +91,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
var httpContext = Requests[0];
|
var httpContext = Requests[0];
|
||||||
var path = httpContext.Request.Path.Value;
|
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 count = FastPathTokenizer.Tokenize(path, segments);
|
||||||
|
|
||||||
var candidates = _dfa.FindCandidateSet(httpContext, path, segments.Slice(0, count));
|
var candidates = _dfa.FindCandidateSet(httpContext, path, segments.Slice(0, count));
|
||||||
|
|
@ -3,12 +3,13 @@
|
||||||
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BenchmarkDotNet.Attributes;
|
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
|
// Generated from https://github.com/APIs-guru/openapi-directory
|
||||||
// Use https://editor2.swagger.io/ to convert from yaml to json-
|
// 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 BarebonesMatcher _baseline;
|
||||||
private Matcher _dfa;
|
private Matcher _dfa;
|
||||||
|
|
@ -4,16 +4,16 @@
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
{
|
{
|
||||||
// This code was generated by the Swaggatherer
|
// This code was generated by the Swaggatherer
|
||||||
public partial class MatcherGithubBenchmark : MatcherBenchmarkBase
|
public partial class MatcherGithubBenchmark : EndpointRoutingBenchmarkBase
|
||||||
{
|
{
|
||||||
private const int EndpointCount = 243;
|
private const int EndpointCount = 243;
|
||||||
|
|
||||||
private void SetupEndpoints()
|
private void SetupEndpoints()
|
||||||
{
|
{
|
||||||
Endpoints = new MatcherEndpoint[243];
|
Endpoints = new RouteEndpoint[243];
|
||||||
Endpoints[0] = CreateEndpoint("/emojis", "GET");
|
Endpoints[0] = CreateEndpoint("/emojis", "GET");
|
||||||
Endpoints[1] = CreateEndpoint("/events", "GET");
|
Endpoints[1] = CreateEndpoint("/events", "GET");
|
||||||
Endpoints[2] = CreateEndpoint("/feeds", "GET");
|
Endpoints[2] = CreateEndpoint("/feeds", "GET");
|
||||||
|
|
@ -4,11 +4,12 @@
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BenchmarkDotNet.Attributes;
|
using BenchmarkDotNet.Attributes;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
{
|
{
|
||||||
// Just like TechEmpower Plaintext
|
// Just like TechEmpower Plaintext
|
||||||
public partial class MatcherSingleEntryBenchmark : MatcherBenchmarkBase
|
public partial class MatcherSingleEntryBenchmark : EndpointRoutingBenchmarkBase
|
||||||
{
|
{
|
||||||
private const int SampleCount = 100;
|
private const int SampleCount = 100;
|
||||||
|
|
||||||
|
|
@ -22,7 +23,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
[GlobalSetup]
|
[GlobalSetup]
|
||||||
public void Setup()
|
public void Setup()
|
||||||
{
|
{
|
||||||
Endpoints = new MatcherEndpoint[1];
|
Endpoints = new RouteEndpoint[1];
|
||||||
Endpoints[0] = CreateEndpoint("/plaintext");
|
Endpoints[0] = CreateEndpoint("/plaintext");
|
||||||
|
|
||||||
Requests = new HttpContext[1];
|
Requests = new HttpContext[1];
|
||||||
|
|
@ -4,25 +4,26 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http;
|
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
|
// 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
|
// perf tests. The idea with this matcher is that we can cheat on the requirements
|
||||||
// to establish a lower bound for perf comparisons.
|
// to establish a lower bound for perf comparisons.
|
||||||
internal sealed class TrivialMatcher : Matcher
|
internal sealed class TrivialMatcher : Matcher
|
||||||
{
|
{
|
||||||
private readonly MatcherEndpoint _endpoint;
|
private readonly RouteEndpoint _endpoint;
|
||||||
private readonly Candidate[] _candidates;
|
private readonly Candidate[] _candidates;
|
||||||
|
|
||||||
public TrivialMatcher(MatcherEndpoint endpoint)
|
public TrivialMatcher(RouteEndpoint endpoint)
|
||||||
{
|
{
|
||||||
_endpoint = endpoint;
|
_endpoint = endpoint;
|
||||||
|
|
||||||
_candidates = new Candidate[] { new Candidate(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)
|
if (httpContext == null)
|
||||||
{
|
{
|
||||||
|
|
@ -38,7 +39,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
if (string.Equals(_endpoint.RoutePattern.RawText, path, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(_endpoint.RoutePattern.RawText, path, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
feature.Endpoint = _endpoint;
|
feature.Endpoint = _endpoint;
|
||||||
feature.Values = new RouteValueDictionary();
|
feature.RouteValues = new RouteValueDictionary();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
|
@ -4,13 +4,13 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
{
|
{
|
||||||
internal class TrivialMatcherBuilder : MatcherBuilder
|
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);
|
_endpoints.Add(endpoint);
|
||||||
}
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>netcoreapp2.2</TargetFrameworks>
|
<TargetFramework>netcoreapp2.2</TargetFramework>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<ServerGarbageCollection>true</ServerGarbageCollection>
|
<ServerGarbageCollection>true</ServerGarbageCollection>
|
||||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
|
|
@ -18,24 +18,25 @@
|
||||||
for perf comparisons.
|
for perf comparisons.
|
||||||
-->
|
-->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matchers\BarebonesMatcher.cs">
|
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matching\BarebonesMatcher.cs">
|
||||||
<Link>Matchers\BarebonesMatcher.cs</Link>
|
<Link>Matching\BarebonesMatcher.cs</Link>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matchers\BarebonesMatcherBuilder.cs">
|
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matching\BarebonesMatcherBuilder.cs">
|
||||||
<Link>Matchers\BarebonesMatcherBuilder.cs</Link>
|
<Link>Matching\BarebonesMatcherBuilder.cs</Link>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matchers\RouteMatcher.cs">
|
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matching\RouteMatcher.cs">
|
||||||
<Link>Matchers\RouteMatcher.cs</Link>
|
<Link>Matching\RouteMatcher.cs</Link>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matchers\RouteMatcherBuilder.cs">
|
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matching\RouteMatcherBuilder.cs">
|
||||||
<Link>Matchers\RouteMatcherBuilder.cs</Link>
|
<Link>Matching\RouteMatcherBuilder.cs</Link>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matchers\TreeRouterMatcher.cs">
|
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matching\TreeRouterMatcher.cs">
|
||||||
<Link>Matchers\TreeRouterMatcher.cs</Link>
|
<Link>Matching\TreeRouterMatcher.cs</Link>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matchers\TreeRouterMatcherBuilder.cs">
|
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matching\TreeRouterMatcherBuilder.cs">
|
||||||
<Link>Matchers\TreeRouterMatcherBuilder.cs</Link>
|
<Link>Matching\TreeRouterMatcherBuilder.cs</Link>
|
||||||
</Compile>
|
</Compile>
|
||||||
|
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\TestObjects\TestServiceProvider.cs" Link="Matching\TestServiceProvider.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<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>
|
<MoqPackageVersion>4.7.49</MoqPackageVersion>
|
||||||
<NETStandardLibrary20PackageVersion>2.0.3</NETStandardLibrary20PackageVersion>
|
<NETStandardLibrary20PackageVersion>2.0.3</NETStandardLibrary20PackageVersion>
|
||||||
<NewtonsoftJsonPackageVersion>11.0.2</NewtonsoftJsonPackageVersion>
|
<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>
|
<XunitPackageVersion>2.3.1</XunitPackageVersion>
|
||||||
<XunitRunnerVisualStudioPackageVersion>2.4.0-rc.1.build4038</XunitRunnerVisualStudioPackageVersion>
|
<XunitRunnerVisualStudioPackageVersion>2.4.0</XunitRunnerVisualStudioPackageVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<Import Project="$(DotNetPackageVersionPropsPath)" Condition=" '$(DotNetPackageVersionPropsPath)' != '' " />
|
<Import Project="$(DotNetPackageVersionPropsPath)" Condition=" '$(DotNetPackageVersionPropsPath)' != '' " />
|
||||||
|
<PropertyGroup Label="Package Versions: Pinned" />
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -12,4 +12,8 @@
|
||||||
<DotNetCoreRuntime Include="$(MicrosoftNETCoreApp21PackageVersion)" />
|
<DotNetCoreRuntime Include="$(MicrosoftNETCoreApp21PackageVersion)" />
|
||||||
<DotNetCoreRuntime Include="$(MicrosoftNETCoreApp22PackageVersion)" />
|
<DotNetCoreRuntime Include="$(MicrosoftNETCoreApp22PackageVersion)" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<EnableBenchmarkValidation>true</EnableBenchmarkValidation>
|
||||||
|
</PropertyGroup>
|
||||||
</Project>
|
</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 class Program
|
||||||
{
|
{
|
||||||
public static readonly string GlobalRoutingScenario = "globalrouting";
|
public const string EndpointRoutingScenario = "endpointrouting";
|
||||||
public static readonly string RouterScenario = "router";
|
public const string RouterScenario = "router";
|
||||||
|
|
||||||
public static void Main(string[] args)
|
public static void Main(string[] args)
|
||||||
{
|
{
|
||||||
|
|
@ -25,7 +25,7 @@ namespace RoutingSample.Web
|
||||||
if (args.Length == 0)
|
if (args.Length == 0)
|
||||||
{
|
{
|
||||||
Console.WriteLine("Choose a sample to run:");
|
Console.WriteLine("Choose a sample to run:");
|
||||||
Console.WriteLine($"1. {GlobalRoutingScenario}");
|
Console.WriteLine($"1. {EndpointRoutingScenario}");
|
||||||
Console.WriteLine($"2. {RouterScenario}");
|
Console.WriteLine($"2. {RouterScenario}");
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
|
|
||||||
|
|
@ -40,18 +40,18 @@ namespace RoutingSample.Web
|
||||||
switch (scenario)
|
switch (scenario)
|
||||||
{
|
{
|
||||||
case "1":
|
case "1":
|
||||||
case "globalrouting":
|
case EndpointRoutingScenario:
|
||||||
startupType = typeof(UseGlobalRoutingStartup);
|
startupType = typeof(UseEndpointRoutingStartup);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "2":
|
case "2":
|
||||||
case "router":
|
case RouterScenario:
|
||||||
startupType = typeof(UseRouterStartup);
|
startupType = typeof(UseRouterStartup);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
Console.WriteLine($"unknown scenario {scenario}");
|
Console.WriteLine($"unknown scenario {scenario}");
|
||||||
Console.WriteLine($"usage: dotnet run -- ({GlobalRoutingScenario}|{RouterScenario})");
|
Console.WriteLine($"usage: dotnet run -- ({EndpointRoutingScenario}|{RouterScenario})");
|
||||||
throw new InvalidOperationException();
|
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.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System.Diagnostics;
|
namespace Microsoft.AspNetCore.Http
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing
|
|
||||||
{
|
{
|
||||||
[DebuggerDisplay("{DisplayName,nq}")]
|
/// <summary>
|
||||||
public abstract class Endpoint
|
/// 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,
|
EndpointMetadataCollection metadata,
|
||||||
string displayName)
|
string displayName)
|
||||||
{
|
{
|
||||||
// All are allowed to be null
|
// All are allowed to be null
|
||||||
|
RequestDelegate = requestDelegate;
|
||||||
Metadata = metadata ?? EndpointMetadataCollection.Empty;
|
Metadata = metadata ?? EndpointMetadataCollection.Empty;
|
||||||
DisplayName = displayName;
|
DisplayName = displayName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the informational display name of this endpoint.
|
||||||
|
/// </summary>
|
||||||
public string DisplayName { get; }
|
public string DisplayName { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the collection of metadata associated with this endpoint.
|
||||||
|
/// </summary>
|
||||||
public EndpointMetadataCollection Metadata { get; }
|
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;
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
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>());
|
public static readonly EndpointMetadataCollection Empty = new EndpointMetadataCollection(Array.Empty<object>());
|
||||||
|
|
||||||
private readonly object[] _items;
|
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)
|
public EndpointMetadataCollection(IEnumerable<object> items)
|
||||||
{
|
{
|
||||||
if (items == null)
|
if (items == null)
|
||||||
|
|
@ -22,80 +40,161 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
}
|
}
|
||||||
|
|
||||||
_items = items.ToArray();
|
_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];
|
public object this[int index] => _items[index];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the count of metadata items.
|
||||||
|
/// </summary>
|
||||||
public int Count => _items.Length;
|
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
|
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;
|
var length = result.Length;
|
||||||
if (item != null)
|
return length > 0 ? (T)result[length - 1] : default;
|
||||||
{
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
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;
|
return (T[])result;
|
||||||
if (item != null)
|
|
||||||
{
|
|
||||||
yield return item;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
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();
|
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();
|
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enumerates the elements of an <see cref="EndpointMetadataCollection"/>.
|
||||||
|
/// </summary>
|
||||||
public struct Enumerator : IEnumerator<object>
|
public struct Enumerator : IEnumerator<object>
|
||||||
{
|
{
|
||||||
// Intentionally not readonly to prevent defensive struct copies
|
// Intentionally not readonly to prevent defensive struct copies
|
||||||
private object[] _items;
|
private object[] _items;
|
||||||
private int _index;
|
private int _index;
|
||||||
private object _current;
|
|
||||||
|
|
||||||
internal Enumerator(EndpointMetadataCollection collection)
|
internal Enumerator(EndpointMetadataCollection collection)
|
||||||
{
|
{
|
||||||
_items = collection._items;
|
_items = collection._items;
|
||||||
_index = 0;
|
_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()
|
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()
|
public bool MoveNext()
|
||||||
{
|
{
|
||||||
if (_index < _items.Length)
|
if (_index < _items.Length)
|
||||||
{
|
{
|
||||||
_current = _items[_index++];
|
Current = _items[_index++];
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_current = null;
|
Current = null;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the enumerator to its initial position, which is before the first element in the collection.
|
||||||
|
/// </summary>
|
||||||
public void Reset()
|
public void Reset()
|
||||||
{
|
{
|
||||||
_index = 0;
|
_index = 0;
|
||||||
_current = null;
|
Current = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,18 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
namespace Microsoft.AspNetCore.Http.Features
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing
|
|
||||||
{
|
{
|
||||||
|
/// <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
|
public interface IEndpointFeature
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the selected <see cref="Http.Endpoint"/> for the current
|
||||||
|
/// request.
|
||||||
|
/// </summary>
|
||||||
Endpoint Endpoint { get; set; }
|
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.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing
|
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
|
/// Defines the contract that a class must implement in order to check whether a URL parameter
|
||||||
/// value is valid for a constraint.
|
/// value is valid for a constraint.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IRouteConstraint
|
public interface IRouteConstraint : IParameterPolicy
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Determines whether the URL parameter contains a valid value for this constraint.
|
/// 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.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing
|
namespace Microsoft.AspNetCore.Routing
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Defines a contract to generate URLs to endpoints.
|
||||||
|
/// </summary>
|
||||||
public abstract class LinkGenerator
|
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.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing
|
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>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether all generated paths URLs are lower-case.
|
/// 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.
|
/// 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
|
IEnumerable<string> IReadOnlyDictionary<string, object>.Keys => Keys;
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return Keys;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public ICollection<object> Values
|
public ICollection<object> Values
|
||||||
|
|
@ -248,13 +242,7 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
IEnumerable<object> IReadOnlyDictionary<string, object>.Values
|
IEnumerable<object> IReadOnlyDictionary<string, object>.Values => Values;
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return Values;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
void ICollection<KeyValuePair<string, object>>.Add(KeyValuePair<string, object> item)
|
void ICollection<KeyValuePair<string, object>>.Add(KeyValuePair<string, object> item)
|
||||||
|
|
@ -565,7 +553,7 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
|
|
||||||
internal class PropertyStorage
|
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 object Value;
|
||||||
public readonly PropertyHelper[] Properties;
|
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.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using Microsoft.AspNetCore.Routing.EndpointConstraints;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Routing.Matchers;
|
|
||||||
using Microsoft.AspNetCore.Routing.Metadata;
|
|
||||||
using Microsoft.Extensions.Primitives;
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing
|
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}")]
|
[DebuggerDisplay("{DebuggerDisplayString,nq}")]
|
||||||
public class CompositeEndpointDataSource : EndpointDataSource
|
public sealed class CompositeEndpointDataSource : EndpointDataSource
|
||||||
{
|
{
|
||||||
private readonly EndpointDataSource[] _dataSources;
|
private readonly EndpointDataSource[] _dataSources;
|
||||||
private readonly object _lock;
|
private readonly object _lock;
|
||||||
|
|
@ -35,12 +36,20 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
_lock = new object();
|
_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()
|
public override IChangeToken GetChangeToken()
|
||||||
{
|
{
|
||||||
EnsureInitialized();
|
EnsureInitialized();
|
||||||
return _consumerChangeToken;
|
return _consumerChangeToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a read-only collection of <see cref="Endpoint"/> instances.
|
||||||
|
/// </summary>
|
||||||
public override IReadOnlyList<Endpoint> Endpoints
|
public override IReadOnlyList<Endpoint> Endpoints
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
|
@ -123,37 +132,57 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
foreach (var endpoint in _endpoints)
|
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;
|
template = string.IsNullOrEmpty(template) ? "\"\"" : template;
|
||||||
sb.Append(template);
|
sb.Append(template);
|
||||||
var requiredValues = matcherEndpoint.RequiredValues.Select(kvp => $"{kvp.Key} = \"{kvp.Value ?? "null"}\"");
|
sb.Append(", Defaults: new { ");
|
||||||
sb.Append(", Required Values: new { ");
|
sb.Append(string.Join(", ", FormatValues(routeEndpoint.RoutePattern.Defaults)));
|
||||||
sb.Append(string.Join(", ", requiredValues));
|
|
||||||
sb.Append(" }");
|
sb.Append(" }");
|
||||||
sb.Append(", Order:");
|
var routeValuesAddressMetadata = routeEndpoint.Metadata.GetMetadata<IRouteValuesAddressMetadata>();
|
||||||
sb.Append(matcherEndpoint.Order);
|
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)
|
if (httpMethodMetadata != null)
|
||||||
{
|
{
|
||||||
foreach (var httpMethod in httpMethodMetadata.HttpMethods)
|
sb.Append(", Http Methods: ");
|
||||||
{
|
sb.Append(string.Join(", ", httpMethodMetadata.HttpMethods));
|
||||||
sb.Append(", Http Methods: ");
|
|
||||||
sb.Append(string.Join(", ", httpMethod));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
sb.Append(", Display Name: ");
|
||||||
|
sb.Append(routeEndpoint.DisplayName);
|
||||||
sb.AppendLine();
|
sb.AppendLine();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
sb.Append("Non-MatcherEndpoint. DisplayName:");
|
sb.Append("Non-RouteEndpoint. DisplayName:");
|
||||||
sb.AppendLine(endpoint.DisplayName);
|
sb.AppendLine(endpoint.DisplayName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return sb.ToString();
|
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,
|
RouteValueDictionary values,
|
||||||
RouteDirection routeDirection)
|
RouteDirection routeDirection)
|
||||||
{
|
{
|
||||||
if (httpContext == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(httpContext));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (route == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(route));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (routeKey == null)
|
if (routeKey == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(routeKey));
|
throw new ArgumentNullException(nameof(routeKey));
|
||||||
|
|
|
||||||
|
|
@ -39,16 +39,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
|
||||||
RouteValueDictionary values,
|
RouteValueDictionary values,
|
||||||
RouteDirection routeDirection)
|
RouteDirection routeDirection)
|
||||||
{
|
{
|
||||||
if (httpContext == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(httpContext));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (route == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(route));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (routeKey == null)
|
if (routeKey == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(routeKey));
|
throw new ArgumentNullException(nameof(routeKey));
|
||||||
|
|
|
||||||
|
|
@ -26,16 +26,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
|
||||||
RouteValueDictionary values,
|
RouteValueDictionary values,
|
||||||
RouteDirection routeDirection)
|
RouteDirection routeDirection)
|
||||||
{
|
{
|
||||||
if (httpContext == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(httpContext));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (route == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(route));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (routeKey == null)
|
if (routeKey == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(routeKey));
|
throw new ArgumentNullException(nameof(routeKey));
|
||||||
|
|
|
||||||
|
|
@ -20,16 +20,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
|
||||||
RouteValueDictionary values,
|
RouteValueDictionary values,
|
||||||
RouteDirection routeDirection)
|
RouteDirection routeDirection)
|
||||||
{
|
{
|
||||||
if (httpContext == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(httpContext));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (route == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(route));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (routeKey == null)
|
if (routeKey == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(routeKey));
|
throw new ArgumentNullException(nameof(routeKey));
|
||||||
|
|
|
||||||
|
|
@ -20,16 +20,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
|
||||||
RouteValueDictionary values,
|
RouteValueDictionary values,
|
||||||
RouteDirection routeDirection)
|
RouteDirection routeDirection)
|
||||||
{
|
{
|
||||||
if (httpContext == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(httpContext));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (route == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(route));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (routeKey == null)
|
if (routeKey == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(routeKey));
|
throw new ArgumentNullException(nameof(routeKey));
|
||||||
|
|
|
||||||
|
|
@ -20,16 +20,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
|
||||||
RouteValueDictionary values,
|
RouteValueDictionary values,
|
||||||
RouteDirection routeDirection)
|
RouteDirection routeDirection)
|
||||||
{
|
{
|
||||||
if (httpContext == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(httpContext));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (route == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(route));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (routeKey == null)
|
if (routeKey == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(routeKey));
|
throw new ArgumentNullException(nameof(routeKey));
|
||||||
|
|
|
||||||
|
|
@ -22,16 +22,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
|
||||||
RouteValueDictionary values,
|
RouteValueDictionary values,
|
||||||
RouteDirection routeDirection)
|
RouteDirection routeDirection)
|
||||||
{
|
{
|
||||||
if (httpContext == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(httpContext));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (route == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(route));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (routeKey == null)
|
if (routeKey == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(routeKey));
|
throw new ArgumentNullException(nameof(routeKey));
|
||||||
|
|
|
||||||
|
|
@ -41,16 +41,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
|
||||||
RouteValueDictionary values,
|
RouteValueDictionary values,
|
||||||
RouteDirection routeDirection)
|
RouteDirection routeDirection)
|
||||||
{
|
{
|
||||||
if (httpContext == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(httpContext));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (route == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(route));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (routeKey == null)
|
if (routeKey == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(routeKey));
|
throw new ArgumentNullException(nameof(routeKey));
|
||||||
|
|
@ -64,6 +54,12 @@ namespace Microsoft.AspNetCore.Routing.Constraints
|
||||||
switch (routeDirection)
|
switch (routeDirection)
|
||||||
{
|
{
|
||||||
case RouteDirection.IncomingRequest:
|
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);
|
return AllowedMethods.Contains(httpContext.Request.Method, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
case RouteDirection.UrlGeneration:
|
case RouteDirection.UrlGeneration:
|
||||||
|
|
|
||||||
|
|
@ -20,16 +20,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
|
||||||
RouteValueDictionary values,
|
RouteValueDictionary values,
|
||||||
RouteDirection routeDirection)
|
RouteDirection routeDirection)
|
||||||
{
|
{
|
||||||
if (httpContext == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(httpContext));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (route == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(route));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (routeKey == null)
|
if (routeKey == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(routeKey));
|
throw new ArgumentNullException(nameof(routeKey));
|
||||||
|
|
|
||||||
|
|
@ -77,16 +77,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
|
||||||
RouteValueDictionary values,
|
RouteValueDictionary values,
|
||||||
RouteDirection routeDirection)
|
RouteDirection routeDirection)
|
||||||
{
|
{
|
||||||
if (httpContext == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(httpContext));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (route == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(route));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (routeKey == null)
|
if (routeKey == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(routeKey));
|
throw new ArgumentNullException(nameof(routeKey));
|
||||||
|
|
|
||||||
|
|
@ -20,16 +20,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
|
||||||
RouteValueDictionary values,
|
RouteValueDictionary values,
|
||||||
RouteDirection routeDirection)
|
RouteDirection routeDirection)
|
||||||
{
|
{
|
||||||
if (httpContext == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(httpContext));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (route == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(route));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (routeKey == null)
|
if (routeKey == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(routeKey));
|
throw new ArgumentNullException(nameof(routeKey));
|
||||||
|
|
|
||||||
|
|
@ -40,16 +40,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
|
||||||
RouteValueDictionary values,
|
RouteValueDictionary values,
|
||||||
RouteDirection routeDirection)
|
RouteDirection routeDirection)
|
||||||
{
|
{
|
||||||
if (httpContext == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(httpContext));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (route == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(route));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (routeKey == null)
|
if (routeKey == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(routeKey));
|
throw new ArgumentNullException(nameof(routeKey));
|
||||||
|
|
|
||||||
|
|
@ -34,16 +34,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
|
||||||
RouteValueDictionary values,
|
RouteValueDictionary values,
|
||||||
RouteDirection routeDirection)
|
RouteDirection routeDirection)
|
||||||
{
|
{
|
||||||
if (httpContext == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(httpContext));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (route == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(route));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (routeKey == null)
|
if (routeKey == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(routeKey));
|
throw new ArgumentNullException(nameof(routeKey));
|
||||||
|
|
|
||||||
|
|
@ -40,16 +40,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
|
||||||
RouteValueDictionary values,
|
RouteValueDictionary values,
|
||||||
RouteDirection routeDirection)
|
RouteDirection routeDirection)
|
||||||
{
|
{
|
||||||
if (httpContext == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(httpContext));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (route == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(route));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (routeKey == null)
|
if (routeKey == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(routeKey));
|
throw new ArgumentNullException(nameof(routeKey));
|
||||||
|
|
|
||||||
|
|
@ -34,16 +34,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
|
||||||
RouteValueDictionary values,
|
RouteValueDictionary values,
|
||||||
RouteDirection routeDirection)
|
RouteDirection routeDirection)
|
||||||
{
|
{
|
||||||
if (httpContext == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(httpContext));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (route == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(route));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (routeKey == null)
|
if (routeKey == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(routeKey));
|
throw new ArgumentNullException(nameof(routeKey));
|
||||||
|
|
|
||||||
|
|
@ -30,16 +30,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
|
||||||
RouteValueDictionary values,
|
RouteValueDictionary values,
|
||||||
RouteDirection routeDirection)
|
RouteDirection routeDirection)
|
||||||
{
|
{
|
||||||
if (httpContext == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(httpContext));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (route == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(route));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (routeKey == null)
|
if (routeKey == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(routeKey));
|
throw new ArgumentNullException(nameof(routeKey));
|
||||||
|
|
|
||||||
|
|
@ -48,16 +48,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
|
||||||
RouteValueDictionary values,
|
RouteValueDictionary values,
|
||||||
RouteDirection routeDirection)
|
RouteDirection routeDirection)
|
||||||
{
|
{
|
||||||
if (httpContext == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(httpContext));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (route == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(route));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (routeKey == null)
|
if (routeKey == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(routeKey));
|
throw new ArgumentNullException(nameof(routeKey));
|
||||||
|
|
|
||||||
|
|
@ -44,16 +44,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
|
||||||
RouteValueDictionary values,
|
RouteValueDictionary values,
|
||||||
RouteDirection routeDirection)
|
RouteDirection routeDirection)
|
||||||
{
|
{
|
||||||
if (httpContext == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(httpContext));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (route == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(route));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (routeKey == null)
|
if (routeKey == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(routeKey));
|
throw new ArgumentNullException(nameof(routeKey));
|
||||||
|
|
|
||||||
|
|
@ -24,16 +24,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
|
||||||
RouteValueDictionary values,
|
RouteValueDictionary values,
|
||||||
RouteDirection routeDirection)
|
RouteDirection routeDirection)
|
||||||
{
|
{
|
||||||
if (httpContext == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(httpContext));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (route == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(route));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (routeKey == null)
|
if (routeKey == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(routeKey));
|
throw new ArgumentNullException(nameof(routeKey));
|
||||||
|
|
|
||||||
|
|
@ -31,16 +31,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
|
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)
|
if (routeKey == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(routeKey));
|
throw new ArgumentNullException(nameof(routeKey));
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing
|
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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.FileProviders;
|
using Microsoft.Extensions.FileProviders;
|
||||||
using Microsoft.Extensions.Primitives;
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing
|
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)
|
public DefaultEndpointDataSource(IEnumerable<Endpoint> endpoints)
|
||||||
{
|
{
|
||||||
if (endpoints == null)
|
if (endpoints == null)
|
||||||
|
|
@ -23,8 +40,16 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
_endpoints.AddRange(endpoints);
|
_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;
|
public override IChangeToken GetChangeToken() => NullChangeToken.Singleton;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a read-only collection of <see cref="Endpoint"/> instances.
|
||||||
|
/// </summary>
|
||||||
public override IReadOnlyList<Endpoint> Endpoints => _endpoints;
|
public override IReadOnlyList<Endpoint> Endpoints => _endpoints;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,7 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using Microsoft.AspNetCore.Routing.Internal;
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing
|
namespace Microsoft.AspNetCore.Routing
|
||||||
|
|
@ -18,6 +16,7 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
public class DefaultInlineConstraintResolver : IInlineConstraintResolver
|
public class DefaultInlineConstraintResolver : IInlineConstraintResolver
|
||||||
{
|
{
|
||||||
private readonly IDictionary<string, Type> _inlineConstraintMap;
|
private readonly IDictionary<string, Type> _inlineConstraintMap;
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="DefaultInlineConstraintResolver"/> class.
|
/// Initializes a new instance of the <see cref="DefaultInlineConstraintResolver"/> class.
|
||||||
|
|
@ -25,11 +24,23 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
/// <param name="routeOptions">
|
/// <param name="routeOptions">
|
||||||
/// Accessor for <see cref="RouteOptions"/> containing the constraints of interest.
|
/// Accessor for <see cref="RouteOptions"/> containing the constraints of interest.
|
||||||
/// </param>
|
/// </param>
|
||||||
|
[Obsolete("This constructor is obsolete. Use DefaultInlineConstraintResolver.ctor(IOptions<RouteOptions>, IServiceProvider) instead.")]
|
||||||
public DefaultInlineConstraintResolver(IOptions<RouteOptions> routeOptions)
|
public DefaultInlineConstraintResolver(IOptions<RouteOptions> routeOptions)
|
||||||
{
|
{
|
||||||
_inlineConstraintMap = routeOptions.Value.ConstraintMap;
|
_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 />
|
/// <inheritdoc />
|
||||||
/// <example>
|
/// <example>
|
||||||
/// A typical constraint looks like the following
|
/// A typical constraint looks like the following
|
||||||
|
|
@ -45,112 +56,7 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
throw new ArgumentNullException(nameof(inlineConstraint));
|
throw new ArgumentNullException(nameof(inlineConstraint));
|
||||||
}
|
}
|
||||||
|
|
||||||
string constraintKey;
|
return ParameterPolicyActivator.ResolveParameterPolicy<IRouteConstraint>(_inlineConstraintMap, _serviceProvider, inlineConstraint, out _);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
using Microsoft.AspNetCore.Routing.Internal;
|
using Microsoft.AspNetCore.Routing.Internal;
|
||||||
using Microsoft.AspNetCore.Routing.Matchers;
|
using Microsoft.AspNetCore.Routing.Matching;
|
||||||
using Microsoft.AspNetCore.Routing.Template;
|
using Microsoft.AspNetCore.Routing.Template;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.ObjectPool;
|
using Microsoft.Extensions.ObjectPool;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
@ -17,62 +20,165 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
internal class DefaultLinkGenerator : LinkGenerator
|
internal class DefaultLinkGenerator : LinkGenerator
|
||||||
{
|
{
|
||||||
private readonly static char[] UrlQueryDelimiters = new char[] { '?', '#' };
|
private readonly static char[] UrlQueryDelimiters = new char[] { '?', '#' };
|
||||||
private readonly MatchProcessorFactory _matchProcessorFactory;
|
private readonly ParameterPolicyFactory _parameterPolicyFactory;
|
||||||
private readonly ObjectPool<UriBuildingContext> _uriBuildingContextPool;
|
private readonly ObjectPool<UriBuildingContext> _uriBuildingContextPool;
|
||||||
private readonly RouteOptions _options;
|
|
||||||
private readonly ILogger<DefaultLinkGenerator> _logger;
|
private readonly ILogger<DefaultLinkGenerator> _logger;
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
private readonly RouteOptions _options;
|
||||||
|
|
||||||
public DefaultLinkGenerator(
|
public DefaultLinkGenerator(
|
||||||
MatchProcessorFactory matchProcessorFactory,
|
ParameterPolicyFactory parameterPolicyFactory,
|
||||||
ObjectPool<UriBuildingContext> uriBuildingContextPool,
|
ObjectPool<UriBuildingContext> uriBuildingContextPool,
|
||||||
IOptions<RouteOptions> routeOptions,
|
IOptions<RouteOptions> routeOptions,
|
||||||
ILogger<DefaultLinkGenerator> logger)
|
ILogger<DefaultLinkGenerator> logger,
|
||||||
|
IServiceProvider serviceProvider)
|
||||||
{
|
{
|
||||||
_matchProcessorFactory = matchProcessorFactory;
|
_parameterPolicyFactory = parameterPolicyFactory;
|
||||||
_uriBuildingContextPool = uriBuildingContextPool;
|
_uriBuildingContextPool = uriBuildingContextPool;
|
||||||
_options = routeOptions.Value;
|
_options = routeOptions.Value;
|
||||||
_logger = logger;
|
_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)
|
return TryGetLinkByRouteValues(
|
||||||
{
|
httpContext,
|
||||||
throw new ArgumentNullException(nameof(context));
|
routeName,
|
||||||
}
|
values,
|
||||||
|
options,
|
||||||
if (TryGetLink(context, out var link))
|
out link);
|
||||||
{
|
|
||||||
return link;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new InvalidOperationException("Could not find a matching endpoint to generate a 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;
|
link = null;
|
||||||
|
|
||||||
if (context.Endpoints == null)
|
var endpoints = FindEndpoints(address);
|
||||||
|
if (endpoints == null)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var matcherEndpoints = context.Endpoints.OfType<MatcherEndpoint>();
|
foreach (var endpoint in endpoints)
|
||||||
if (!matcherEndpoints.Any())
|
|
||||||
{
|
{
|
||||||
//todo:log here
|
link = MakeLink(
|
||||||
return false;
|
httpContext,
|
||||||
}
|
endpoint,
|
||||||
|
ambientValues,
|
||||||
|
new RouteValueDictionary(explicitValues),
|
||||||
|
options);
|
||||||
|
|
||||||
foreach (var endpoint in matcherEndpoints)
|
|
||||||
{
|
|
||||||
link = GetLink(endpoint, context);
|
|
||||||
if (link != null)
|
if (link != null)
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -82,35 +188,52 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetLink(MatcherEndpoint endpoint, LinkGeneratorContext context)
|
private LinkGenerationTemplate GetTemplateInternal<TAddress>(
|
||||||
|
HttpContext httpContext,
|
||||||
|
TAddress address,
|
||||||
|
object values)
|
||||||
{
|
{
|
||||||
var templateBinder = new TemplateBinder(
|
var endpoints = FindEndpoints(address);
|
||||||
UrlEncoder.Default,
|
if (endpoints == null)
|
||||||
_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))
|
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var url = templateBinder.BindValues(templateValuesResult.AcceptedValues);
|
var ambientValues = GetAmbientValues(httpContext);
|
||||||
return Normalize(context, url);
|
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(
|
private bool MatchesConstraints(
|
||||||
HttpContext httpContext,
|
HttpContext httpContext,
|
||||||
MatcherEndpoint endpoint,
|
RouteEndpoint endpoint,
|
||||||
RouteValueDictionary routeValues)
|
RouteValueDictionary routeValues)
|
||||||
{
|
{
|
||||||
if (routeValues == null)
|
if (routeValues == null)
|
||||||
|
|
@ -118,15 +241,16 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
throw new ArgumentNullException(nameof(routeValues));
|
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 parameter = endpoint.RoutePattern.GetParameter(kvp.Key); // may be null, that's ok
|
||||||
var constraintReferences = kvp.Value;
|
var constraintReferences = kvp.Value;
|
||||||
for (var i = 0; i < constraintReferences.Count; i++)
|
for (var i = 0; i < constraintReferences.Count; i++)
|
||||||
{
|
{
|
||||||
var constraintReference = constraintReferences[i];
|
var constraintReference = constraintReferences[i];
|
||||||
var matchProcessor = _matchProcessorFactory.Create(parameter, constraintReference);
|
var parameterPolicy = _parameterPolicyFactory.Create(parameter, constraintReference);
|
||||||
if (!matchProcessor.ProcessOutbound(httpContext, routeValues))
|
if (parameterPolicy is IRouteConstraint routeConstraint
|
||||||
|
&& !routeConstraint.Match(httpContext, NullRouter.Instance, kvp.Key, routeValues, RouteDirection.UrlGeneration))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -136,13 +260,11 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
return true;
|
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 lowercaseUrls = options?.LowercaseUrls ?? _options.LowercaseUrls;
|
||||||
var lowercaseQueryStrings = context.LowercaseQueryStrings.HasValue ?
|
var lowercaseQueryStrings = options?.LowercaseQueryStrings ?? _options.LowercaseQueryStrings;
|
||||||
context.LowercaseQueryStrings.Value : _options.LowercaseQueryStrings;
|
var appendTrailingSlash = options?.AppendTrailingSlash ?? _options.AppendTrailingSlash;
|
||||||
var appendTrailingSlash = context.AppendTrailingSlash.HasValue ?
|
|
||||||
context.AppendTrailingSlash.Value : _options.AppendTrailingSlash;
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(url) && (lowercaseUrls || appendTrailingSlash))
|
if (!string.IsNullOrEmpty(url) && (lowercaseUrls || appendTrailingSlash))
|
||||||
{
|
{
|
||||||
|
|
@ -177,5 +299,36 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
|
|
||||||
return url;
|
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 System;
|
||||||
using Microsoft.AspNetCore.Routing;
|
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.Internal;
|
||||||
using Microsoft.AspNetCore.Routing.Matchers;
|
using Microsoft.AspNetCore.Routing.Matching;
|
||||||
using Microsoft.AspNetCore.Routing.Tree;
|
using Microsoft.AspNetCore.Routing.Tree;
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
@ -65,26 +62,21 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
//
|
//
|
||||||
// Default matcher implementation
|
// Default matcher implementation
|
||||||
//
|
//
|
||||||
services.TryAddSingleton<MatchProcessorFactory, DefaultMatchProcessorFactory>();
|
services.TryAddSingleton<ParameterPolicyFactory, DefaultParameterPolicyFactory>();
|
||||||
services.TryAddSingleton<MatcherFactory, DfaMatcherFactory>();
|
services.TryAddSingleton<MatcherFactory, DfaMatcherFactory>();
|
||||||
services.TryAddTransient<DfaMatcherBuilder>();
|
services.TryAddTransient<DfaMatcherBuilder>();
|
||||||
|
services.TryAddSingleton<DfaGraphWriter>();
|
||||||
|
|
||||||
// Link generation related services
|
// Link generation related services
|
||||||
services.TryAddSingleton<IEndpointFinder<string>, NameBasedEndpointFinder>();
|
services.TryAddSingleton<IEndpointFinder<RouteValuesAddress>, RouteValuesBasedEndpointFinder>();
|
||||||
services.TryAddSingleton<IEndpointFinder<RouteValuesBasedEndpointFinderContext>, RouteValuesBasedEndpointFinder>();
|
|
||||||
services.TryAddSingleton<LinkGenerator, DefaultLinkGenerator>();
|
services.TryAddSingleton<LinkGenerator, DefaultLinkGenerator>();
|
||||||
|
|
||||||
//
|
//
|
||||||
// Endpoint Selection
|
// Endpoint Selection
|
||||||
//
|
//
|
||||||
services.TryAddSingleton<EndpointSelector, EndpointConstraintEndpointSelector>();
|
services.TryAddSingleton<EndpointSelector, DefaultEndpointSelector>();
|
||||||
services.TryAddSingleton<EndpointConstraintCache>();
|
|
||||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, HttpMethodMatcherPolicy>());
|
services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, HttpMethodMatcherPolicy>());
|
||||||
|
|
||||||
// Will be cached by the EndpointSelector
|
|
||||||
services.TryAddEnumerable(
|
|
||||||
ServiceDescriptor.Transient<IEndpointConstraintProvider, DefaultEndpointConstraintProvider>());
|
|
||||||
|
|
||||||
return services;
|
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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.Primitives;
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing
|
namespace Microsoft.AspNetCore.Routing
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides a collection of <see cref="Endpoint"/> instances.
|
||||||
|
/// </summary>
|
||||||
public abstract class EndpointDataSource
|
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();
|
public abstract IChangeToken GetChangeToken();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a read-only collection of <see cref="Endpoint"/> instances.
|
||||||
|
/// </summary>
|
||||||
public abstract IReadOnlyList<Endpoint> Endpoints { get; }
|
public abstract IReadOnlyList<Endpoint> Endpoints { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,37 +3,63 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing
|
namespace Microsoft.AspNetCore.Routing
|
||||||
{
|
{
|
||||||
public sealed class EndpointFeature : IEndpointFeature, IRoutingFeature
|
public sealed class EndpointFeature : IEndpointFeature, IRouteValuesFeature, IRoutingFeature
|
||||||
{
|
{
|
||||||
private RouteData _routeData;
|
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 Endpoint Endpoint { get; set; }
|
||||||
|
|
||||||
public Func<RequestDelegate, RequestDelegate> Invoker { get; set; }
|
/// <summary>
|
||||||
|
/// Gets or sets the <see cref="RouteValueDictionary"/> associated with the currrent
|
||||||
public RouteValueDictionary Values
|
/// request.
|
||||||
|
/// </summary>
|
||||||
|
public RouteValueDictionary RouteValues
|
||||||
{
|
{
|
||||||
get => _values;
|
get => _routeValues;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_values = value;
|
_routeValues = value;
|
||||||
|
|
||||||
// RouteData will be created next get with new Values
|
// RouteData will be created next get with new Values
|
||||||
_routeData = null;
|
_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
|
RouteData IRoutingFeature.RouteData
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (_routeData == null)
|
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;
|
return _routeData;
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing
|
namespace Microsoft.AspNetCore.Routing
|
||||||
|
|
@ -31,26 +32,18 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
|
|
||||||
public async Task Invoke(HttpContext httpContext)
|
public async Task Invoke(HttpContext httpContext)
|
||||||
{
|
{
|
||||||
var feature = httpContext.Features.Get<IEndpointFeature>();
|
var endpoint = httpContext.Features.Get<IEndpointFeature>()?.Endpoint;
|
||||||
if (feature == null)
|
if (endpoint?.RequestDelegate != null)
|
||||||
{
|
{
|
||||||
var message = $"Unable to execute an endpoint because the {nameof(GlobalRoutingMiddleware)} was not run for this request. " +
|
Log.ExecutingEndpoint(_logger, endpoint);
|
||||||
$"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);
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await feature.Invoker(_next)(httpContext);
|
await endpoint.RequestDelegate(httpContext);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
Log.ExecutedEndpoint(_logger, feature.Endpoint);
|
Log.ExecutedEndpoint(_logger, endpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@ using System.Collections.Generic;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing
|
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>();
|
public IList<EndpointDataSource> DataSources { get; } = new List<EndpointDataSource>();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,14 @@ using System;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http;
|
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.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing
|
namespace Microsoft.AspNetCore.Routing
|
||||||
{
|
{
|
||||||
internal sealed class GlobalRoutingMiddleware
|
internal sealed class EndpointRoutingMiddleware
|
||||||
{
|
{
|
||||||
private readonly MatcherFactory _matcherFactory;
|
private readonly MatcherFactory _matcherFactory;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
|
@ -20,10 +21,10 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
|
|
||||||
private Task<Matcher> _initializationTask;
|
private Task<Matcher> _initializationTask;
|
||||||
|
|
||||||
public GlobalRoutingMiddleware(
|
public EndpointRoutingMiddleware(
|
||||||
MatcherFactory matcherFactory,
|
MatcherFactory matcherFactory,
|
||||||
CompositeEndpointDataSource endpointDataSource,
|
CompositeEndpointDataSource endpointDataSource,
|
||||||
ILogger<GlobalRoutingMiddleware> logger,
|
ILogger<EndpointRoutingMiddleware> logger,
|
||||||
RequestDelegate next)
|
RequestDelegate next)
|
||||||
{
|
{
|
||||||
if (matcherFactory == null)
|
if (matcherFactory == null)
|
||||||
|
|
@ -55,10 +56,6 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
public async Task Invoke(HttpContext httpContext)
|
public async Task Invoke(HttpContext httpContext)
|
||||||
{
|
{
|
||||||
var feature = new EndpointFeature();
|
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
|
// 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.
|
// 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);
|
await matcher.MatchAsync(httpContext, feature);
|
||||||
if (feature.Endpoint != null)
|
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);
|
Log.MatchSuccess(_logger, feature);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
@ -77,6 +78,15 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
await _next(httpContext);
|
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
|
// Initialization is async to avoid blocking threads while reflection and things
|
||||||
// of that nature take place.
|
// of that nature take place.
|
||||||
//
|
//
|
||||||
|
|
@ -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