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:
Ryan Nowak 2018-09-03 11:34:35 -07:00
commit 825141eeeb
269 changed files with 10470 additions and 5563 deletions

View File

@ -18,6 +18,6 @@
<PackageSigningCertName>MicrosoftNuGet</PackageSigningCertName>
<PublicSign Condition="'$(OS)' != 'Windows_NT'">true</PublicSign>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<LangVersion>7.2</LangVersion>
<LangVersion>7.3</LangVersion>
</PropertyGroup>
</Project>

View File

@ -51,7 +51,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarkapps", "benchmarka
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmarks", "benchmarkapps\Benchmarks\Benchmarks.csproj", "{91F47A60-9A78-4968-B10D-157D9BFAC37F}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Swaggatherer", "benchmarks\Swaggatherer\Swaggatherer.csproj", "{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}"
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{6824486A-3EFF-45D1-BEE8-8B137639C890}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Swaggatherer", "tools\Swaggatherer\Swaggatherer.csproj", "{B8516771-E850-4724-BEC3-63FC00C2AE57}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -165,18 +167,18 @@ Global
{91F47A60-9A78-4968-B10D-157D9BFAC37F}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{91F47A60-9A78-4968-B10D-157D9BFAC37F}.Release|x86.ActiveCfg = Release|Any CPU
{91F47A60-9A78-4968-B10D-157D9BFAC37F}.Release|x86.Build.0 = Release|Any CPU
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}.Debug|x86.ActiveCfg = Debug|Any CPU
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}.Debug|x86.Build.0 = Debug|Any CPU
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}.Release|Any CPU.Build.0 = Release|Any CPU
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}.Release|x86.ActiveCfg = Release|Any CPU
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}.Release|x86.Build.0 = Release|Any CPU
{B8516771-E850-4724-BEC3-63FC00C2AE57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B8516771-E850-4724-BEC3-63FC00C2AE57}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B8516771-E850-4724-BEC3-63FC00C2AE57}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{B8516771-E850-4724-BEC3-63FC00C2AE57}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{B8516771-E850-4724-BEC3-63FC00C2AE57}.Debug|x86.ActiveCfg = Debug|Any CPU
{B8516771-E850-4724-BEC3-63FC00C2AE57}.Debug|x86.Build.0 = Debug|Any CPU
{B8516771-E850-4724-BEC3-63FC00C2AE57}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B8516771-E850-4724-BEC3-63FC00C2AE57}.Release|Any CPU.Build.0 = Release|Any CPU
{B8516771-E850-4724-BEC3-63FC00C2AE57}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{B8516771-E850-4724-BEC3-63FC00C2AE57}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{B8516771-E850-4724-BEC3-63FC00C2AE57}.Release|x86.ActiveCfg = Release|Any CPU
{B8516771-E850-4724-BEC3-63FC00C2AE57}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -191,7 +193,7 @@ Global
{5C73140B-41F3-466F-A07B-3614E4D80DF9} = {95359B4B-4C85-4B44-A75B-0621905C4CF6}
{F3D86714-4E64-41A6-9B36-A47B3683CF5D} = {D5F39F59-5725-4127-82E7-67028D006185}
{91F47A60-9A78-4968-B10D-157D9BFAC37F} = {7F5914E2-C63F-4759-898E-462804357C90}
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D} = {D5F39F59-5725-4127-82E7-67028D006185}
{B8516771-E850-4724-BEC3-63FC00C2AE57} = {6824486A-3EFF-45D1-BEE8-8B137639C890}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {36C8D815-B7F1-479D-894B-E606FB8DECDA}

View File

@ -29,11 +29,11 @@ namespace Benchmarks
.UseKestrel();
var scenario = config["scenarios"]?.ToLower();
if (scenario == "plaintextdispatcher" || scenario == "plaintextglobalrouting")
if (scenario == "plaintextdispatcher" || scenario == "plaintextendpointrouting")
{
webHostBuilder.UseStartup<StartupUsingGlobalRouting>();
webHostBuilder.UseStartup<StartupUsingEndpointRouting>();
// for testing
webHostBuilder.UseSetting("Startup", nameof(StartupUsingGlobalRouting));
webHostBuilder.UseSetting("Startup", nameof(StartupUsingEndpointRouting));
}
else if (scenario == "plaintextrouting" || scenario == "plaintextrouter")
{
@ -44,7 +44,7 @@ namespace Benchmarks
else
{
throw new InvalidOperationException(
$"Invalid scenario '{scenario}'. Allowed scenarios are PlaintextGlobalRouting and PlaintextRouter");
$"Invalid scenario '{scenario}'. Allowed scenarios are PlaintextEndpointRouting and PlaintextRouter");
}
return webHostBuilder;

View File

@ -2,15 +2,16 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Text;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Internal;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Matchers;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Benchmarks
{
public class StartupUsingGlobalRouting
public class StartupUsingEndpointRouting
{
private static readonly byte[] _helloWorldPayload = Encoding.UTF8.GetBytes("Hello, World!");
@ -18,12 +19,10 @@ namespace Benchmarks
{
services.AddRouting();
services.Configure<EndpointOptions>(options =>
{
options.DataSources.Add(new DefaultEndpointDataSource(new[]
var endpointDataSource = new DefaultEndpointDataSource(new[]
{
new MatcherEndpoint(
invoker: (next) => (httpContext) =>
new RouteEndpoint(
requestDelegate: (httpContext) =>
{
var response = httpContext.Response;
var payloadLength = _helloWorldPayload.Length;
@ -33,17 +32,17 @@ namespace Benchmarks
return response.Body.WriteAsync(_helloWorldPayload, 0, payloadLength);
},
routePattern: RoutePatternFactory.Parse("/plaintext"),
requiredValues: new RouteValueDictionary(),
order: 0,
metadata: EndpointMetadataCollection.Empty,
displayName: "Plaintext"),
}));
});
});
services.TryAddEnumerable(ServiceDescriptor.Singleton<EndpointDataSource>(endpointDataSource));
}
public void Configure(IApplicationBuilder app)
public void Configure(Microsoft.AspNetCore.Builder.IApplicationBuilder app)
{
app.UseGlobalRouting();
app.UseEndpointRouting();
app.UseEndpoint();
}

View File

@ -22,7 +22,7 @@
"PlaintextDispatcher": {
"Path": "/plaintext"
},
"PlaintextGlobalRouting": {
"PlaintextEndpointRouting": {
"Path": "/plaintext"
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Routing.Matchers
namespace Microsoft.AspNetCore.Routing.Matching
{
public abstract class FastPathTokenizerBenchmarkBase
{

View File

@ -4,7 +4,7 @@
using System;
using BenchmarkDotNet.Attributes;
namespace Microsoft.AspNetCore.Routing.Matchers
namespace Microsoft.AspNetCore.Routing.Matching
{
public class FastPathTokenizerEmptyBenchmark : FastPathTokenizerBenchmarkBase
{

View File

@ -4,7 +4,7 @@
using System;
using BenchmarkDotNet.Attributes;
namespace Microsoft.AspNetCore.Routing.Matchers
namespace Microsoft.AspNetCore.Routing.Matching
{
public class FastPathTokenizerLargeBenchmark : FastPathTokenizerBenchmarkBase
{

View File

@ -4,7 +4,7 @@
using System;
using BenchmarkDotNet.Attributes;
namespace Microsoft.AspNetCore.Routing.Matchers
namespace Microsoft.AspNetCore.Routing.Matching
{
public class FastPathTokenizerPlaintextBenchmark : FastPathTokenizerBenchmarkBase
{

View File

@ -4,7 +4,7 @@
using System;
using BenchmarkDotNet.Attributes;
namespace Microsoft.AspNetCore.Routing.Matchers
namespace Microsoft.AspNetCore.Routing.Matching
{
public class FastPathTokenizerSmallBenchmark : FastPathTokenizerBenchmarkBase
{

View File

@ -3,10 +3,9 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using BenchmarkDotNet.Attributes;
namespace Microsoft.AspNetCore.Routing.Matchers
namespace Microsoft.AspNetCore.Routing.Matching
{
public class JumpTableMultipleEntryBenchmark
{
@ -15,12 +14,11 @@ namespace Microsoft.AspNetCore.Routing.Matchers
private JumpTable _linearSearch;
private JumpTable _dictionary;
private JumpTable _ascii;
private JumpTable _dictionaryLookup;
private JumpTable _customHashTable;
private JumpTable _trie;
private JumpTable _vectorTrie;
// All factors of 100 to support sampling
[Params(2, 4, 5, 10, 25)]
[Params(2, 5, 10, 25, 50, 100)]
public int Count;
[GlobalSetup]
@ -48,9 +46,8 @@ namespace Microsoft.AspNetCore.Routing.Matchers
_linearSearch = new LinearSearchJumpTable(0, -1, entries.ToArray());
_dictionary = new DictionaryJumpTable(0, -1, entries.ToArray());
Debug.Assert(AsciiKeyedJumpTable.TryCreate(0, -1, entries, out _ascii));
_dictionaryLookup = new DictionaryLookupJumpTable(0, -1, entries.ToArray());
_customHashTable = new CustomHashTableJumpTable(0, -1, entries.ToArray());
_trie = new ILEmitTrieJumpTable(0, -1, entries.ToArray(), vectorize: false, _dictionary);
_vectorTrie = new ILEmitTrieJumpTable(0, -1, entries.ToArray(), vectorize: true, _dictionary);
}
// This baseline is similar to SingleEntryJumpTable. We just want
@ -67,15 +64,24 @@ namespace Microsoft.AspNetCore.Routing.Matchers
var @string = strings[i];
var segment = segments[i];
destination = segment.Length == 0 ? -1 :
segment.Length != @string.Length ? 1 :
string.Compare(
if (segment.Length == 0)
{
destination = -1;
}
else if (segment.Length != @string.Length)
{
destination = 1;
}
else
{
destination = string.Compare(
@string,
segment.Start,
@string,
0,
@string.Length,
segment.Length,
StringComparison.OrdinalIgnoreCase);
}
}
return destination;
@ -112,7 +118,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
}
[Benchmark(OperationsPerInvoke = 100)]
public int Ascii()
public int Trie()
{
var strings = _strings;
var segments = _segments;
@ -120,14 +126,14 @@ namespace Microsoft.AspNetCore.Routing.Matchers
var destination = 0;
for (var i = 0; i < strings.Length; i++)
{
destination = _ascii.GetDestination(strings[i], segments[i]);
destination = _trie.GetDestination(strings[i], segments[i]);
}
return destination;
}
[Benchmark(OperationsPerInvoke = 100)]
public int DictionaryLookup()
public int VectorTrie()
{
var strings = _strings;
var segments = _segments;
@ -135,22 +141,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
var destination = 0;
for (var i = 0; i < strings.Length; i++)
{
destination = _dictionaryLookup.GetDestination(strings[i], segments[i]);
}
return destination;
}
[Benchmark(OperationsPerInvoke = 100)]
public int CustomHashTable()
{
var strings = _strings;
var segments = _segments;
var destination = 0;
for (var i = 0; i < strings.Length; i++)
{
destination = _customHashTable.GetDestination(strings[i], segments[i]);
destination = _vectorTrie.GetDestination(strings[i], segments[i]);
}
return destination;
@ -164,7 +155,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
var guid = Guid.NewGuid().ToString();
// Between 5 and 36 characters
var text = guid.Substring(0, Math.Max(5, Math.Min(count, 36)));
var text = guid.Substring(0, Math.Max(5, Math.Min(i, 36)));
if (char.IsDigit(text[0]))
{
// Convert first character to a letter.

View File

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

View File

@ -3,7 +3,7 @@
using BenchmarkDotNet.Attributes;
namespace Microsoft.AspNetCore.Routing.Matchers
namespace Microsoft.AspNetCore.Routing.Matching
{
public class JumpTableZeroEntryBenchmark
{

View File

@ -3,11 +3,12 @@
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using Microsoft.AspNetCore.Http.Features;
namespace Microsoft.AspNetCore.Routing.Matchers
namespace Microsoft.AspNetCore.Routing.Matching
{
// Generated from https://github.com/Azure/azure-rest-api-specs
public partial class MatcherAzureBenchmark : MatcherBenchmarkBase
public partial class MatcherAzureBenchmark : EndpointRoutingBenchmarkBase
{
private const int SampleCount = 100;

View File

@ -4,16 +4,16 @@
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing.Matchers
namespace Microsoft.AspNetCore.Routing.Matching
{
// This code was generated by the Swaggatherer
public partial class MatcherAzureBenchmark : MatcherBenchmarkBase
public partial class MatcherAzureBenchmark : EndpointRoutingBenchmarkBase
{
private const int EndpointCount = 5160;
private void SetupEndpoints()
{
Endpoints = new MatcherEndpoint[5160];
Endpoints = new RouteEndpoint[5160];
Endpoints[0] = CreateEndpoint("/account", "GET");
Endpoints[1] = CreateEndpoint("/analyze", "POST");
Endpoints[2] = CreateEndpoint("/apis", "GET");

View File

@ -4,11 +4,15 @@
using System;
using BenchmarkDotNet.Attributes;
namespace Microsoft.AspNetCore.Routing.Matchers
namespace Microsoft.AspNetCore.Routing.Matching
{
// Generated from https://github.com/Azure/azure-rest-api-specs
public partial class MatcherFindCandidateSetAzureBenchmark : MatcherBenchmarkBase
public partial class MatcherFindCandidateSetAzureBenchmark : EndpointRoutingBenchmarkBase
{
// SegmentCount should be max-segments + 1, but we don't have a good way to compute
// it here, so using 32 as a safe guess.
private const int SegmentCount = 32;
private const int SampleCount = 100;
private BarebonesMatcher _baseline;
@ -58,7 +62,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
var httpContext = Requests[sample];
var path = httpContext.Request.Path.Value;
Span<PathSegment> segments = stackalloc PathSegment[FastPathTokenizer.DefaultSegmentCount];
Span<PathSegment> segments = stackalloc PathSegment[SegmentCount];
var count = FastPathTokenizer.Tokenize(path, segments);
var candidates = _dfa.FindCandidateSet(httpContext, path, segments.Slice(0, count));

View File

@ -4,16 +4,16 @@
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing.Matchers
namespace Microsoft.AspNetCore.Routing.Matching
{
// This code was generated by the Swaggatherer
public partial class MatcherFindCandidateSetAzureBenchmark : MatcherBenchmarkBase
public partial class MatcherFindCandidateSetAzureBenchmark : EndpointRoutingBenchmarkBase
{
private const int EndpointCount = 3517;
private void SetupEndpoints()
{
Endpoints = new MatcherEndpoint[3517];
Endpoints = new RouteEndpoint[3517];
Endpoints[0] = CreateEndpoint("/account");
Endpoints[1] = CreateEndpoint("/analyze");
Endpoints[2] = CreateEndpoint("/apis");

View File

@ -5,12 +5,16 @@ using System;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
namespace Microsoft.AspNetCore.Routing.Matchers
namespace Microsoft.AspNetCore.Routing.Matching
{
// Generated from https://github.com/APIs-guru/openapi-directory
// Use https://editor2.swagger.io/ to convert from yaml to json-
public partial class MatcherFindCandidateSetGithubBenchmark : MatcherBenchmarkBase
public partial class MatcherFindCandidateSetGithubBenchmark : EndpointRoutingBenchmarkBase
{
// SegmentCount should be max-segments + 1, but we don't have a good way to compute
// it here, so using 32 as a safe guess.
private const int SegmentCount = 32;
private BarebonesMatcher _baseline;
private DfaMatcher _dfa;
@ -50,7 +54,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
var httpContext = Requests[i];
var path = httpContext.Request.Path.Value;
Span<PathSegment> segments = stackalloc PathSegment[FastPathTokenizer.DefaultSegmentCount];
Span<PathSegment> segments = stackalloc PathSegment[SegmentCount];
var count = FastPathTokenizer.Tokenize(path, segments);
var candidates = _dfa.FindCandidateSet(httpContext, path, segments.Slice(0, count));

View File

@ -4,16 +4,16 @@
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing.Matchers
namespace Microsoft.AspNetCore.Routing.Matching
{
// This code was generated by the Swaggatherer
public partial class MatcherFindCandidateSetGithubBenchmark : MatcherBenchmarkBase
public partial class MatcherFindCandidateSetGithubBenchmark : EndpointRoutingBenchmarkBase
{
private const int EndpointCount = 155;
private void SetupEndpoints()
{
Endpoints = new MatcherEndpoint[155];
Endpoints = new RouteEndpoint[155];
Endpoints[0] = CreateEndpoint("/emojis");
Endpoints[1] = CreateEndpoint("/events");
Endpoints[2] = CreateEndpoint("/feeds");

View File

@ -5,24 +5,27 @@ using System;
using BenchmarkDotNet.Attributes;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing.Matchers
namespace Microsoft.AspNetCore.Routing.Matching
{
public class MatcheFindCandidateSetSingleEntryBenchmark : MatcherBenchmarkBase
public class MatcherFindCandidateSetSingleEntryBenchmark : EndpointRoutingBenchmarkBase
{
// SegmentCount should be max-segments + 1
private const int SegmentCount = 2;
private TrivialMatcher _baseline;
private DfaMatcher _dfa;
[GlobalSetup]
public void Setup()
{
Endpoints = new MatcherEndpoint[1];
Endpoints = new RouteEndpoint[1];
Endpoints[0] = CreateEndpoint("/plaintext");
Requests = new HttpContext[1];
Requests[0] = new DefaultHttpContext();
Requests[0].RequestServices = CreateServices();
Requests[0].Request.Path = "/plaintext";
_baseline = (TrivialMatcher)SetupMatcher(new TrivialMatcherBuilder());
_dfa = (DfaMatcher)SetupMatcher(CreateDfaMatcherBuilder());
}
@ -51,7 +54,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
{
var httpContext = Requests[0];
var path = httpContext.Request.Path.Value;
Span<PathSegment> segments = stackalloc PathSegment[FastPathTokenizer.DefaultSegmentCount];
Span<PathSegment> segments = stackalloc PathSegment[SegmentCount];
var count = FastPathTokenizer.Tokenize(path, segments);
var candidates = _dfa.FindCandidateSet(httpContext, path, segments.Slice(0, count));

View File

@ -4,11 +4,15 @@
using System;
using BenchmarkDotNet.Attributes;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
namespace Microsoft.AspNetCore.Routing.Matchers
namespace Microsoft.AspNetCore.Routing.Matching
{
public class MatcherFindCandidateSetSmallEntryCountBenchmark : MatcherBenchmarkBase
public class MatcherFindCandidateSetSmallEntryCountBenchmark : EndpointRoutingBenchmarkBase
{
// SegmentCount should be max-segments + 1
private const int SegmentCount = 6;
private TrivialMatcher _baseline;
private DfaMatcher _dfa;
@ -29,7 +33,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
private void SetupEndpoints()
{
Endpoints = new MatcherEndpoint[10];
Endpoints = new RouteEndpoint[10];
Endpoints[0] = CreateEndpoint("/another-really-cool-entry");
Endpoints[1] = CreateEndpoint("/Some-Entry");
Endpoints[2] = CreateEndpoint("/a/path/with/more/segments");
@ -87,7 +91,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
var httpContext = Requests[0];
var path = httpContext.Request.Path.Value;
Span<PathSegment> segments = stackalloc PathSegment[FastPathTokenizer.DefaultSegmentCount];
Span<PathSegment> segments = stackalloc PathSegment[SegmentCount];
var count = FastPathTokenizer.Tokenize(path, segments);
var candidates = _dfa.FindCandidateSet(httpContext, path, segments.Slice(0, count));

View File

@ -3,12 +3,13 @@
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using Microsoft.AspNetCore.Http.Features;
namespace Microsoft.AspNetCore.Routing.Matchers
namespace Microsoft.AspNetCore.Routing.Matching
{
// Generated from https://github.com/APIs-guru/openapi-directory
// Use https://editor2.swagger.io/ to convert from yaml to json-
public partial class MatcherGithubBenchmark : MatcherBenchmarkBase
public partial class MatcherGithubBenchmark : EndpointRoutingBenchmarkBase
{
private BarebonesMatcher _baseline;
private Matcher _dfa;

View File

@ -4,16 +4,16 @@
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing.Matchers
namespace Microsoft.AspNetCore.Routing.Matching
{
// This code was generated by the Swaggatherer
public partial class MatcherGithubBenchmark : MatcherBenchmarkBase
public partial class MatcherGithubBenchmark : EndpointRoutingBenchmarkBase
{
private const int EndpointCount = 243;
private void SetupEndpoints()
{
Endpoints = new MatcherEndpoint[243];
Endpoints = new RouteEndpoint[243];
Endpoints[0] = CreateEndpoint("/emojis", "GET");
Endpoints[1] = CreateEndpoint("/events", "GET");
Endpoints[2] = CreateEndpoint("/feeds", "GET");

View File

@ -4,11 +4,12 @@
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
namespace Microsoft.AspNetCore.Routing.Matchers
namespace Microsoft.AspNetCore.Routing.Matching
{
// Just like TechEmpower Plaintext
public partial class MatcherSingleEntryBenchmark : MatcherBenchmarkBase
public partial class MatcherSingleEntryBenchmark : EndpointRoutingBenchmarkBase
{
private const int SampleCount = 100;
@ -22,7 +23,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
[GlobalSetup]
public void Setup()
{
Endpoints = new MatcherEndpoint[1];
Endpoints = new RouteEndpoint[1];
Endpoints[0] = CreateEndpoint("/plaintext");
Requests = new HttpContext[1];

View File

@ -4,25 +4,26 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
namespace Microsoft.AspNetCore.Routing.Matchers
namespace Microsoft.AspNetCore.Routing.Matching
{
// A test-only matcher implementation - used as a baseline for simpler
// perf tests. The idea with this matcher is that we can cheat on the requirements
// to establish a lower bound for perf comparisons.
internal sealed class TrivialMatcher : Matcher
{
private readonly MatcherEndpoint _endpoint;
private readonly RouteEndpoint _endpoint;
private readonly Candidate[] _candidates;
public TrivialMatcher(MatcherEndpoint endpoint)
public TrivialMatcher(RouteEndpoint endpoint)
{
_endpoint = endpoint;
_candidates = new Candidate[] { new Candidate(endpoint), };
}
public sealed override Task MatchAsync(HttpContext httpContext, IEndpointFeature feature)
public sealed override Task MatchAsync(HttpContext httpContext, EndpointFeature feature)
{
if (httpContext == null)
{
@ -38,7 +39,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
if (string.Equals(_endpoint.RoutePattern.RawText, path, StringComparison.OrdinalIgnoreCase))
{
feature.Endpoint = _endpoint;
feature.Values = new RouteValueDictionary();
feature.RouteValues = new RouteValueDictionary();
}
return Task.CompletedTask;

View File

@ -4,13 +4,13 @@
using System.Collections.Generic;
using System.Linq;
namespace Microsoft.AspNetCore.Routing.Matchers
namespace Microsoft.AspNetCore.Routing.Matching
{
internal class TrivialMatcherBuilder : MatcherBuilder
{
private readonly List<MatcherEndpoint> _endpoints = new List<MatcherEndpoint>();
private readonly List<RouteEndpoint> _endpoints = new List<RouteEndpoint>();
public override void AddEndpoint(MatcherEndpoint endpoint)
public override void AddEndpoint(RouteEndpoint endpoint)
{
_endpoints.Add(endpoint);
}

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.2</TargetFrameworks>
<TargetFramework>netcoreapp2.2</TargetFramework>
<OutputType>Exe</OutputType>
<ServerGarbageCollection>true</ServerGarbageCollection>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
@ -18,24 +18,25 @@
for perf comparisons.
-->
<ItemGroup>
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matchers\BarebonesMatcher.cs">
<Link>Matchers\BarebonesMatcher.cs</Link>
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matching\BarebonesMatcher.cs">
<Link>Matching\BarebonesMatcher.cs</Link>
</Compile>
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matchers\BarebonesMatcherBuilder.cs">
<Link>Matchers\BarebonesMatcherBuilder.cs</Link>
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matching\BarebonesMatcherBuilder.cs">
<Link>Matching\BarebonesMatcherBuilder.cs</Link>
</Compile>
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matchers\RouteMatcher.cs">
<Link>Matchers\RouteMatcher.cs</Link>
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matching\RouteMatcher.cs">
<Link>Matching\RouteMatcher.cs</Link>
</Compile>
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matchers\RouteMatcherBuilder.cs">
<Link>Matchers\RouteMatcherBuilder.cs</Link>
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matching\RouteMatcherBuilder.cs">
<Link>Matching\RouteMatcherBuilder.cs</Link>
</Compile>
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matchers\TreeRouterMatcher.cs">
<Link>Matchers\TreeRouterMatcher.cs</Link>
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matching\TreeRouterMatcher.cs">
<Link>Matching\TreeRouterMatcher.cs</Link>
</Compile>
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matchers\TreeRouterMatcherBuilder.cs">
<Link>Matchers\TreeRouterMatcherBuilder.cs</Link>
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matching\TreeRouterMatcherBuilder.cs">
<Link>Matching\TreeRouterMatcherBuilder.cs</Link>
</Compile>
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\TestObjects\TestServiceProvider.cs" Link="Matching\TestServiceProvider.cs" />
</ItemGroup>
<ItemGroup>

View File

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

View File

@ -38,9 +38,12 @@
<MoqPackageVersion>4.7.49</MoqPackageVersion>
<NETStandardLibrary20PackageVersion>2.0.3</NETStandardLibrary20PackageVersion>
<NewtonsoftJsonPackageVersion>11.0.2</NewtonsoftJsonPackageVersion>
<XunitAnalyzersPackageVersion>0.9.0</XunitAnalyzersPackageVersion>
<SystemReflectionEmitLightweightPackageVersion>4.3.0</SystemReflectionEmitLightweightPackageVersion>
<SystemReflectionEmitPackageVersion>4.3.0</SystemReflectionEmitPackageVersion>
<XunitAnalyzersPackageVersion>0.10.0</XunitAnalyzersPackageVersion>
<XunitPackageVersion>2.3.1</XunitPackageVersion>
<XunitRunnerVisualStudioPackageVersion>2.4.0-rc.1.build4038</XunitRunnerVisualStudioPackageVersion>
<XunitRunnerVisualStudioPackageVersion>2.4.0</XunitRunnerVisualStudioPackageVersion>
</PropertyGroup>
<Import Project="$(DotNetPackageVersionPropsPath)" Condition=" '$(DotNetPackageVersionPropsPath)' != '' " />
<PropertyGroup Label="Package Versions: Pinned" />
</Project>

View File

@ -12,4 +12,8 @@
<DotNetCoreRuntime Include="$(MicrosoftNETCoreApp21PackageVersion)" />
<DotNetCoreRuntime Include="$(MicrosoftNETCoreApp22PackageVersion)" />
</ItemGroup>
<PropertyGroup>
<EnableBenchmarkValidation>true</EnableBenchmarkValidation>
</PropertyGroup>
</Project>

View File

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

View File

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

View File

@ -9,8 +9,8 @@ namespace RoutingSample.Web
{
public class Program
{
public static readonly string GlobalRoutingScenario = "globalrouting";
public static readonly string RouterScenario = "router";
public const string EndpointRoutingScenario = "endpointrouting";
public const string RouterScenario = "router";
public static void Main(string[] args)
{
@ -25,7 +25,7 @@ namespace RoutingSample.Web
if (args.Length == 0)
{
Console.WriteLine("Choose a sample to run:");
Console.WriteLine($"1. {GlobalRoutingScenario}");
Console.WriteLine($"1. {EndpointRoutingScenario}");
Console.WriteLine($"2. {RouterScenario}");
Console.WriteLine();
@ -40,18 +40,18 @@ namespace RoutingSample.Web
switch (scenario)
{
case "1":
case "globalrouting":
startupType = typeof(UseGlobalRoutingStartup);
case EndpointRoutingScenario:
startupType = typeof(UseEndpointRoutingStartup);
break;
case "2":
case "router":
case RouterScenario:
startupType = typeof(UseRouterStartup);
break;
default:
Console.WriteLine($"unknown scenario {scenario}");
Console.WriteLine($"usage: dotnet run -- ({GlobalRoutingScenario}|{RouterScenario})");
Console.WriteLine($"usage: dotnet run -- ({EndpointRoutingScenario}|{RouterScenario})");
throw new InvalidOperationException();
}

View File

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

View File

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

View File

@ -1,24 +1,49 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Diagnostics;
namespace Microsoft.AspNetCore.Routing
namespace Microsoft.AspNetCore.Http
{
[DebuggerDisplay("{DisplayName,nq}")]
public abstract class Endpoint
/// <summary>
/// Respresents a logical endpoint in an application.
/// </summary>
public class Endpoint
{
protected Endpoint(
/// <summary>
/// Creates a new instance of <see cref="Endpoint"/>.
/// </summary>
/// <param name="requestDelegate">The delegate used to process requests for the endpoint.</param>
/// <param name="metadata">
/// The endpoint <see cref="EndpointMetadataCollection"/>. May be null.
/// </param>
/// <param name="displayName">
/// The informational display name of the endpoint. May be null.
/// </param>
public Endpoint(
RequestDelegate requestDelegate,
EndpointMetadataCollection metadata,
string displayName)
{
// All are allowed to be null
RequestDelegate = requestDelegate;
Metadata = metadata ?? EndpointMetadataCollection.Empty;
DisplayName = displayName;
}
/// <summary>
/// Gets the informational display name of this endpoint.
/// </summary>
public string DisplayName { get; }
/// <summary>
/// Gets the collection of metadata associated with this endpoint.
/// </summary>
public EndpointMetadataCollection Metadata { get; }
/// <summary>
/// Gets the delegate used to process requests for the endpoint.
/// </summary>
public RequestDelegate RequestDelegate { get; }
public override string ToString() => DisplayName ?? base.ToString();
}
}

View File

@ -3,17 +3,35 @@
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
namespace Microsoft.AspNetCore.Routing
namespace Microsoft.AspNetCore.Http
{
public class EndpointMetadataCollection : IReadOnlyList<object>
/// <summary>
/// A collection of arbitrary metadata associated with an endpoint.
/// </summary>
/// <remarks>
/// <see cref="EndpointMetadataCollection"/> instances contain a list of metadata items
/// of arbitrary types. The metadata items are stored as an ordered collection with
/// items arranged in ascending order of precedence.
/// </remarks>
public sealed class EndpointMetadataCollection : IReadOnlyList<object>
{
/// <summary>
/// An empty <see cref="EndpointMetadataCollection"/>.
/// </summary>
public static readonly EndpointMetadataCollection Empty = new EndpointMetadataCollection(Array.Empty<object>());
private readonly object[] _items;
private readonly ConcurrentDictionary<Type, object[]> _cache;
/// <summary>
/// Creates a new <see cref="EndpointMetadataCollection"/>.
/// </summary>
/// <param name="items">The metadata items.</param>
public EndpointMetadataCollection(IEnumerable<object> items)
{
if (items == null)
@ -22,80 +40,161 @@ namespace Microsoft.AspNetCore.Routing
}
_items = items.ToArray();
_cache = new ConcurrentDictionary<Type, object[]>();
}
/// <summary>
/// Creates a new <see cref="EndpointMetadataCollection"/>.
/// </summary>
/// <param name="items">The metadata items.</param>
public EndpointMetadataCollection(params object[] items)
: this((IEnumerable<object>)items)
{
}
/// <summary>
/// Gets the item at <paramref name="index"/>.
/// </summary>
/// <param name="index">The index of the item to retrieve.</param>
/// <returns>The item at <paramref name="index"/>.</returns>
public object this[int index] => _items[index];
/// <summary>
/// Gets the count of metadata items.
/// </summary>
public int Count => _items.Length;
/// <summary>
/// Gets the most significant metadata item of type <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The type of metadata to retrieve.</typeparam>
/// <returns>
/// The most significant metadata of type <typeparamref name="T"/> or <c>null</c>.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public T GetMetadata<T>() where T : class
{
for (var i = _items.Length - 1; i >= 0; i--)
if (_cache.TryGetValue(typeof(T), out var result))
{
var item = _items[i] as T;
if (item != null)
{
return item;
}
var length = result.Length;
return length > 0 ? (T)result[length - 1] : default;
}
return default;
return GetMetadataSlow<T>();
}
private T GetMetadataSlow<T>() where T : class
{
var array = GetOrderedMetadataSlow<T>();
var length = array.Length;
return length > 0 ? array[length - 1] : default;
}
/// <summary>
/// Gets the metadata items of type <typeparamref name="T"/> in ascending
/// order of precedence.
/// </summary>
/// <typeparam name="T">The type of metadata.</typeparam>
/// <returns>A sequence of metadata items of <typeparamref name="T"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public IEnumerable<T> GetOrderedMetadata<T>() where T : class
{
for (var i = 0; i < _items.Length; i++)
if (_cache.TryGetValue(typeof(T), out var result))
{
var item = _items[i] as T;
if (item != null)
{
yield return item;
}
return (T[])result;
}
return GetOrderedMetadataSlow<T>();
}
private T[] GetOrderedMetadataSlow<T>() where T : class
{
var items = new List<T>();
for (var i = 0; i < _items.Length; i++)
{
if (_items[i] is T item)
{
items.Add(item);
}
}
var array = items.ToArray();
_cache.TryAdd(typeof(T), array);
return array;
}
/// <summary>
/// Gets an <see cref="IEnumerator"/> of all metadata items.
/// </summary>
/// <returns>An <see cref="IEnumerator"/> of all metadata items.</returns>
public Enumerator GetEnumerator() => new Enumerator(this);
/// <summary>
/// Gets an <see cref="IEnumerator{Object}"/> of all metadata items.
/// </summary>
/// <returns>An <see cref="IEnumerator{Object}"/> of all metadata items.</returns>
IEnumerator<object> IEnumerable<object>.GetEnumerator() => GetEnumerator();
/// <summary>
/// Gets an <see cref="IEnumerator"/> of all metadata items.
/// </summary>
/// <returns>An <see cref="IEnumerator"/> of all metadata items.</returns>
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
/// <summary>
/// Enumerates the elements of an <see cref="EndpointMetadataCollection"/>.
/// </summary>
public struct Enumerator : IEnumerator<object>
{
// Intentionally not readonly to prevent defensive struct copies
private object[] _items;
private int _index;
private object _current;
internal Enumerator(EndpointMetadataCollection collection)
{
_items = collection._items;
_index = 0;
_current = null;
Current = null;
}
public object Current => _current;
/// <summary>
/// Gets the element at the current position of the enumerator
/// </summary>
public object Current { get; private set; }
/// <summary>
/// Releases all resources used by the <see cref="Enumerator"/>.
/// </summary>
public void Dispose()
{
}
/// <summary>
/// Advances the enumerator to the next element of the <see cref="Enumerator"/>.
/// </summary>
/// <returns>
/// <c>true</c> if the enumerator was successfully advanced to the next element;
/// <c>false</c> if the enumerator has passed the end of the collection.
/// </returns>
public bool MoveNext()
{
if (_index < _items.Length)
{
_current = _items[_index++];
Current = _items[_index++];
return true;
}
_current = null;
Current = null;
return false;
}
/// <summary>
/// Sets the enumerator to its initial position, which is before the first element in the collection.
/// </summary>
public void Reset()
{
_index = 0;
_current = null;
Current = null;
}
}
}

View File

@ -1,17 +1,18 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing
namespace Microsoft.AspNetCore.Http.Features
{
/// <summary>
/// A feature interface for endpoint routing. Use <see cref="HttpContext.Features"/>
/// to access an instance associated with the current request.
/// </summary>
public interface IEndpointFeature
{
/// <summary>
/// Gets or sets the selected <see cref="Http.Endpoint"/> for the current
/// request.
/// </summary>
Endpoint Endpoint { get; set; }
Func<RequestDelegate, RequestDelegate> Invoker { get; set; }
RouteValueDictionary Values { get; set; }
}
}

View File

@ -1,11 +1,12 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Routing
{
public interface INameMetadata
/// <summary>
/// A marker interface for types that are associated with route parameters.
/// </summary>
public interface IParameterPolicy
{
string Name { get; }
}
}

View File

@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Routing
/// Defines the contract that a class must implement in order to check whether a URL parameter
/// value is valid for a constraint.
/// </summary>
public interface IRouteConstraint
public interface IRouteConstraint : IParameterPolicy
{
/// <summary>
/// Determines whether the URL parameter contains a valid value for this constraint.

View File

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

View File

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

View File

@ -1,13 +1,463 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing
{
/// <summary>
/// Defines a contract to generate URLs to endpoints.
/// </summary>
public abstract class LinkGenerator
{
public abstract bool TryGetLink(LinkGeneratorContext context, out string link);
/// <summary>
/// Generates a URL with an absolute path from the specified route values.
/// </summary>
/// <param name="values">An object that contains route values.</param>
/// <returns>The generated URL.</returns>
public string GetLink(object values)
{
return GetLink(httpContext: null, routeName: null, values, options: null);
}
public abstract string GetLink(LinkGeneratorContext context);
/// <summary>
/// Generates a URL with an absolute path from the specified route values and link options.
/// </summary>
/// <param name="values">An object that contains route values.</param>
/// <param name="options">The <see cref="LinkOptions"/>.</param>
/// <returns>The generated URL.</returns>
public string GetLink(object values, LinkOptions options)
{
return GetLink(httpContext: null, routeName: null, values, options);
}
/// <summary>
/// Generates a URL with an absolute path from the specified route values.
/// A return value indicates whether the operation succeeded.
/// </summary>
/// <param name="values">An object that contains route values.</param>
/// <param name="link">The generated URL.</param>
/// <returns><c>true</c> if a URL was generated successfully; otherwise, <c>false</c>.</returns>
public bool TryGetLink(object values, out string link)
{
return TryGetLink(httpContext: null, routeName: null, values, options: null, out link);
}
/// <summary>
/// Generates a URL with an absolute path from the specified route values and link options.
/// A return value indicates whether the operation succeeded.
/// </summary>
/// <param name="values">An object that contains route values.</param>
/// <param name="options">The <see cref="LinkOptions"/>.</param>
/// <param name="link">The generated URL.</param>
/// <returns><c>true</c> if a URL was generated successfully; otherwise, <c>false</c>.</returns>
public bool TryGetLink(object values, LinkOptions options, out string link)
{
return TryGetLink(httpContext: null, routeName: null, values, options, out link);
}
/// <summary>
/// Generates a URL with an absolute path from the specified route values.
/// </summary>
/// <param name="httpContext">The <see cref="HttpContext"/> associated with current request.</param>
/// <param name="values">An object that contains route values.</param>
/// <returns>The generated URL.</returns>
public string GetLink(HttpContext httpContext, object values)
{
return GetLink(httpContext, routeName: null, values, options: null);
}
/// <summary>
/// Generates a URL with an absolute path from the specified route values.
/// A return value indicates whether the operation succeeded.
/// </summary>
/// <param name="httpContext">The <see cref="HttpContext"/> associated with current request.</param>
/// <param name="values">An object that contains route values.</param>
/// <param name="link">The generated URL.</param>
/// <returns><c>true</c> if a URL was generated successfully; otherwise, <c>false</c>.</returns>
public bool TryGetLink(HttpContext httpContext, object values, out string link)
{
return TryGetLink(httpContext, routeName: null, values, options: null, out link);
}
/// <summary>
/// Generates a URL with an absolute path from the specified route values and link options.
/// </summary>
/// <param name="httpContext">The <see cref="HttpContext"/> associated with current request.</param>
/// <param name="values">An object that contains route values.</param>
/// <param name="options">The <see cref="LinkOptions"/>.</param>
/// <returns>The generated URL.</returns>
public string GetLink(HttpContext httpContext, object values, LinkOptions options)
{
return GetLink(httpContext, routeName: null, values, options);
}
/// <summary>
/// Generates a URL with an absolute path from the specified route values and link options.
/// A return value indicates whether the operation succeeded.
/// </summary>
/// <param name="httpContext">The <see cref="HttpContext"/> associated with current request.</param>
/// <param name="values">An object that contains route values.</param>
/// <param name="options">The <see cref="LinkOptions"/>.</param>
/// <param name="link">The generated URL.</param>
/// <returns><c>true</c> if a URL was generated successfully; otherwise, <c>false</c>.</returns>
public bool TryGetLink(HttpContext httpContext, object values, LinkOptions options, out string link)
{
return TryGetLink(httpContext, routeName: null, values, options, out link);
}
/// <summary>
/// Generates a URL with an absolute path from the specified route name and route values.
/// </summary>
/// <param name="routeName">The name of the route to generate the URL to.</param>
/// <param name="values">An object that contains route values.</param>
/// <returns>The generated URL.</returns>
public string GetLink(string routeName, object values)
{
return GetLink(httpContext: null, routeName, values, options: null);
}
/// <summary>
/// Generates a URL with an absolute path from the specified route name and route values.
/// A return value indicates whether the operation succeeded.
/// </summary>
/// <param name="routeName">The name of the route to generate the URL to.</param>
/// <param name="values">An object that contains route values.</param>
/// <param name="link">The generated URL.</param>
/// <returns><c>true</c> if a URL was generated successfully; otherwise, <c>false</c>.</returns>
public bool TryGetLink(string routeName, object values, out string link)
{
return TryGetLink(httpContext: null, routeName, values, options: null, out link);
}
/// <summary>
/// Generates a URL with an absolute path from the specified route name and route values.
/// </summary>
/// <param name="routeName">The name of the route to generate the URL to.</param>
/// <param name="values">An object that contains route values.</param>
/// <param name="options">The <see cref="LinkOptions"/>.</param>
/// <returns>The generated URL.</returns>
public string GetLink(string routeName, object values, LinkOptions options)
{
return GetLink(httpContext: null, routeName, values, options);
}
/// <summary>
/// Generates a URL with an absolute path from the specified route name, route values and link options.
/// A return value indicates whether the operation succeeded.
/// </summary>
/// <param name="routeName">The name of the route to generate the URL to.</param>
/// <param name="values">An object that contains route values.</param>
/// <param name="options">The <see cref="LinkOptions"/>.</param>
/// <param name="link">The generated URL.</param>
/// <returns><c>true</c> if a URL was generated successfully; otherwise, <c>false</c>.</returns>
public bool TryGetLink(string routeName, object values, LinkOptions options, out string link)
{
return TryGetLink(httpContext: null, routeName, values, options, out link);
}
/// <summary>
/// Generates a URL with an absolute path from the specified route name and route values.
/// </summary>
/// <param name="routeName">The name of the route to generate the URL to.</param>
/// <param name="httpContext">The <see cref="HttpContext"/> associated with current request.</param>
/// <param name="values">An object that contains route values.</param>
/// <returns>The generated URL.</returns>
public string GetLink(HttpContext httpContext, string routeName, object values)
{
return GetLink(httpContext, routeName, values, options: null);
}
/// <summary>
/// Generates a URL with an absolute path from the specified route name and route values.
/// A return value indicates whether the operation succeeded.
/// </summary>
/// <param name="httpContext">The <see cref="HttpContext"/> associated with current request.</param>
/// <param name="routeName">The name of the route to generate the URL to.</param>
/// <param name="values">An object that contains route values.</param>
/// <param name="link">The generated URL.</param>
/// <returns><c>true</c> if a URL was generated successfully; otherwise, <c>false</c>.</returns>
public bool TryGetLink(HttpContext httpContext, string routeName, object values, out string link)
{
return TryGetLink(httpContext, routeName, values, options: null, out link);
}
/// <summary>
/// Generates a URL with an absolute path from the specified route name, route values and link options.
/// </summary>
/// <param name="routeName">The name of the route to generate the URL to.</param>
/// <param name="httpContext">The <see cref="HttpContext"/> associated with current request.</param>
/// <param name="values">An object that contains route values.</param>
/// <param name="options">The <see cref="LinkOptions"/>.</param>
/// <returns>The generated URL.</returns>
public string GetLink(HttpContext httpContext, string routeName, object values, LinkOptions options)
{
if (TryGetLink(httpContext, routeName, values, options, out var link))
{
return link;
}
throw new InvalidOperationException("Could not find a matching endpoint to generate a link.");
}
/// <summary>
/// Generates a URL with an absolute path from the specified route name, route values and link options.
/// A return value indicates whether the operation succeeded.
/// </summary>
/// <param name="httpContext">The <see cref="HttpContext"/> associated with current request.</param>
/// <param name="routeName">The name of the route to generate the URL to.</param>
/// <param name="values">An object that contains route values.</param>
/// <param name="options">The <see cref="LinkOptions"/>.</param>
/// <param name="link">The generated URL.</param>
/// <returns><c>true</c> if a URL was generated successfully; otherwise, <c>false</c>.</returns>
public abstract bool TryGetLink(
HttpContext httpContext,
string routeName,
object values,
LinkOptions options,
out string link);
/// <summary>
/// Generates a URL with an absolute path from the specified lookup information and route values.
/// This lookup information is used to find endpoints using a registered 'IEndpointFinder&lt;TAddress&gt;'.
/// </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&lt;TAddress&gt;'.
/// 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&lt;TAddress&gt;'.
/// </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&lt;TAddress&gt;'.
/// 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&lt;TAddress&gt;'.
/// </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&lt;TAddress&gt;'.
/// 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&lt;TAddress&gt;'.
/// </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&lt;TAddress&gt;'.
/// 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&lt;TAddress&gt;'.
/// </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&lt;TAddress&gt;'.
/// </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);
}
}

View File

@ -1,21 +1,10 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing
{
public class LinkGeneratorContext
public class LinkOptions
{
public HttpContext HttpContext { get; set; }
public IEnumerable<Endpoint> Endpoints { get; set; }
public RouteValueDictionary ExplicitValues { get; set; }
public RouteValueDictionary AmbientValues { get; set; }
/// <summary>
/// Gets or sets a value indicating whether all generated paths URLs are lower-case.
/// Use <see cref="LowercaseQueryStrings" /> to configure the behavior for query strings.

View File

@ -222,13 +222,7 @@ namespace Microsoft.AspNetCore.Routing
}
}
IEnumerable<string> IReadOnlyDictionary<string, object>.Keys
{
get
{
return Keys;
}
}
IEnumerable<string> IReadOnlyDictionary<string, object>.Keys => Keys;
/// <inheritdoc />
public ICollection<object> Values
@ -248,13 +242,7 @@ namespace Microsoft.AspNetCore.Routing
}
}
IEnumerable<object> IReadOnlyDictionary<string, object>.Values
{
get
{
return Values;
}
}
IEnumerable<object> IReadOnlyDictionary<string, object>.Values => Values;
/// <inheritdoc />
void ICollection<KeyValuePair<string, object>>.Add(KeyValuePair<string, object> item)
@ -565,7 +553,7 @@ namespace Microsoft.AspNetCore.Routing
internal class PropertyStorage
{
private static readonly PropertyCache _propertyCache = new PropertyCache();
private static readonly ConcurrentDictionary<Type, PropertyHelper[]> _propertyCache = new ConcurrentDictionary<Type, PropertyHelper[]>();
public readonly object Value;
public readonly PropertyHelper[] Properties;
@ -606,9 +594,5 @@ namespace Microsoft.AspNetCore.Routing
}
}
}
private class PropertyCache : ConcurrentDictionary<Type, PropertyHelper[]>
{
}
}
}

View File

@ -7,15 +7,16 @@ using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using Microsoft.AspNetCore.Routing.EndpointConstraints;
using Microsoft.AspNetCore.Routing.Matchers;
using Microsoft.AspNetCore.Routing.Metadata;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Routing
{
/// <summary>
/// Represents an <see cref="EndpointDataSource"/> whose values come from a collection of <see cref="EndpointDataSource"/> instances.
/// </summary>
[DebuggerDisplay("{DebuggerDisplayString,nq}")]
public class CompositeEndpointDataSource : EndpointDataSource
public sealed class CompositeEndpointDataSource : EndpointDataSource
{
private readonly EndpointDataSource[] _dataSources;
private readonly object _lock;
@ -35,12 +36,20 @@ namespace Microsoft.AspNetCore.Routing
_lock = new object();
}
/// <summary>
/// Gets a <see cref="IChangeToken"/> used to signal invalidation of cached <see cref="Endpoint"/>
/// instances.
/// </summary>
/// <returns>The <see cref="IChangeToken"/>.</returns>
public override IChangeToken GetChangeToken()
{
EnsureInitialized();
return _consumerChangeToken;
}
/// <summary>
/// Returns a read-only collection of <see cref="Endpoint"/> instances.
/// </summary>
public override IReadOnlyList<Endpoint> Endpoints
{
get
@ -123,37 +132,57 @@ namespace Microsoft.AspNetCore.Routing
var sb = new StringBuilder();
foreach (var endpoint in _endpoints)
{
if (endpoint is MatcherEndpoint matcherEndpoint)
if (endpoint is RouteEndpoint routeEndpoint)
{
var template = matcherEndpoint.RoutePattern.RawText;
var template = routeEndpoint.RoutePattern.RawText;
template = string.IsNullOrEmpty(template) ? "\"\"" : template;
sb.Append(template);
var requiredValues = matcherEndpoint.RequiredValues.Select(kvp => $"{kvp.Key} = \"{kvp.Value ?? "null"}\"");
sb.Append(", Required Values: new { ");
sb.Append(string.Join(", ", requiredValues));
sb.Append(", Defaults: new { ");
sb.Append(string.Join(", ", FormatValues(routeEndpoint.RoutePattern.Defaults)));
sb.Append(" }");
sb.Append(", Order:");
sb.Append(matcherEndpoint.Order);
var routeValuesAddressMetadata = routeEndpoint.Metadata.GetMetadata<IRouteValuesAddressMetadata>();
sb.Append(", Route Name: ");
sb.Append(routeValuesAddressMetadata?.Name);
if (routeValuesAddressMetadata?.RequiredValues != null)
{
sb.Append(", Required Values: new { ");
sb.Append(string.Join(", ", FormatValues(routeValuesAddressMetadata.RequiredValues)));
sb.Append(" }");
}
sb.Append(", Order: ");
sb.Append(routeEndpoint.Order);
var httpMethodMetadata = matcherEndpoint.Metadata.GetMetadata<IHttpMethodMetadata>();
var httpMethodMetadata = routeEndpoint.Metadata.GetMetadata<IHttpMethodMetadata>();
if (httpMethodMetadata != null)
{
foreach (var httpMethod in httpMethodMetadata.HttpMethods)
{
sb.Append(", Http Methods: ");
sb.Append(string.Join(", ", httpMethod));
}
sb.Append(", Http Methods: ");
sb.Append(string.Join(", ", httpMethodMetadata.HttpMethods));
}
sb.Append(", Display Name: ");
sb.Append(routeEndpoint.DisplayName);
sb.AppendLine();
}
else
{
sb.Append("Non-MatcherEndpoint. DisplayName:");
sb.Append("Non-RouteEndpoint. DisplayName:");
sb.AppendLine(endpoint.DisplayName);
}
}
return sb.ToString();
IEnumerable<string> FormatValues(IEnumerable<KeyValuePair<string, object>> values)
{
return values.Select(
kvp =>
{
var value = "null";
if (kvp.Value != null)
{
value = "\"" + kvp.Value.ToString() + "\"";
}
return kvp.Key + " = " + value;
});
}
}
}
}

View File

@ -20,16 +20,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
RouteValueDictionary values,
RouteDirection routeDirection)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
if (route == null)
{
throw new ArgumentNullException(nameof(route));
}
if (routeKey == null)
{
throw new ArgumentNullException(nameof(routeKey));

View File

@ -39,16 +39,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
RouteValueDictionary values,
RouteDirection routeDirection)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
if (route == null)
{
throw new ArgumentNullException(nameof(route));
}
if (routeKey == null)
{
throw new ArgumentNullException(nameof(routeKey));

View File

@ -26,16 +26,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
RouteValueDictionary values,
RouteDirection routeDirection)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
if (route == null)
{
throw new ArgumentNullException(nameof(route));
}
if (routeKey == null)
{
throw new ArgumentNullException(nameof(routeKey));

View File

@ -20,16 +20,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
RouteValueDictionary values,
RouteDirection routeDirection)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
if (route == null)
{
throw new ArgumentNullException(nameof(route));
}
if (routeKey == null)
{
throw new ArgumentNullException(nameof(routeKey));

View File

@ -20,16 +20,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
RouteValueDictionary values,
RouteDirection routeDirection)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
if (route == null)
{
throw new ArgumentNullException(nameof(route));
}
if (routeKey == null)
{
throw new ArgumentNullException(nameof(routeKey));

View File

@ -20,16 +20,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
RouteValueDictionary values,
RouteDirection routeDirection)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
if (route == null)
{
throw new ArgumentNullException(nameof(route));
}
if (routeKey == null)
{
throw new ArgumentNullException(nameof(routeKey));

View File

@ -22,16 +22,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
RouteValueDictionary values,
RouteDirection routeDirection)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
if (route == null)
{
throw new ArgumentNullException(nameof(route));
}
if (routeKey == null)
{
throw new ArgumentNullException(nameof(routeKey));

View File

@ -41,16 +41,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
RouteValueDictionary values,
RouteDirection routeDirection)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
if (route == null)
{
throw new ArgumentNullException(nameof(route));
}
if (routeKey == null)
{
throw new ArgumentNullException(nameof(routeKey));
@ -64,6 +54,12 @@ namespace Microsoft.AspNetCore.Routing.Constraints
switch (routeDirection)
{
case RouteDirection.IncomingRequest:
// Only required for constraining incoming requests
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
return AllowedMethods.Contains(httpContext.Request.Method, StringComparer.OrdinalIgnoreCase);
case RouteDirection.UrlGeneration:

View File

@ -20,16 +20,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
RouteValueDictionary values,
RouteDirection routeDirection)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
if (route == null)
{
throw new ArgumentNullException(nameof(route));
}
if (routeKey == null)
{
throw new ArgumentNullException(nameof(routeKey));

View File

@ -77,16 +77,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
RouteValueDictionary values,
RouteDirection routeDirection)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
if (route == null)
{
throw new ArgumentNullException(nameof(route));
}
if (routeKey == null)
{
throw new ArgumentNullException(nameof(routeKey));

View File

@ -20,16 +20,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
RouteValueDictionary values,
RouteDirection routeDirection)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
if (route == null)
{
throw new ArgumentNullException(nameof(route));
}
if (routeKey == null)
{
throw new ArgumentNullException(nameof(routeKey));

View File

@ -40,16 +40,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
RouteValueDictionary values,
RouteDirection routeDirection)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
if (route == null)
{
throw new ArgumentNullException(nameof(route));
}
if (routeKey == null)
{
throw new ArgumentNullException(nameof(routeKey));

View File

@ -34,16 +34,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
RouteValueDictionary values,
RouteDirection routeDirection)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
if (route == null)
{
throw new ArgumentNullException(nameof(route));
}
if (routeKey == null)
{
throw new ArgumentNullException(nameof(routeKey));

View File

@ -40,16 +40,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
RouteValueDictionary values,
RouteDirection routeDirection)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
if (route == null)
{
throw new ArgumentNullException(nameof(route));
}
if (routeKey == null)
{
throw new ArgumentNullException(nameof(routeKey));

View File

@ -34,16 +34,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
RouteValueDictionary values,
RouteDirection routeDirection)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
if (route == null)
{
throw new ArgumentNullException(nameof(route));
}
if (routeKey == null)
{
throw new ArgumentNullException(nameof(routeKey));

View File

@ -30,16 +30,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
RouteValueDictionary values,
RouteDirection routeDirection)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
if (route == null)
{
throw new ArgumentNullException(nameof(route));
}
if (routeKey == null)
{
throw new ArgumentNullException(nameof(routeKey));

View File

@ -48,16 +48,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
RouteValueDictionary values,
RouteDirection routeDirection)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
if (route == null)
{
throw new ArgumentNullException(nameof(route));
}
if (routeKey == null)
{
throw new ArgumentNullException(nameof(routeKey));

View File

@ -44,16 +44,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
RouteValueDictionary values,
RouteDirection routeDirection)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
if (route == null)
{
throw new ArgumentNullException(nameof(route));
}
if (routeKey == null)
{
throw new ArgumentNullException(nameof(routeKey));

View File

@ -24,16 +24,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
RouteValueDictionary values,
RouteDirection routeDirection)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
if (route == null)
{
throw new ArgumentNullException(nameof(route));
}
if (routeKey == null)
{
throw new ArgumentNullException(nameof(routeKey));

View File

@ -31,16 +31,6 @@ namespace Microsoft.AspNetCore.Routing.Constraints
/// <inheritdoc />
public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
if (route == null)
{
throw new ArgumentNullException(nameof(route));
}
if (routeKey == null)
{
throw new ArgumentNullException(nameof(routeKey));

View File

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Threading;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing
{

View File

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

View File

@ -3,15 +3,32 @@
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Routing
{
public class DefaultEndpointDataSource : EndpointDataSource
/// <summary>
/// Provides a collection of <see cref="Endpoint"/> instances.
/// </summary>
public sealed class DefaultEndpointDataSource : EndpointDataSource
{
private readonly List<Endpoint> _endpoints;
private readonly List<Endpoint> _endpoints;
/// <summary>
/// Initializes a new instance of the <see cref="DefaultEndpointDataSource" /> class.
/// </summary>
/// <param name="endpoints">The <see cref="Endpoint"/> instances that the data source will return.</param>
public DefaultEndpointDataSource(params Endpoint[] endpoints)
: this((IEnumerable<Endpoint>) endpoints)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="DefaultEndpointDataSource" /> class.
/// </summary>
/// <param name="endpoints">The <see cref="Endpoint"/> instances that the data source will return.</param>
public DefaultEndpointDataSource(IEnumerable<Endpoint> endpoints)
{
if (endpoints == null)
@ -23,8 +40,16 @@ namespace Microsoft.AspNetCore.Routing
_endpoints.AddRange(endpoints);
}
/// <summary>
/// Gets a <see cref="IChangeToken"/> used to signal invalidation of cached <see cref="Endpoint"/>
/// instances.
/// </summary>
/// <returns>The <see cref="IChangeToken"/>.</returns>
public override IChangeToken GetChangeToken() => NullChangeToken.Singleton;
/// <summary>
/// Returns a read-only collection of <see cref="Endpoint"/> instances.
/// </summary>
public override IReadOnlyList<Endpoint> Endpoints => _endpoints;
}
}

View File

@ -3,9 +3,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Routing.Internal;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Routing
@ -18,6 +16,7 @@ namespace Microsoft.AspNetCore.Routing
public class DefaultInlineConstraintResolver : IInlineConstraintResolver
{
private readonly IDictionary<string, Type> _inlineConstraintMap;
private readonly IServiceProvider _serviceProvider;
/// <summary>
/// Initializes a new instance of the <see cref="DefaultInlineConstraintResolver"/> class.
@ -25,11 +24,23 @@ namespace Microsoft.AspNetCore.Routing
/// <param name="routeOptions">
/// Accessor for <see cref="RouteOptions"/> containing the constraints of interest.
/// </param>
[Obsolete("This constructor is obsolete. Use DefaultInlineConstraintResolver.ctor(IOptions<RouteOptions>, IServiceProvider) instead.")]
public DefaultInlineConstraintResolver(IOptions<RouteOptions> routeOptions)
{
_inlineConstraintMap = routeOptions.Value.ConstraintMap;
}
public DefaultInlineConstraintResolver(IOptions<RouteOptions> routeOptions, IServiceProvider serviceProvider)
{
if (serviceProvider == null)
{
throw new ArgumentNullException(nameof(serviceProvider));
}
_inlineConstraintMap = routeOptions.Value.ConstraintMap;
_serviceProvider = serviceProvider;
}
/// <inheritdoc />
/// <example>
/// A typical constraint looks like the following
@ -45,112 +56,7 @@ namespace Microsoft.AspNetCore.Routing
throw new ArgumentNullException(nameof(inlineConstraint));
}
string constraintKey;
string argumentString;
var indexOfFirstOpenParens = inlineConstraint.IndexOf('(');
if (indexOfFirstOpenParens >= 0 && inlineConstraint.EndsWith(")", StringComparison.Ordinal))
{
constraintKey = inlineConstraint.Substring(0, indexOfFirstOpenParens);
argumentString = inlineConstraint.Substring(indexOfFirstOpenParens + 1,
inlineConstraint.Length - indexOfFirstOpenParens - 2);
}
else
{
constraintKey = inlineConstraint;
argumentString = null;
}
Type constraintType;
if (!_inlineConstraintMap.TryGetValue(constraintKey, out constraintType))
{
// Cannot resolve the constraint key
return null;
}
if (!typeof(IRouteConstraint).GetTypeInfo().IsAssignableFrom(constraintType.GetTypeInfo()))
{
throw new RouteCreationException(
Resources.FormatDefaultInlineConstraintResolver_TypeNotConstraint(
constraintType, constraintKey, typeof(IRouteConstraint).Name));
}
try
{
return CreateConstraint(constraintType, argumentString);
}
catch (RouteCreationException)
{
throw;
}
catch (Exception exception)
{
throw new RouteCreationException(
$"An error occurred while trying to create an instance of route constraint '{constraintType.FullName}'.",
exception);
}
}
internal static IRouteConstraint CreateConstraint(Type constraintType, string argumentString)
{
// No arguments - call the default constructor
if (argumentString == null)
{
return (IRouteConstraint)Activator.CreateInstance(constraintType);
}
var constraintTypeInfo = constraintType.GetTypeInfo();
ConstructorInfo activationConstructor = null;
object[] parameters = null;
var constructors = constraintTypeInfo.DeclaredConstructors.ToArray();
// If there is only one constructor and it has a single parameter, pass the argument string directly
// This is necessary for the Regex RouteConstraint to ensure that patterns are not split on commas.
if (constructors.Length == 1 && constructors[0].GetParameters().Length == 1)
{
activationConstructor = constructors[0];
parameters = ConvertArguments(activationConstructor.GetParameters(), new string[] { argumentString });
}
else
{
var arguments = argumentString.Split(',').Select(argument => argument.Trim()).ToArray();
var matchingConstructors = constructors.Where(ci => ci.GetParameters().Length == arguments.Length)
.ToArray();
var constructorMatches = matchingConstructors.Length;
if (constructorMatches == 0)
{
throw new RouteCreationException(
Resources.FormatDefaultInlineConstraintResolver_CouldNotFindCtor(
constraintTypeInfo.Name, arguments.Length));
}
else if (constructorMatches == 1)
{
activationConstructor = matchingConstructors[0];
parameters = ConvertArguments(activationConstructor.GetParameters(), arguments);
}
else
{
throw new RouteCreationException(
Resources.FormatDefaultInlineConstraintResolver_AmbiguousCtors(
constraintTypeInfo.Name, arguments.Length));
}
}
return (IRouteConstraint)activationConstructor.Invoke(parameters);
}
private static object[] ConvertArguments(ParameterInfo[] parameterInfos, string[] arguments)
{
var parameters = new object[parameterInfos.Length];
for (var i = 0; i < parameterInfos.Length; i++)
{
var parameter = parameterInfos[i];
var parameterType = parameter.ParameterType;
parameters[i] = Convert.ChangeType(arguments[i], parameterType, CultureInfo.InvariantCulture);
}
return parameters;
return ParameterPolicyActivator.ResolveParameterPolicy<IRouteConstraint>(_inlineConstraintMap, _serviceProvider, inlineConstraint, out _);
}
}
}

View File

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

View File

@ -2,12 +2,15 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Routing.Internal;
using Microsoft.AspNetCore.Routing.Matchers;
using Microsoft.AspNetCore.Routing.Matching;
using Microsoft.AspNetCore.Routing.Template;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.Options;
@ -17,62 +20,165 @@ namespace Microsoft.AspNetCore.Routing
internal class DefaultLinkGenerator : LinkGenerator
{
private readonly static char[] UrlQueryDelimiters = new char[] { '?', '#' };
private readonly MatchProcessorFactory _matchProcessorFactory;
private readonly ParameterPolicyFactory _parameterPolicyFactory;
private readonly ObjectPool<UriBuildingContext> _uriBuildingContextPool;
private readonly RouteOptions _options;
private readonly ILogger<DefaultLinkGenerator> _logger;
private readonly IServiceProvider _serviceProvider;
private readonly RouteOptions _options;
public DefaultLinkGenerator(
MatchProcessorFactory matchProcessorFactory,
ParameterPolicyFactory parameterPolicyFactory,
ObjectPool<UriBuildingContext> uriBuildingContextPool,
IOptions<RouteOptions> routeOptions,
ILogger<DefaultLinkGenerator> logger)
ILogger<DefaultLinkGenerator> logger,
IServiceProvider serviceProvider)
{
_matchProcessorFactory = matchProcessorFactory;
_parameterPolicyFactory = parameterPolicyFactory;
_uriBuildingContextPool = uriBuildingContextPool;
_options = routeOptions.Value;
_logger = logger;
_serviceProvider = serviceProvider;
}
public override string GetLink(LinkGeneratorContext context)
public override bool TryGetLink(
HttpContext httpContext,
string routeName,
object values,
LinkOptions options,
out string link)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (TryGetLink(context, out var link))
{
return link;
}
throw new InvalidOperationException("Could not find a matching endpoint to generate a link.");
return TryGetLinkByRouteValues(
httpContext,
routeName,
values,
options,
out link);
}
public override bool TryGetLink(LinkGeneratorContext context, out string link)
public override bool TryGetLinkByAddress<TAddress>(
HttpContext httpContext,
TAddress address,
object values,
LinkOptions options,
out string link)
{
if (context == null)
return TryGetLinkByAddressInternal(
httpContext,
address,
explicitValues: values,
ambientValues: GetAmbientValues(httpContext),
options,
out link);
}
public override LinkGenerationTemplate GetTemplate(HttpContext httpContext, string routeName, object values)
{
var ambientValues = GetAmbientValues(httpContext);
var explicitValues = new RouteValueDictionary(values);
return GetTemplateInternal(
httpContext,
new RouteValuesAddress
{
RouteName = routeName,
ExplicitValues = explicitValues,
AmbientValues = ambientValues
},
ambientValues,
explicitValues,
values);
}
public override LinkGenerationTemplate GetTemplateByAddress<TAddress>(
HttpContext httpContext,
TAddress address)
{
return GetTemplateInternal(httpContext, address, values: null);
}
internal string MakeLink(
HttpContext httpContext,
RouteEndpoint endpoint,
RouteValueDictionary ambientValues,
RouteValueDictionary explicitValues,
LinkOptions options)
{
var templateBinder = new TemplateBinder(
UrlEncoder.Default,
_uriBuildingContextPool,
endpoint.RoutePattern,
new RouteValueDictionary(endpoint.RoutePattern.Defaults));
var routeValuesAddressMetadata = endpoint.Metadata.GetMetadata<IRouteValuesAddressMetadata>();
var templateValuesResult = templateBinder.GetValues(
ambientValues: ambientValues,
explicitValues: explicitValues,
requiredKeys: routeValuesAddressMetadata?.RequiredValues.Keys);
if (templateValuesResult == null)
{
throw new ArgumentNullException(nameof(context));
// We're missing one of the required values for this route.
return null;
}
if (!MatchesConstraints(httpContext, endpoint, templateValuesResult.CombinedValues))
{
return null;
}
var url = templateBinder.BindValues(templateValuesResult.AcceptedValues);
return Normalize(url, options);
}
private bool TryGetLinkByRouteValues(
HttpContext httpContext,
string routeName,
object values,
LinkOptions options,
out string link)
{
var ambientValues = GetAmbientValues(httpContext);
var address = new RouteValuesAddress
{
RouteName = routeName,
ExplicitValues = new RouteValueDictionary(values),
AmbientValues = ambientValues
};
return TryGetLinkByAddressInternal(
httpContext,
address,
explicitValues: values,
ambientValues: ambientValues,
options,
out link);
}
private bool TryGetLinkByAddressInternal<TAddress>(
HttpContext httpContext,
TAddress address,
object explicitValues,
RouteValueDictionary ambientValues,
LinkOptions options,
out string link)
{
link = null;
if (context.Endpoints == null)
var endpoints = FindEndpoints(address);
if (endpoints == null)
{
return false;
}
var matcherEndpoints = context.Endpoints.OfType<MatcherEndpoint>();
if (!matcherEndpoints.Any())
foreach (var endpoint in endpoints)
{
//todo:log here
return false;
}
link = MakeLink(
httpContext,
endpoint,
ambientValues,
new RouteValueDictionary(explicitValues),
options);
foreach (var endpoint in matcherEndpoints)
{
link = GetLink(endpoint, context);
if (link != null)
{
return true;
@ -82,35 +188,52 @@ namespace Microsoft.AspNetCore.Routing
return false;
}
private string GetLink(MatcherEndpoint endpoint, LinkGeneratorContext context)
private LinkGenerationTemplate GetTemplateInternal<TAddress>(
HttpContext httpContext,
TAddress address,
object values)
{
var templateBinder = new TemplateBinder(
UrlEncoder.Default,
_uriBuildingContextPool,
new RouteTemplate(endpoint.RoutePattern),
new RouteValueDictionary(endpoint.RoutePattern.Defaults));
var templateValuesResult = templateBinder.GetValues(
ambientValues: context.AmbientValues,
values: context.ExplicitValues);
if (templateValuesResult == null)
{
// We're missing one of the required values for this route.
return null;
}
if (!MatchesConstraints(context.HttpContext, endpoint, templateValuesResult.CombinedValues))
var endpoints = FindEndpoints(address);
if (endpoints == null)
{
return null;
}
var url = templateBinder.BindValues(templateValuesResult.AcceptedValues);
return Normalize(context, url);
var ambientValues = GetAmbientValues(httpContext);
var explicitValues = new RouteValueDictionary(values);
return new DefaultLinkGenerationTemplate(
this,
endpoints,
httpContext,
explicitValues,
ambientValues);
}
private LinkGenerationTemplate GetTemplateInternal<TAddress>(
HttpContext httpContext,
TAddress address,
RouteValueDictionary ambientValues,
RouteValueDictionary explicitValues,
object values)
{
var endpoints = FindEndpoints(address);
if (endpoints == null)
{
return null;
}
return new DefaultLinkGenerationTemplate(
this,
endpoints,
httpContext,
explicitValues,
ambientValues);
}
private bool MatchesConstraints(
HttpContext httpContext,
MatcherEndpoint endpoint,
RouteEndpoint endpoint,
RouteValueDictionary routeValues)
{
if (routeValues == null)
@ -118,15 +241,16 @@ namespace Microsoft.AspNetCore.Routing
throw new ArgumentNullException(nameof(routeValues));
}
foreach (var kvp in endpoint.RoutePattern.Constraints)
foreach (var kvp in endpoint.RoutePattern.ParameterPolicies)
{
var parameter = endpoint.RoutePattern.GetParameter(kvp.Key); // may be null, that's ok
var constraintReferences = kvp.Value;
for (var i = 0; i < constraintReferences.Count; i++)
{
var constraintReference = constraintReferences[i];
var matchProcessor = _matchProcessorFactory.Create(parameter, constraintReference);
if (!matchProcessor.ProcessOutbound(httpContext, routeValues))
var parameterPolicy = _parameterPolicyFactory.Create(parameter, constraintReference);
if (parameterPolicy is IRouteConstraint routeConstraint
&& !routeConstraint.Match(httpContext, NullRouter.Instance, kvp.Key, routeValues, RouteDirection.UrlGeneration))
{
return false;
}
@ -136,13 +260,11 @@ namespace Microsoft.AspNetCore.Routing
return true;
}
private string Normalize(LinkGeneratorContext context, string url)
private string Normalize(string url, LinkOptions options)
{
var lowercaseUrls = context.LowercaseUrls.HasValue ? context.LowercaseUrls.Value : _options.LowercaseUrls;
var lowercaseQueryStrings = context.LowercaseQueryStrings.HasValue ?
context.LowercaseQueryStrings.Value : _options.LowercaseQueryStrings;
var appendTrailingSlash = context.AppendTrailingSlash.HasValue ?
context.AppendTrailingSlash.Value : _options.AppendTrailingSlash;
var lowercaseUrls = options?.LowercaseUrls ?? _options.LowercaseUrls;
var lowercaseQueryStrings = options?.LowercaseQueryStrings ?? _options.LowercaseQueryStrings;
var appendTrailingSlash = options?.AppendTrailingSlash ?? _options.AppendTrailingSlash;
if (!string.IsNullOrEmpty(url) && (lowercaseUrls || appendTrailingSlash))
{
@ -177,5 +299,36 @@ namespace Microsoft.AspNetCore.Routing
return url;
}
private RouteValueDictionary GetAmbientValues(HttpContext httpContext)
{
if (httpContext != null)
{
var feature = httpContext.Features.Get<IRouteValuesFeature>();
if (feature != null)
{
return feature.RouteValues;
}
}
return new RouteValueDictionary();
}
private IEnumerable<RouteEndpoint> FindEndpoints<TAddress>(TAddress address)
{
var finder = _serviceProvider.GetRequiredService<IEndpointFinder<TAddress>>();
var endpoints = finder.FindEndpoints(address);
if (endpoints == null)
{
return null;
}
var routeEndpoints = endpoints.OfType<RouteEndpoint>();
if (!routeEndpoints.Any())
{
return null;
}
return routeEndpoints;
}
}
}

View File

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

View File

@ -3,11 +3,8 @@
using System;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Constraints;
using Microsoft.AspNetCore.Routing.EndpointConstraints;
using Microsoft.AspNetCore.Routing.EndpointFinders;
using Microsoft.AspNetCore.Routing.Internal;
using Microsoft.AspNetCore.Routing.Matchers;
using Microsoft.AspNetCore.Routing.Matching;
using Microsoft.AspNetCore.Routing.Tree;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
@ -65,26 +62,21 @@ namespace Microsoft.Extensions.DependencyInjection
//
// Default matcher implementation
//
services.TryAddSingleton<MatchProcessorFactory, DefaultMatchProcessorFactory>();
services.TryAddSingleton<ParameterPolicyFactory, DefaultParameterPolicyFactory>();
services.TryAddSingleton<MatcherFactory, DfaMatcherFactory>();
services.TryAddTransient<DfaMatcherBuilder>();
services.TryAddSingleton<DfaGraphWriter>();
// Link generation related services
services.TryAddSingleton<IEndpointFinder<string>, NameBasedEndpointFinder>();
services.TryAddSingleton<IEndpointFinder<RouteValuesBasedEndpointFinderContext>, RouteValuesBasedEndpointFinder>();
services.TryAddSingleton<IEndpointFinder<RouteValuesAddress>, RouteValuesBasedEndpointFinder>();
services.TryAddSingleton<LinkGenerator, DefaultLinkGenerator>();
//
// Endpoint Selection
//
services.TryAddSingleton<EndpointSelector, EndpointConstraintEndpointSelector>();
services.TryAddSingleton<EndpointConstraintCache>();
services.TryAddSingleton<EndpointSelector, DefaultEndpointSelector>();
services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, HttpMethodMatcherPolicy>());
// Will be cached by the EndpointSelector
services.TryAddEnumerable(
ServiceDescriptor.Transient<IEndpointConstraintProvider, DefaultEndpointConstraintProvider>());
return services;
}

View File

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

View File

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

View File

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

View File

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

View File

@ -2,14 +2,26 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Routing
{
/// <summary>
/// Provides a collection of <see cref="Endpoint"/> instances.
/// </summary>
public abstract class EndpointDataSource
{
/// <summary>
/// Gets a <see cref="IChangeToken"/> used to signal invalidation of cached <see cref="Endpoint"/>
/// instances.
/// </summary>
/// <returns>The <see cref="IChangeToken"/>.</returns>
public abstract IChangeToken GetChangeToken();
/// <summary>
/// Returns a read-only collection of <see cref="Endpoint"/> instances.
/// </summary>
public abstract IReadOnlyList<Endpoint> Endpoints { get; }
}
}

View File

@ -3,37 +3,63 @@
using System;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
namespace Microsoft.AspNetCore.Routing
{
public sealed class EndpointFeature : IEndpointFeature, IRoutingFeature
public sealed class EndpointFeature : IEndpointFeature, IRouteValuesFeature, IRoutingFeature
{
private RouteData _routeData;
private RouteValueDictionary _values;
private RouteValueDictionary _routeValues;
/// <summary>
/// Gets or sets the selected <see cref="Http.Endpoint"/> for the current
/// request.
/// </summary>
public Endpoint Endpoint { get; set; }
public Func<RequestDelegate, RequestDelegate> Invoker { get; set; }
public RouteValueDictionary Values
/// <summary>
/// Gets or sets the <see cref="RouteValueDictionary"/> associated with the currrent
/// request.
/// </summary>
public RouteValueDictionary RouteValues
{
get => _values;
get => _routeValues;
set
{
_values = value;
_routeValues = value;
// RouteData will be created next get with new Values
_routeData = null;
}
}
/// <summary>
/// Gets or sets the <see cref="RouteData"/> for the current request.
/// </summary>
/// <remarks>
/// The setter is not implemented. Use <see cref="RouteValues"/> to set the route values.
/// </remarks>
RouteData IRoutingFeature.RouteData
{
get
{
if (_routeData == null)
{
_routeData = new RouteData(_values);
_routeData = _routeValues == null ? new RouteData() : new RouteData(_routeValues);
// Note: DataTokens won't update if someone else overwrites the Endpoint
// after route values has been set. This seems find since endpoints are a new
// feature and DataTokens are for back-compat.
var dataTokensMetadata = Endpoint?.Metadata.GetMetadata<IDataTokensMetadata>();
if (dataTokensMetadata != null)
{
var dataTokens = _routeData.DataTokens;
foreach (var kvp in dataTokensMetadata.DataTokens)
{
_routeData.DataTokens.Add(kvp.Key, kvp.Value);
}
}
}
return _routeData;
@ -41,4 +67,4 @@ namespace Microsoft.AspNetCore.Routing
set => throw new NotSupportedException();
}
}
}
}

View File

@ -4,6 +4,7 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Routing
@ -31,26 +32,18 @@ namespace Microsoft.AspNetCore.Routing
public async Task Invoke(HttpContext httpContext)
{
var feature = httpContext.Features.Get<IEndpointFeature>();
if (feature == null)
var endpoint = httpContext.Features.Get<IEndpointFeature>()?.Endpoint;
if (endpoint?.RequestDelegate != null)
{
var message = $"Unable to execute an endpoint because the {nameof(GlobalRoutingMiddleware)} was not run for this request. " +
$"Ensure {nameof(GlobalRoutingMiddleware)} is added to the request execution pipeline before {nameof(EndpointMiddleware)} in application startup code.";
throw new InvalidOperationException(message);
}
if (feature.Invoker != null)
{
Log.ExecutingEndpoint(_logger, feature.Endpoint);
Log.ExecutingEndpoint(_logger, endpoint);
try
{
await feature.Invoker(_next)(httpContext);
await endpoint.RequestDelegate(httpContext);
}
finally
{
Log.ExecutedEndpoint(_logger, feature.Endpoint);
Log.ExecutedEndpoint(_logger, endpoint);
}
return;

View File

@ -5,7 +5,8 @@ using System.Collections.Generic;
namespace Microsoft.AspNetCore.Routing
{
public class EndpointOptions
// Internal for 2.2. Public API for configuring endpoints will be added in 3.0
internal class EndpointOptions
{
public IList<EndpointDataSource> DataSources { get; } = new List<EndpointDataSource>();
}

View File

@ -5,13 +5,14 @@ using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Matchers;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Routing.Matching;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Routing
{
internal sealed class GlobalRoutingMiddleware
internal sealed class EndpointRoutingMiddleware
{
private readonly MatcherFactory _matcherFactory;
private readonly ILogger _logger;
@ -20,10 +21,10 @@ namespace Microsoft.AspNetCore.Routing
private Task<Matcher> _initializationTask;
public GlobalRoutingMiddleware(
public EndpointRoutingMiddleware(
MatcherFactory matcherFactory,
CompositeEndpointDataSource endpointDataSource,
ILogger<GlobalRoutingMiddleware> logger,
ILogger<EndpointRoutingMiddleware> logger,
RequestDelegate next)
{
if (matcherFactory == null)
@ -55,10 +56,6 @@ namespace Microsoft.AspNetCore.Routing
public async Task Invoke(HttpContext httpContext)
{
var feature = new EndpointFeature();
httpContext.Features.Set<IEndpointFeature>(feature);
// Back compat support for users of IRoutingFeature
httpContext.Features.Set<IRoutingFeature>(feature);
// There's an inherent race condition between waiting for init and accessing the matcher
// this is OK because once `_matcher` is initialized, it will not be set to null again.
@ -67,6 +64,10 @@ namespace Microsoft.AspNetCore.Routing
await matcher.MatchAsync(httpContext, feature);
if (feature.Endpoint != null)
{
// Set the endpoint feature only on success. This means we won't overwrite any
// existing state for related features unless we did something.
SetEndpointFeature(httpContext, feature);
Log.MatchSuccess(_logger, feature);
}
else
@ -77,6 +78,15 @@ namespace Microsoft.AspNetCore.Routing
await _next(httpContext);
}
private static void SetEndpointFeature(HttpContext httpContext, EndpointFeature feature)
{
// For back-compat EndpointRouteValuesFeature implements IEndpointFeature,
// IRouteValuesFeature and IRoutingFeature
httpContext.Features.Set<IRoutingFeature>(feature);
httpContext.Features.Set<IRouteValuesFeature>(feature);
httpContext.Features.Set<IEndpointFeature>(feature);
}
// Initialization is async to avoid blocking threads while reflection and things
// of that nature take place.
//
@ -127,4 +137,4 @@ namespace Microsoft.AspNetCore.Routing
}
}
}
}
}

View File

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

View File

@ -0,0 +1,21 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.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; }
}
}

View File

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

View File

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