Merge release/2.2
This commit is contained in:
commit
b658d90785
|
|
@ -0,0 +1,145 @@
|
||||||
|
// 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 Microsoft.AspNetCore.Http;
|
||||||
|
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 MatcherEndpoint[] Endpoints;
|
||||||
|
private protected HttpContext[] Requests;
|
||||||
|
|
||||||
|
private protected void SetupEndpoints(params MatcherEndpoint[] 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: '{((MatcherEndpoint)expected)?.DisplayName ?? "null"}'");
|
||||||
|
message.AppendLine($"actual: '{((MatcherEndpoint)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 MatcherEndpoint 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 MatcherEndpoint(
|
||||||
|
MatcherEndpoint.EmptyInvoker,
|
||||||
|
RoutePatternFactory.Parse(template, defaults, constraints),
|
||||||
|
order,
|
||||||
|
new EndpointMetadataCollection(endpointMetadata),
|
||||||
|
displayName);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected (HttpContext httpContext, RouteValueDictionary ambientValues) CreateCurrentRequestContext(
|
||||||
|
object ambientValues = null)
|
||||||
|
{
|
||||||
|
var feature = new EndpointFeature { Values = new RouteValueDictionary(ambientValues) };
|
||||||
|
var context = new DefaultHttpContext();
|
||||||
|
context.Features.Set<IEndpointFeature>(feature);
|
||||||
|
|
||||||
|
return (context, feature.Values);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void CreateOutboundRouteEntry(TreeRouteBuilder treeRouteBuilder, MatcherEndpoint 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,
|
||||||
|
constraints: null)),
|
||||||
|
requiredLinkValues: new RouteValueDictionary(requiredValues),
|
||||||
|
routeName: null,
|
||||||
|
order: 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using BenchmarkDotNet.Attributes;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Routing.Tree;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing.LinkGeneration
|
||||||
|
{
|
||||||
|
public partial class LinkGenerationGithubBenchmark
|
||||||
|
{
|
||||||
|
private LinkGenerator _linkGenerator;
|
||||||
|
private TreeRouter _treeRouter;
|
||||||
|
private (HttpContext HttpContext, RouteValueDictionary AmbientValues) _requestContext;
|
||||||
|
private RouteValueDictionary _lookUpValues;
|
||||||
|
|
||||||
|
[GlobalSetup]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
SetupEndpoints();
|
||||||
|
|
||||||
|
var services = CreateServices();
|
||||||
|
_linkGenerator = services.GetRequiredService<LinkGenerator>();
|
||||||
|
|
||||||
|
// Attribute routing related
|
||||||
|
var treeRouteBuilder = services.GetRequiredService<TreeRouteBuilder>();
|
||||||
|
foreach (var endpoint in Endpoints)
|
||||||
|
{
|
||||||
|
CreateOutboundRouteEntry(treeRouteBuilder, endpoint);
|
||||||
|
}
|
||||||
|
_treeRouter = treeRouteBuilder.Build();
|
||||||
|
|
||||||
|
_requestContext = CreateCurrentRequestContext();
|
||||||
|
|
||||||
|
// Get the endpoint to test and pre-populate the lookup values with the defaults
|
||||||
|
// (as they are dynamically generated) and update with other required parameter values.
|
||||||
|
// /repos/{owner}/{repo}/issues/comments/{commentId}
|
||||||
|
var endpointToTest = Endpoints[176];
|
||||||
|
_lookUpValues = new RouteValueDictionary(endpointToTest.RoutePattern.Defaults);
|
||||||
|
_lookUpValues["owner"] = "aspnet";
|
||||||
|
_lookUpValues["repo"] = "routing";
|
||||||
|
_lookUpValues["commentId"] = "20202";
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark(Baseline = true)]
|
||||||
|
public void TreeRouter()
|
||||||
|
{
|
||||||
|
var virtualPathData = _treeRouter.GetVirtualPath(new VirtualPathContext(
|
||||||
|
_requestContext.HttpContext,
|
||||||
|
ambientValues: _requestContext.AmbientValues,
|
||||||
|
values: new RouteValueDictionary(_lookUpValues)));
|
||||||
|
|
||||||
|
AssertUrl("/repos/aspnet/routing/issues/comments/20202", virtualPathData?.VirtualPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
public void EndpointRouting()
|
||||||
|
{
|
||||||
|
var actualUrl = _linkGenerator.GetLink(
|
||||||
|
_requestContext.HttpContext,
|
||||||
|
values: new RouteValueDictionary(_lookUpValues));
|
||||||
|
|
||||||
|
AssertUrl("/repos/aspnet/routing/issues/comments/20202", actualUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,74 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using BenchmarkDotNet.Attributes;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing.LinkGeneration
|
||||||
|
{
|
||||||
|
public class SingleRouteRouteValuesBasedEndpointFinderBenchmark : EndpointRoutingBenchmarkBase
|
||||||
|
{
|
||||||
|
private IEndpointFinder<RouteValuesAddress> _finder;
|
||||||
|
private TestEndpointFinder _baseFinder;
|
||||||
|
private (HttpContext HttpContext, RouteValueDictionary AmbientValues) _requestContext;
|
||||||
|
|
||||||
|
[GlobalSetup]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
var template = "Products/Details";
|
||||||
|
var defaults = new { controller = "Products", action = "Details" };
|
||||||
|
var requiredValues = new { controller = "Products", action = "Details" };
|
||||||
|
|
||||||
|
SetupEndpoints(CreateEndpoint(template, defaults, requiredValues: requiredValues, routeName: "ProductDetails"));
|
||||||
|
var services = CreateServices();
|
||||||
|
_finder = services.GetRequiredService<IEndpointFinder<RouteValuesAddress>>();
|
||||||
|
_baseFinder = new TestEndpointFinder(Endpoints[0]);
|
||||||
|
|
||||||
|
_requestContext = CreateCurrentRequestContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark(Baseline = true)]
|
||||||
|
public void Baseline()
|
||||||
|
{
|
||||||
|
var actual = _baseFinder.FindEndpoints(address: 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
public void RouteValues()
|
||||||
|
{
|
||||||
|
var actual = _finder.FindEndpoints(new RouteValuesAddress
|
||||||
|
{
|
||||||
|
AmbientValues = _requestContext.AmbientValues,
|
||||||
|
ExplicitValues = new RouteValueDictionary(new { controller = "Products", action = "Details" }),
|
||||||
|
RouteName = null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
public void RouteName()
|
||||||
|
{
|
||||||
|
var actual = _finder.FindEndpoints(new RouteValuesAddress
|
||||||
|
{
|
||||||
|
AmbientValues = _requestContext.AmbientValues,
|
||||||
|
RouteName = "ProductDetails"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestEndpointFinder : IEndpointFinder<int>
|
||||||
|
{
|
||||||
|
private readonly Endpoint _endpoint;
|
||||||
|
|
||||||
|
public TestEndpointFinder(Endpoint endpoint)
|
||||||
|
{
|
||||||
|
_endpoint = endpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<Endpoint> FindEndpoints(int address)
|
||||||
|
{
|
||||||
|
return new[] { _endpoint };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using BenchmarkDotNet.Attributes;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Routing.Tree;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing.LinkGeneration
|
||||||
|
{
|
||||||
|
public class SingleRouteWithConstraintsBenchmark : EndpointRoutingBenchmarkBase
|
||||||
|
{
|
||||||
|
private TreeRouter _treeRouter;
|
||||||
|
private LinkGenerator _linkGenerator;
|
||||||
|
private (HttpContext HttpContext, RouteValueDictionary AmbientValues) _requestContext;
|
||||||
|
|
||||||
|
[GlobalSetup]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
var template = "Customers/Details/{category}/{region}/{id:int}";
|
||||||
|
var defaults = new { controller = "Customers", action = "Details" };
|
||||||
|
var requiredValues = new { controller = "Customers", action = "Details" };
|
||||||
|
|
||||||
|
// Endpoint routing related
|
||||||
|
SetupEndpoints(CreateEndpoint(template, defaults, requiredValues: requiredValues));
|
||||||
|
var services = CreateServices();
|
||||||
|
_linkGenerator = services.GetRequiredService<LinkGenerator>();
|
||||||
|
|
||||||
|
// Attribute routing related
|
||||||
|
var treeRouteBuilder = services.GetRequiredService<TreeRouteBuilder>();
|
||||||
|
CreateOutboundRouteEntry(treeRouteBuilder, Endpoints[0]);
|
||||||
|
_treeRouter = treeRouteBuilder.Build();
|
||||||
|
|
||||||
|
_requestContext = CreateCurrentRequestContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark(Baseline = true)]
|
||||||
|
public void TreeRouter()
|
||||||
|
{
|
||||||
|
var virtualPathData = _treeRouter.GetVirtualPath(new VirtualPathContext(
|
||||||
|
_requestContext.HttpContext,
|
||||||
|
ambientValues: _requestContext.AmbientValues,
|
||||||
|
values: new RouteValueDictionary(
|
||||||
|
new
|
||||||
|
{
|
||||||
|
controller = "Customers",
|
||||||
|
action = "Details",
|
||||||
|
category = "Administration",
|
||||||
|
region = "US",
|
||||||
|
id = 10
|
||||||
|
})));
|
||||||
|
|
||||||
|
AssertUrl("/Customers/Details/Administration/US/10", virtualPathData?.VirtualPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
public void EndpointRouting()
|
||||||
|
{
|
||||||
|
var actualUrl = _linkGenerator.GetLink(
|
||||||
|
_requestContext.HttpContext,
|
||||||
|
values: new RouteValueDictionary(
|
||||||
|
new
|
||||||
|
{
|
||||||
|
controller = "Customers",
|
||||||
|
action = "Details",
|
||||||
|
category = "Administration",
|
||||||
|
region = "US",
|
||||||
|
id = 10
|
||||||
|
}));
|
||||||
|
|
||||||
|
AssertUrl("/Customers/Details/Administration/US/10", actualUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using BenchmarkDotNet.Attributes;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Routing.Tree;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing.LinkGeneration
|
||||||
|
{
|
||||||
|
public class SingleRouteWithNoParametersBenchmark : EndpointRoutingBenchmarkBase
|
||||||
|
{
|
||||||
|
private TreeRouter _treeRouter;
|
||||||
|
private LinkGenerator _linkGenerator;
|
||||||
|
private (HttpContext HttpContext, RouteValueDictionary AmbientValues) _requestContext;
|
||||||
|
|
||||||
|
[GlobalSetup]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
var template = "Products/Details";
|
||||||
|
var defaults = new { controller = "Products", action = "Details" };
|
||||||
|
var requiredValues = new { controller = "Products", action = "Details" };
|
||||||
|
|
||||||
|
// Endpoint routing related
|
||||||
|
SetupEndpoints(CreateEndpoint(template, defaults, requiredValues: requiredValues));
|
||||||
|
var services = CreateServices();
|
||||||
|
_linkGenerator = services.GetRequiredService<LinkGenerator>();
|
||||||
|
|
||||||
|
// Attribute routing related
|
||||||
|
var treeRouteBuilder = services.GetRequiredService<TreeRouteBuilder>();
|
||||||
|
CreateOutboundRouteEntry(treeRouteBuilder, Endpoints[0]);
|
||||||
|
_treeRouter = treeRouteBuilder.Build();
|
||||||
|
|
||||||
|
_requestContext = CreateCurrentRequestContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark(Baseline = true)]
|
||||||
|
public void TreeRouter()
|
||||||
|
{
|
||||||
|
var virtualPathData = _treeRouter.GetVirtualPath(new VirtualPathContext(
|
||||||
|
_requestContext.HttpContext,
|
||||||
|
ambientValues: _requestContext.AmbientValues,
|
||||||
|
values: new RouteValueDictionary(
|
||||||
|
new
|
||||||
|
{
|
||||||
|
controller = "Products",
|
||||||
|
action = "Details",
|
||||||
|
})));
|
||||||
|
|
||||||
|
AssertUrl("/Products/Details", virtualPathData?.VirtualPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
public void EndpointRouting()
|
||||||
|
{
|
||||||
|
var actualUrl = _linkGenerator.GetLink(
|
||||||
|
_requestContext.HttpContext,
|
||||||
|
values: new RouteValueDictionary(
|
||||||
|
new
|
||||||
|
{
|
||||||
|
controller = "Products",
|
||||||
|
action = "Details",
|
||||||
|
}));
|
||||||
|
|
||||||
|
AssertUrl("/Products/Details", actualUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using BenchmarkDotNet.Attributes;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Routing.Tree;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing.LinkGeneration
|
||||||
|
{
|
||||||
|
public class SingleRouteWithParametersBenchmark : EndpointRoutingBenchmarkBase
|
||||||
|
{
|
||||||
|
private TreeRouter _treeRouter;
|
||||||
|
private LinkGenerator _linkGenerator;
|
||||||
|
private (HttpContext HttpContext, RouteValueDictionary AmbientValues) _requestContext;
|
||||||
|
|
||||||
|
[GlobalSetup]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
var template = "Customers/Details/{category}/{region}/{id}";
|
||||||
|
var defaults = new { controller = "Customers", action = "Details" };
|
||||||
|
var requiredValues = new { controller = "Customers", action = "Details" };
|
||||||
|
|
||||||
|
// Endpoint routing related
|
||||||
|
SetupEndpoints(CreateEndpoint(template, defaults, requiredValues: requiredValues));
|
||||||
|
var services = CreateServices();
|
||||||
|
_linkGenerator = services.GetRequiredService<LinkGenerator>();
|
||||||
|
|
||||||
|
// Attribute routing related
|
||||||
|
var treeRouteBuilder = services.GetRequiredService<TreeRouteBuilder>();
|
||||||
|
CreateOutboundRouteEntry(treeRouteBuilder, Endpoints[0]);
|
||||||
|
_treeRouter = treeRouteBuilder.Build();
|
||||||
|
|
||||||
|
_requestContext = CreateCurrentRequestContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark(Baseline = true)]
|
||||||
|
public void TreeRouter()
|
||||||
|
{
|
||||||
|
var virtualPathData = _treeRouter.GetVirtualPath(new VirtualPathContext(
|
||||||
|
_requestContext.HttpContext,
|
||||||
|
ambientValues: _requestContext.AmbientValues,
|
||||||
|
values: new RouteValueDictionary(
|
||||||
|
new
|
||||||
|
{
|
||||||
|
controller = "Customers",
|
||||||
|
action = "Details",
|
||||||
|
category = "Administration",
|
||||||
|
region = "US",
|
||||||
|
id = 10
|
||||||
|
})));
|
||||||
|
|
||||||
|
AssertUrl("/Customers/Details/Administration/US/10", virtualPathData?.VirtualPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
public void EndpointRouting()
|
||||||
|
{
|
||||||
|
var actualUrl = _linkGenerator.GetLink(
|
||||||
|
_requestContext.HttpContext,
|
||||||
|
values: new RouteValueDictionary(
|
||||||
|
new
|
||||||
|
{
|
||||||
|
controller = "Customers",
|
||||||
|
action = "Details",
|
||||||
|
category = "Administration",
|
||||||
|
region = "US",
|
||||||
|
id = 10
|
||||||
|
}));
|
||||||
|
|
||||||
|
AssertUrl("/Customers/Details/Administration/US/10", actualUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,7 +7,7 @@ using BenchmarkDotNet.Attributes;
|
||||||
namespace Microsoft.AspNetCore.Routing.Matching
|
namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
{
|
{
|
||||||
// Generated from https://github.com/Azure/azure-rest-api-specs
|
// Generated from https://github.com/Azure/azure-rest-api-specs
|
||||||
public partial class MatcherAzureBenchmark : MatcherBenchmarkBase
|
public partial class MatcherAzureBenchmark : EndpointRoutingBenchmarkBase
|
||||||
{
|
{
|
||||||
private const int SampleCount = 100;
|
private const int SampleCount = 100;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Http;
|
||||||
namespace Microsoft.AspNetCore.Routing.Matching
|
namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
{
|
{
|
||||||
// This code was generated by the Swaggatherer
|
// This code was generated by the Swaggatherer
|
||||||
public partial class MatcherAzureBenchmark : MatcherBenchmarkBase
|
public partial class MatcherAzureBenchmark : EndpointRoutingBenchmarkBase
|
||||||
{
|
{
|
||||||
private const int EndpointCount = 5160;
|
private const int EndpointCount = 5160;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,87 +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.Patterns;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing.Matching
|
|
||||||
{
|
|
||||||
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),
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -7,7 +7,7 @@ using BenchmarkDotNet.Attributes;
|
||||||
namespace Microsoft.AspNetCore.Routing.Matching
|
namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
{
|
{
|
||||||
// Generated from https://github.com/Azure/azure-rest-api-specs
|
// Generated from https://github.com/Azure/azure-rest-api-specs
|
||||||
public partial class MatcherFindCandidateSetAzureBenchmark : MatcherBenchmarkBase
|
public partial class MatcherFindCandidateSetAzureBenchmark : EndpointRoutingBenchmarkBase
|
||||||
{
|
{
|
||||||
// SegmentCount should be max-segments + 1, but we don't have a good way to compute
|
// SegmentCount should be max-segments + 1, but we don't have a good way to compute
|
||||||
// it here, so using 16 as a safe guess.
|
// it here, so using 16 as a safe guess.
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Http;
|
||||||
namespace Microsoft.AspNetCore.Routing.Matching
|
namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
{
|
{
|
||||||
// This code was generated by the Swaggatherer
|
// This code was generated by the Swaggatherer
|
||||||
public partial class MatcherFindCandidateSetAzureBenchmark : MatcherBenchmarkBase
|
public partial class MatcherFindCandidateSetAzureBenchmark : EndpointRoutingBenchmarkBase
|
||||||
{
|
{
|
||||||
private const int EndpointCount = 3517;
|
private const int EndpointCount = 3517;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
{
|
{
|
||||||
// Generated from https://github.com/APIs-guru/openapi-directory
|
// Generated from https://github.com/APIs-guru/openapi-directory
|
||||||
// Use https://editor2.swagger.io/ to convert from yaml to json-
|
// Use https://editor2.swagger.io/ to convert from yaml to json-
|
||||||
public partial class MatcherFindCandidateSetGithubBenchmark : MatcherBenchmarkBase
|
public partial class MatcherFindCandidateSetGithubBenchmark : EndpointRoutingBenchmarkBase
|
||||||
{
|
{
|
||||||
// SegmentCount should be max-segments + 1, but we don't have a good way to compute
|
// SegmentCount should be max-segments + 1, but we don't have a good way to compute
|
||||||
// it here, so using 16 as a safe guess.
|
// it here, so using 16 as a safe guess.
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Http;
|
||||||
namespace Microsoft.AspNetCore.Routing.Matching
|
namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
{
|
{
|
||||||
// This code was generated by the Swaggatherer
|
// This code was generated by the Swaggatherer
|
||||||
public partial class MatcherFindCandidateSetGithubBenchmark : MatcherBenchmarkBase
|
public partial class MatcherFindCandidateSetGithubBenchmark : EndpointRoutingBenchmarkBase
|
||||||
{
|
{
|
||||||
private const int EndpointCount = 155;
|
private const int EndpointCount = 155;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing.Matching
|
namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
{
|
{
|
||||||
public class MatcherFindCandidateSetSingleEntryBenchmark : MatcherBenchmarkBase
|
public class MatcherFindCandidateSetSingleEntryBenchmark : EndpointRoutingBenchmarkBase
|
||||||
{
|
{
|
||||||
// SegmentCount should be max-segments + 1
|
// SegmentCount should be max-segments + 1
|
||||||
private const int SegmentCount = 2;
|
private const int SegmentCount = 2;
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing.Matching
|
namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
{
|
{
|
||||||
public class MatcherFindCandidateSetSmallEntryCountBenchmark : MatcherBenchmarkBase
|
public class MatcherFindCandidateSetSmallEntryCountBenchmark : EndpointRoutingBenchmarkBase
|
||||||
{
|
{
|
||||||
// SegmentCount should be max-segments + 1
|
// SegmentCount should be max-segments + 1
|
||||||
private const int SegmentCount = 6;
|
private const int SegmentCount = 6;
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
{
|
{
|
||||||
// Generated from https://github.com/APIs-guru/openapi-directory
|
// Generated from https://github.com/APIs-guru/openapi-directory
|
||||||
// Use https://editor2.swagger.io/ to convert from yaml to json-
|
// Use https://editor2.swagger.io/ to convert from yaml to json-
|
||||||
public partial class MatcherGithubBenchmark : MatcherBenchmarkBase
|
public partial class MatcherGithubBenchmark : EndpointRoutingBenchmarkBase
|
||||||
{
|
{
|
||||||
private BarebonesMatcher _baseline;
|
private BarebonesMatcher _baseline;
|
||||||
private Matcher _dfa;
|
private Matcher _dfa;
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Http;
|
||||||
namespace Microsoft.AspNetCore.Routing.Matching
|
namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
{
|
{
|
||||||
// This code was generated by the Swaggatherer
|
// This code was generated by the Swaggatherer
|
||||||
public partial class MatcherGithubBenchmark : MatcherBenchmarkBase
|
public partial class MatcherGithubBenchmark : EndpointRoutingBenchmarkBase
|
||||||
{
|
{
|
||||||
private const int EndpointCount = 243;
|
private const int EndpointCount = 243;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ using Microsoft.AspNetCore.Http;
|
||||||
namespace Microsoft.AspNetCore.Routing.Matching
|
namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
{
|
{
|
||||||
// Just like TechEmpower Plaintext
|
// Just like TechEmpower Plaintext
|
||||||
public partial class MatcherSingleEntryBenchmark : MatcherBenchmarkBase
|
public partial class MatcherSingleEntryBenchmark : EndpointRoutingBenchmarkBase
|
||||||
{
|
{
|
||||||
private const int SampleCount = 100;
|
private const int SampleCount = 100;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,51 +10,81 @@ namespace Swaggatherer
|
||||||
{
|
{
|
||||||
public static string Execute(IReadOnlyList<RouteEntry> entries)
|
public static string Execute(IReadOnlyList<RouteEntry> entries)
|
||||||
{
|
{
|
||||||
|
var controllerCount = 0;
|
||||||
|
var templatesVisited = new Dictionary<string, (int ControllerIndex, int ActionIndex)>(
|
||||||
|
StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
var setupEndpointsLines = new List<string>();
|
var setupEndpointsLines = new List<string>();
|
||||||
for (var i = 0; i < entries.Count; i++)
|
for (var i = 0; i < entries.Count; i++)
|
||||||
{
|
{
|
||||||
var entry = entries[i];
|
var entry = entries[i];
|
||||||
var httpMethodText = entry.Method == null ? string.Empty : $", \"{entry.Method.ToUpperInvariant()}\"";
|
|
||||||
setupEndpointsLines.Add($" _endpoints[{i}] = CreateEndpoint(\"{entry.Template.TemplateText}\"{httpMethodText});");
|
// In attribute routing, same template is used for all actions within that controller. The following
|
||||||
|
// simulates that where we only increment the controller count when a new endpoint for a new template
|
||||||
|
// is being created.
|
||||||
|
var template = entry.Template.TemplateText;
|
||||||
|
if (!templatesVisited.TryGetValue(template, out var visitedTemplateInfo))
|
||||||
|
{
|
||||||
|
controllerCount++;
|
||||||
|
visitedTemplateInfo = (controllerCount, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment the action count within a controller template
|
||||||
|
visitedTemplateInfo.ActionIndex++;
|
||||||
|
templatesVisited[template] = visitedTemplateInfo;
|
||||||
|
|
||||||
|
var controllerName = $"Controller{visitedTemplateInfo.ControllerIndex}";
|
||||||
|
var actionName = $"Action{visitedTemplateInfo.ActionIndex}";
|
||||||
|
|
||||||
|
var httpMethodText = entry.Method == null ? "httpMethod: null" : $"\"{entry.Method.ToUpperInvariant()}\"";
|
||||||
|
setupEndpointsLines.Add($" Endpoints[{i}] = CreateEndpoint(\"{template}\", \"{controllerName}\", \"{actionName}\", {httpMethodText});");
|
||||||
}
|
}
|
||||||
|
|
||||||
var setupRequestsLines = new List<string>();
|
var setupRequestsLines = new List<string>();
|
||||||
for (var i = 0; i < entries.Count; i++)
|
for (var i = 0; i < entries.Count; i++)
|
||||||
{
|
{
|
||||||
setupRequestsLines.Add($" _requests[{i}] = new DefaultHttpContext();");
|
var entry = entries[i];
|
||||||
setupRequestsLines.Add($" _requests[{i}].RequestServices = CreateServices();");
|
setupRequestsLines.Add($" Requests[{i}] = new DefaultHttpContext();");
|
||||||
setupRequestsLines.Add($" _requests[{i}].Request.Method = \"{entries[i].Method.ToUpperInvariant()}\";");
|
setupRequestsLines.Add($" Requests[{i}].RequestServices = CreateServices();");
|
||||||
setupRequestsLines.Add($" _requests[{i}].Request.Path = \"{entries[i].RequestUrl}\";");
|
|
||||||
|
if (entry.Method != null)
|
||||||
|
{
|
||||||
|
setupRequestsLines.Add($" Requests[{i}].Request.Method = \"{entries[i].Method.ToUpperInvariant()}\";");
|
||||||
|
}
|
||||||
|
|
||||||
|
setupRequestsLines.Add($" Requests[{i}].Request.Path = \"{entries[i].RequestUrl}\";");
|
||||||
}
|
}
|
||||||
|
|
||||||
var setupMatcherLines = new List<string>();
|
var setupMatcherLines = new List<string>();
|
||||||
for (var i = 0; i < entries.Count; i++)
|
for (var i = 0; i < entries.Count; i++)
|
||||||
{
|
{
|
||||||
setupMatcherLines.Add($" builder.AddEndpoint(_endpoints[{i}]);");
|
setupMatcherLines.Add($" builder.AddEndpoint(Endpoints[{i}]);");
|
||||||
}
|
}
|
||||||
|
|
||||||
return string.Format(@"
|
return string.Format(@"
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Routing.Matching;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing.Matching
|
namespace Microsoft.AspNetCore.Routing
|
||||||
{{
|
{{
|
||||||
// This code was generated by the Swaggatherer
|
// This code was generated by the Swaggatherer
|
||||||
public partial class GeneratedBenchmark : MatcherBenchmarkBase
|
public partial class GeneratedBenchmark : EndpointRoutingBenchmarkBase
|
||||||
{{
|
{{
|
||||||
private const int EndpointCount = {3};
|
private const int EndpointCount = {3};
|
||||||
|
|
||||||
private void SetupEndpoints()
|
private void SetupEndpoints()
|
||||||
{{
|
{{
|
||||||
_endpoints = new MatcherEndpoint[{3}];
|
Endpoints = new MatcherEndpoint[{3}];
|
||||||
{0}
|
{0}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
private void SetupRequests()
|
private void SetupRequests()
|
||||||
{{
|
{{
|
||||||
_requests = new HttpContext[{3}];
|
Requests = new HttpContext[{3}];
|
||||||
{1}
|
{1}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
|
@ -63,8 +93,39 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
{2}
|
{2}
|
||||||
return builder.Build();
|
return builder.Build();
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
private MatcherEndpoint CreateEndpoint(string template, string controllerName, string actionName, string httpMethod)
|
||||||
|
{{
|
||||||
|
var requiredValues = new
|
||||||
|
{{
|
||||||
|
area = (string)null,
|
||||||
|
controller = controllerName,
|
||||||
|
action = actionName,
|
||||||
|
page = (string)null
|
||||||
|
}};
|
||||||
|
var defaults = new
|
||||||
|
{{
|
||||||
|
area = (string)null,
|
||||||
|
controller = controllerName,
|
||||||
|
action = actionName,
|
||||||
|
page = (string)null
|
||||||
|
}};
|
||||||
|
|
||||||
|
var metadata = new List<object>();
|
||||||
|
if (httpMethod != null)
|
||||||
|
{{
|
||||||
|
metadata.Add(new HttpMethodMetadata(new string[] {{ httpMethod }}));
|
||||||
|
}}
|
||||||
|
|
||||||
|
return CreateEndpoint(
|
||||||
|
template,
|
||||||
|
defaults: defaults,
|
||||||
|
requiredValues: requiredValues,
|
||||||
|
metadata: metadata,
|
||||||
|
routeName: controllerName);
|
||||||
|
}}
|
||||||
}}
|
}}
|
||||||
}}",
|
}}",
|
||||||
string.Join(Environment.NewLine, setupEndpointsLines),
|
string.Join(Environment.NewLine, setupEndpointsLines),
|
||||||
string.Join(Environment.NewLine, setupRequestsLines),
|
string.Join(Environment.NewLine, setupRequestsLines),
|
||||||
string.Join(Environment.NewLine, setupMatcherLines),
|
string.Join(Environment.NewLine, setupMatcherLines),
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,6 @@
|
||||||
<XunitPackageVersion>2.3.1</XunitPackageVersion>
|
<XunitPackageVersion>2.3.1</XunitPackageVersion>
|
||||||
<XunitRunnerVisualStudioPackageVersion>2.4.0</XunitRunnerVisualStudioPackageVersion>
|
<XunitRunnerVisualStudioPackageVersion>2.4.0</XunitRunnerVisualStudioPackageVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup Label="Package Versions: Pinned" />
|
|
||||||
<Import Project="$(DotNetPackageVersionPropsPath)" Condition=" '$(DotNetPackageVersionPropsPath)' != '' " />
|
<Import Project="$(DotNetPackageVersionPropsPath)" Condition=" '$(DotNetPackageVersionPropsPath)' != '' " />
|
||||||
|
<PropertyGroup Label="Package Versions: Pinned" />
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
using Microsoft.AspNetCore.Routing.Internal;
|
using Microsoft.AspNetCore.Routing.Internal;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
|
||||||
namespace RoutingSample.Web
|
namespace RoutingSample.Web
|
||||||
{
|
{
|
||||||
|
|
@ -78,21 +79,55 @@ namespace RoutingSample.Web
|
||||||
},
|
},
|
||||||
"/withoptionalconstraints/{id:endsWith(_001)?}",
|
"/withoptionalconstraints/{id:endsWith(_001)?}",
|
||||||
"withoptionalconstraints");
|
"withoptionalconstraints");
|
||||||
builder.MapEndpoint(
|
builder.MapEndpoint(
|
||||||
(next) => (httpContext) =>
|
(next) => (httpContext) =>
|
||||||
{
|
|
||||||
using (var writer = new StreamWriter(httpContext.Response.Body, Encoding.UTF8, 1024, leaveOpen: true))
|
|
||||||
{
|
{
|
||||||
var graphWriter = httpContext.RequestServices.GetRequiredService<DfaGraphWriter>();
|
using (var writer = new StreamWriter(httpContext.Response.Body, Encoding.UTF8, 1024, leaveOpen: true))
|
||||||
var dataSource = httpContext.RequestServices.GetRequiredService<CompositeEndpointDataSource>();
|
{
|
||||||
graphWriter.Write(dataSource, writer);
|
var graphWriter = httpContext.RequestServices.GetRequiredService<DfaGraphWriter>();
|
||||||
}
|
var dataSource = httpContext.RequestServices.GetRequiredService<CompositeEndpointDataSource>();
|
||||||
|
graphWriter.Write(dataSource, writer);
|
||||||
|
}
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
},
|
},
|
||||||
"/graph",
|
"/graph",
|
||||||
"DFA Graph",
|
"DFA Graph",
|
||||||
new object[] { new HttpMethodMetadata(new[] { "GET", }) });
|
new object[] { new HttpMethodMetadata(new[] { "GET", }) });
|
||||||
|
builder.MapEndpoint(
|
||||||
|
(next) => (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 { }));
|
||||||
|
},
|
||||||
|
"/WithSingleAsteriskCatchAll/{*path}",
|
||||||
|
"WithSingleAsteriskCatchAll",
|
||||||
|
new object[]
|
||||||
|
{
|
||||||
|
new RouteValuesAddressMetadata(name: "WithSingleAsteriskCatchAll", requiredValues: new RouteValueDictionary()),
|
||||||
|
});
|
||||||
|
builder.MapEndpoint(
|
||||||
|
(next) => (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 { }));
|
||||||
|
},
|
||||||
|
"/WithDoubleAsteriskCatchAll/{**path}",
|
||||||
|
"WithDoubleAsteriskCatchAll",
|
||||||
|
new object[]
|
||||||
|
{
|
||||||
|
new RouteValuesAddressMetadata(name: "WithDoubleAsteriskCatchAll", requiredValues: new RouteValueDictionary())
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Imagine some more stuff here...
|
// Imagine some more stuff here...
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,11 @@ namespace Microsoft.AspNetCore.Routing.Internal
|
||||||
public TextWriter Writer { get; }
|
public TextWriter Writer { get; }
|
||||||
|
|
||||||
public bool Accept(string value)
|
public bool Accept(string value)
|
||||||
|
{
|
||||||
|
return Accept(value, encodeSlashes: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Accept(string value, bool encodeSlashes)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(value))
|
if (string.IsNullOrEmpty(value))
|
||||||
{
|
{
|
||||||
|
|
@ -67,7 +72,7 @@ namespace Microsoft.AspNetCore.Routing.Internal
|
||||||
{
|
{
|
||||||
if (_buffer[i].RequiresEncoding)
|
if (_buffer[i].RequiresEncoding)
|
||||||
{
|
{
|
||||||
_urlEncoder.Encode(Writer, _buffer[i].Value);
|
EncodeValue(_buffer[i].Value);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -88,16 +93,17 @@ namespace Microsoft.AspNetCore.Routing.Internal
|
||||||
UriState = SegmentState.Inside;
|
UriState = SegmentState.Inside;
|
||||||
|
|
||||||
_lastValueOffset = _uri.Length;
|
_lastValueOffset = _uri.Length;
|
||||||
|
|
||||||
// Allow the first segment to have a leading slash.
|
// Allow the first segment to have a leading slash.
|
||||||
// This prevents the leading slash from PathString segments from being encoded.
|
// This prevents the leading slash from PathString segments from being encoded.
|
||||||
if (_uri.Length == 0 && value.Length > 0 && value[0] == '/')
|
if (_uri.Length == 0 && value.Length > 0 && value[0] == '/')
|
||||||
{
|
{
|
||||||
_uri.Append("/");
|
_uri.Append("/");
|
||||||
_urlEncoder.Encode(Writer, value, 1, value.Length - 1);
|
EncodeValue(value, 1, value.Length - 1, encodeSlashes);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_urlEncoder.Encode(Writer, value);
|
EncodeValue(value, encodeSlashes);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -197,6 +203,44 @@ namespace Microsoft.AspNetCore.Routing.Internal
|
||||||
return _uri.ToString();
|
return _uri.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void EncodeValue(string value)
|
||||||
|
{
|
||||||
|
EncodeValue(value, encodeSlashes: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EncodeValue(string value, bool encodeSlashes)
|
||||||
|
{
|
||||||
|
EncodeValue(value, start: 0, characterCount: value.Length, encodeSlashes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For testing
|
||||||
|
internal void EncodeValue(string value, int start, int characterCount, bool encodeSlashes)
|
||||||
|
{
|
||||||
|
// Just encode everything if its ok to encode slashes
|
||||||
|
if (encodeSlashes)
|
||||||
|
{
|
||||||
|
_urlEncoder.Encode(Writer, value, start, characterCount);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
int end;
|
||||||
|
int length = start + characterCount;
|
||||||
|
while ((end = value.IndexOf('/', start, characterCount)) >= 0)
|
||||||
|
{
|
||||||
|
_urlEncoder.Encode(Writer, value, start, end - start);
|
||||||
|
_uri.Append("/");
|
||||||
|
|
||||||
|
start = end + 1;
|
||||||
|
characterCount = length - start;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (end < 0 && characterCount >= 0)
|
||||||
|
{
|
||||||
|
_urlEncoder.Encode(Writer, value, start, length - start);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private string DebuggerToString()
|
private string DebuggerToString()
|
||||||
{
|
{
|
||||||
return string.Format("{{Accepted: '{0}' Buffered: '{1}'}}", _uri, string.Join("", _buffer));
|
return string.Format("{{Accepted: '{0}' Buffered: '{1}'}}", _uri, string.Join("", _buffer));
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,17 @@ namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
|
|
||||||
var startIndex = 0;
|
var startIndex = 0;
|
||||||
var endIndex = parameter.Length - 1;
|
var endIndex = parameter.Length - 1;
|
||||||
|
var encodeSlashes = true;
|
||||||
|
|
||||||
var parameterKind = RoutePatternParameterKind.Standard;
|
var parameterKind = RoutePatternParameterKind.Standard;
|
||||||
if (parameter[0] == '*')
|
|
||||||
|
if (parameter.StartsWith("**", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
encodeSlashes = false;
|
||||||
|
parameterKind = RoutePatternParameterKind.CatchAll;
|
||||||
|
startIndex += 2;
|
||||||
|
}
|
||||||
|
else if (parameter[0] == '*')
|
||||||
{
|
{
|
||||||
parameterKind = RoutePatternParameterKind.CatchAll;
|
parameterKind = RoutePatternParameterKind.CatchAll;
|
||||||
startIndex++;
|
startIndex++;
|
||||||
|
|
@ -79,7 +87,12 @@ namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
defaultValue = parameter.Substring(currentIndex + 1, endIndex - currentIndex);
|
defaultValue = parameter.Substring(currentIndex + 1, endIndex - currentIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new RoutePatternParameterPart(parameterName, defaultValue, parameterKind, parseResults.Constraints.ToArray());
|
return new RoutePatternParameterPart(
|
||||||
|
parameterName,
|
||||||
|
defaultValue,
|
||||||
|
parameterKind,
|
||||||
|
parseResults.Constraints.ToArray(),
|
||||||
|
encodeSlashes);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ConstraintParseResults ParseConstraints(
|
private static ConstraintParseResults ParseConstraints(
|
||||||
|
|
@ -237,7 +250,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
public readonly int CurrentIndex;
|
public readonly int CurrentIndex;
|
||||||
|
|
||||||
public readonly IReadOnlyList<RoutePatternConstraintReference> Constraints;
|
public readonly IReadOnlyList<RoutePatternConstraintReference> Constraints;
|
||||||
|
|
||||||
public ConstraintParseResults(int currentIndex, IReadOnlyList<RoutePatternConstraintReference> constraints)
|
public ConstraintParseResults(int currentIndex, IReadOnlyList<RoutePatternConstraintReference> constraints)
|
||||||
{
|
{
|
||||||
CurrentIndex = currentIndex;
|
CurrentIndex = currentIndex;
|
||||||
|
|
|
||||||
|
|
@ -284,7 +284,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
{
|
{
|
||||||
var segment = VisitSegment(updatedSegments[i]);
|
var segment = VisitSegment(updatedSegments[i]);
|
||||||
updatedSegments[i] = segment;
|
updatedSegments[i] = segment;
|
||||||
|
|
||||||
for (var j = 0; j < segment.Parts.Count; j++)
|
for (var j = 0; j < segment.Parts.Count; j++)
|
||||||
{
|
{
|
||||||
if (segment.Parts[j] is RoutePatternParameterPart parameter)
|
if (segment.Parts[j] is RoutePatternParameterPart parameter)
|
||||||
|
|
@ -339,7 +339,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
|
|
||||||
@default = newDefault;
|
@default = newDefault;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parameter.Default != null)
|
if (parameter.Default != null)
|
||||||
{
|
{
|
||||||
updatedDefaults.Add(parameter.Name, parameter.Default);
|
updatedDefaults.Add(parameter.Name, parameter.Default);
|
||||||
|
|
@ -361,7 +361,8 @@ namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
parameter.Name,
|
parameter.Name,
|
||||||
@default,
|
@default,
|
||||||
parameter.ParameterKind,
|
parameter.ParameterKind,
|
||||||
(IEnumerable<RoutePatternConstraintReference>)parameterConstraints ?? Array.Empty<RoutePatternConstraintReference>());
|
(IEnumerable<RoutePatternConstraintReference>)parameterConstraints ?? Array.Empty<RoutePatternConstraintReference>(),
|
||||||
|
parameter.EncodeSlashes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -624,7 +625,22 @@ namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
RoutePatternParameterKind parameterKind,
|
RoutePatternParameterKind parameterKind,
|
||||||
IEnumerable<RoutePatternConstraintReference> constraints)
|
IEnumerable<RoutePatternConstraintReference> constraints)
|
||||||
{
|
{
|
||||||
return new RoutePatternParameterPart(parameterName, @default, parameterKind, constraints.ToArray());
|
return ParameterPartCore(parameterName, @default, parameterKind, constraints, encodeSlashes: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RoutePatternParameterPart ParameterPartCore(
|
||||||
|
string parameterName,
|
||||||
|
object @default,
|
||||||
|
RoutePatternParameterKind parameterKind,
|
||||||
|
IEnumerable<RoutePatternConstraintReference> constraints,
|
||||||
|
bool encodeSlashes)
|
||||||
|
{
|
||||||
|
return new RoutePatternParameterPart(
|
||||||
|
parameterName,
|
||||||
|
@default,
|
||||||
|
parameterKind,
|
||||||
|
constraints.ToArray(),
|
||||||
|
encodeSlashes);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,16 @@ namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
object @default,
|
object @default,
|
||||||
RoutePatternParameterKind parameterKind,
|
RoutePatternParameterKind parameterKind,
|
||||||
RoutePatternConstraintReference[] constraints)
|
RoutePatternConstraintReference[] constraints)
|
||||||
|
: this(parameterName, @default, parameterKind, constraints, encodeSlashes: true)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
internal RoutePatternParameterPart(
|
||||||
|
string parameterName,
|
||||||
|
object @default,
|
||||||
|
RoutePatternParameterKind parameterKind,
|
||||||
|
RoutePatternConstraintReference[] constraints,
|
||||||
|
bool encodeSlashes)
|
||||||
: base(RoutePatternPartKind.Parameter)
|
: base(RoutePatternPartKind.Parameter)
|
||||||
{
|
{
|
||||||
// See #475 - this code should have some asserts, but it can't because of the design of RouteParameterParser.
|
// See #475 - this code should have some asserts, but it can't because of the design of RouteParameterParser.
|
||||||
|
|
@ -27,6 +37,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
Default = @default;
|
Default = @default;
|
||||||
ParameterKind = parameterKind;
|
ParameterKind = parameterKind;
|
||||||
Constraints = constraints;
|
Constraints = constraints;
|
||||||
|
EncodeSlashes = encodeSlashes;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -34,6 +45,11 @@ namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IReadOnlyList<RoutePatternConstraintReference> Constraints { get; }
|
public IReadOnlyList<RoutePatternConstraintReference> Constraints { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the value indicating if slashes in current parameter's value should be encoded.
|
||||||
|
/// </summary>
|
||||||
|
public bool EncodeSlashes { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the default value of this route parameter. May be null.
|
/// Gets the default value of this route parameter. May be null.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Routing.Internal;
|
using Microsoft.AspNetCore.Routing.Internal;
|
||||||
using Microsoft.AspNetCore.Routing.Patterns;
|
using Microsoft.AspNetCore.Routing.Patterns;
|
||||||
using Microsoft.Extensions.ObjectPool;
|
using Microsoft.Extensions.ObjectPool;
|
||||||
|
|
@ -223,12 +224,12 @@ namespace Microsoft.AspNetCore.Routing.Template
|
||||||
// If the value is not accepted, it is null or empty value in the
|
// If the value is not accepted, it is null or empty value in the
|
||||||
// middle of the segment. We accept this if the parameter is an
|
// middle of the segment. We accept this if the parameter is an
|
||||||
// optional parameter and it is preceded by an optional seperator.
|
// optional parameter and it is preceded by an optional seperator.
|
||||||
// I this case, we need to remove the optional seperator that we
|
// In this case, we need to remove the optional seperator that we
|
||||||
// have added to the URI
|
// have added to the URI
|
||||||
// Example: template = {id}.{format?}. parameters: id=5
|
// Example: template = {id}.{format?}. parameters: id=5
|
||||||
// In this case after we have generated "5.", we wont find any value
|
// In this case after we have generated "5.", we wont find any value
|
||||||
// for format, so we remove '.' and generate 5.
|
// for format, so we remove '.' and generate 5.
|
||||||
if (!context.Accept(converted))
|
if (!context.Accept(converted, parameterPart.EncodeSlashes))
|
||||||
{
|
{
|
||||||
if (j != 0 && parameterPart.IsOptional && (separatorPart = segment.Parts[j - 1] as RoutePatternSeparatorPart) != null)
|
if (j != 0 && parameterPart.IsOptional && (separatorPart = segment.Parts[j - 1] as RoutePatternSeparatorPart) != null)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,54 @@ namespace Microsoft.AspNetCore.Routing.FunctionalTests
|
||||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("/WithSingleAsteriskCatchAll/a/b/c", "Link: /WithSingleAsteriskCatchAll/a%2Fb%2Fc")]
|
||||||
|
[InlineData("/WithSingleAsteriskCatchAll/a/b b1/c c1", "Link: /WithSingleAsteriskCatchAll/a%2Fb%20b1%2Fc%20c1")]
|
||||||
|
public async Task GeneratesLink_ToEndpointWithSingleAsteriskCatchAllParameter_EncodesValue(
|
||||||
|
string url,
|
||||||
|
string expected)
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var response = await _client.GetAsync(url);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
Assert.NotNull(response.Content);
|
||||||
|
var actualContent = await response.Content.ReadAsStringAsync();
|
||||||
|
Assert.Equal(expected, actualContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("/WithDoubleAsteriskCatchAll/a/b/c", "Link: /WithDoubleAsteriskCatchAll/a/b/c")]
|
||||||
|
[InlineData("/WithDoubleAsteriskCatchAll/a/b/c/", "Link: /WithDoubleAsteriskCatchAll/a/b/c/")]
|
||||||
|
[InlineData("/WithDoubleAsteriskCatchAll/a//b/c", "Link: /WithDoubleAsteriskCatchAll/a//b/c")]
|
||||||
|
public async Task GeneratesLink_ToEndpointWithDoubleAsteriskCatchAllParameter_DoesNotEncodeSlashes(
|
||||||
|
string url,
|
||||||
|
string expected)
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var response = await _client.GetAsync(url);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
Assert.NotNull(response.Content);
|
||||||
|
var actualContent = await response.Content.ReadAsStringAsync();
|
||||||
|
Assert.Equal(expected, actualContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GeneratesLink_ToEndpointWithDoubleAsteriskCatchAllParameter_EncodesContentOtherThanSlashes()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var response = await _client.GetAsync("/WithDoubleAsteriskCatchAll/a/b b1/c c1");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
Assert.NotNull(response.Content);
|
||||||
|
var actualContent = await response.Content.ReadAsStringAsync();
|
||||||
|
Assert.Equal("Link: /WithDoubleAsteriskCatchAll/a/b%20b1/c%20c1", actualContent);
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_testServer.Dispose();
|
_testServer.Dispose();
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,76 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
Assert.Equal("/Home/Index", link);
|
Assert.Equal("/Home/Index", link);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetLink_EncodesIntermediate_DefaultValues()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var endpoint = EndpointFactory.CreateMatcherEndpoint("{p1}/{p2=a b}/{p3=foo}");
|
||||||
|
var linkGenerator = CreateLinkGenerator(endpoint);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var link = linkGenerator.GetLink(new { p1 = "Home", p3 = "bar" });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("/Home/a%20b/bar", link);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("a/b/c", "/Home/Index/a%2Fb%2Fc")]
|
||||||
|
[InlineData("a/b b1/c c1", "/Home/Index/a%2Fb%20b1%2Fc%20c1")]
|
||||||
|
public void GetLink_EncodesValue_OfSingleAsteriskCatchAllParameter(string routeValue, string expected)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}/{*path}");
|
||||||
|
var linkGenerator = CreateLinkGenerator(endpoint);
|
||||||
|
var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Index" });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var link = linkGenerator.GetLink(httpContext, new { path = routeValue });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expected, link);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("/", "/Home/Index//")]
|
||||||
|
[InlineData("a", "/Home/Index/a")]
|
||||||
|
[InlineData("a/", "/Home/Index/a/")]
|
||||||
|
[InlineData("a/b", "/Home/Index/a/b")]
|
||||||
|
[InlineData("a/b/c", "/Home/Index/a/b/c")]
|
||||||
|
[InlineData("a/b/cc", "/Home/Index/a/b/cc")]
|
||||||
|
[InlineData("a/b/c/", "/Home/Index/a/b/c/")]
|
||||||
|
[InlineData("a/b/c//", "/Home/Index/a/b/c//")]
|
||||||
|
[InlineData("a//b//c", "/Home/Index/a//b//c")]
|
||||||
|
public void GetLink_DoesNotEncodeSlashes_OfDoubleAsteriskCatchAllParameter(string routeValue, string expected)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}/{**path}");
|
||||||
|
var linkGenerator = CreateLinkGenerator(endpoint);
|
||||||
|
var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Index" });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var link = linkGenerator.GetLink(httpContext, new { path = routeValue });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expected, link);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetLink_EncodesContentOtherThanSlashes_OfDoubleAsteriskCatchAllParameter()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}/{**path}");
|
||||||
|
var linkGenerator = CreateLinkGenerator(endpoint);
|
||||||
|
var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Index" });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var link = linkGenerator.GetLink(httpContext, new { path = "a/b b1/c c1" });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("/Home/Index/a/b%20b1/c%20c1", link);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GetLink_EncodesValues()
|
public void GetLink_EncodesValues()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
// 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.Extensions.WebEncoders.Testing;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing.Internal
|
||||||
|
{
|
||||||
|
public class UriBuildingContextTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void EncodeValue_EncodesEntireValue_WhenEncodeSlashes_IsFalse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var urlTestEncoder = new UrlTestEncoder();
|
||||||
|
var value = "a/b b1/c";
|
||||||
|
var expected = "/UrlEncode[[a/b b1/c]]";
|
||||||
|
var uriBuilldingContext = new UriBuildingContext(urlTestEncoder);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
uriBuilldingContext.EncodeValue(value, 0, value.Length, encodeSlashes: true);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expected, uriBuilldingContext.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void EncodeValue_EncodesOnlySlashes_WhenEncodeSlashes_IsFalse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var urlTestEncoder = new UrlTestEncoder();
|
||||||
|
var value = "a/b b1/c";
|
||||||
|
var expected = "/UrlEncode[[a]]/UrlEncode[[b b1]]/UrlEncode[[c]]";
|
||||||
|
var uriBuilldingContext = new UriBuildingContext(urlTestEncoder);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
uriBuilldingContext.EncodeValue(value, 0, value.Length, encodeSlashes: false);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expected, uriBuilldingContext.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("a/b b1/c", 0, 2, "/UrlEncode[[a]]/")]
|
||||||
|
[InlineData("a/b b1/c", 3, 4, "/UrlEncode[[ b1]]/")]
|
||||||
|
[InlineData("a/b b1/c", 3, 5, "/UrlEncode[[ b1]]/UrlEncode[[c]]")]
|
||||||
|
[InlineData("a/b b1/c/", 8, 1, "/")]
|
||||||
|
[InlineData("/", 0, 1, "/")]
|
||||||
|
[InlineData("/a", 0, 2, "/UrlEncode[[a]]")]
|
||||||
|
[InlineData("a", 0, 1, "/UrlEncode[[a]]")]
|
||||||
|
[InlineData("a/", 0, 2, "/UrlEncode[[a]]/")]
|
||||||
|
public void EncodeValue_EncodesOnlySlashes_WithinSubsegment_WhenEncodeSlashes_IsFalse(
|
||||||
|
string value,
|
||||||
|
int startIndex,
|
||||||
|
int characterCount,
|
||||||
|
string expected)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var urlTestEncoder = new UrlTestEncoder();
|
||||||
|
var uriBuilldingContext = new UriBuildingContext(urlTestEncoder);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
uriBuilldingContext.EncodeValue(value, startIndex, characterCount, encodeSlashes: false);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expected, uriBuilldingContext.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Microsoft.AspNetCore.Routing.Constraints;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing.Patterns
|
namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
|
|
@ -919,6 +920,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
[InlineData("", "")]
|
[InlineData("", "")]
|
||||||
[InlineData("?", "")]
|
[InlineData("?", "")]
|
||||||
[InlineData("*", "")]
|
[InlineData("*", "")]
|
||||||
|
[InlineData("**", "")]
|
||||||
[InlineData(" ", " ")]
|
[InlineData(" ", " ")]
|
||||||
[InlineData("\t", "\t")]
|
[InlineData("\t", "\t")]
|
||||||
[InlineData("#!@#$%Q@#@%", "#!@#$%Q@#@%")]
|
[InlineData("#!@#$%Q@#@%", "#!@#$%Q@#@%")]
|
||||||
|
|
@ -936,6 +938,134 @@ namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
Assert.Null(templatePart.Default);
|
Assert.Null(templatePart.Default);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_WithSingleAsteriskCatchAll_IsParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var parameterPart = ParseParameter("*path");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("path", parameterPart.Name);
|
||||||
|
Assert.True(parameterPart.IsCatchAll);
|
||||||
|
Assert.Equal(RoutePatternParameterKind.CatchAll, parameterPart.ParameterKind);
|
||||||
|
Assert.True(parameterPart.EncodeSlashes);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_WithSingleAsteriskCatchAll_AndDefaultValue_IsParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var parameterPart = ParseParameter("*path=a/b/c");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("path", parameterPart.Name);
|
||||||
|
Assert.True(parameterPart.IsCatchAll);
|
||||||
|
Assert.NotNull(parameterPart.Default);
|
||||||
|
Assert.Equal("a/b/c", parameterPart.Default.ToString());
|
||||||
|
Assert.Equal(RoutePatternParameterKind.CatchAll, parameterPart.ParameterKind);
|
||||||
|
Assert.True(parameterPart.EncodeSlashes);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_WithSingleAsteriskCatchAll_AndConstraints_IsParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var constraintContent = "regex(^(/[^/ ]*)+/?$)";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var parameterPart = ParseParameter($"*path:{constraintContent}");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("path", parameterPart.Name);
|
||||||
|
Assert.True(parameterPart.IsCatchAll);
|
||||||
|
Assert.Equal(RoutePatternParameterKind.CatchAll, parameterPart.ParameterKind);
|
||||||
|
var constraintReference = Assert.Single(parameterPart.Constraints);
|
||||||
|
Assert.Equal(constraintContent, constraintReference.Content);
|
||||||
|
Assert.True(parameterPart.EncodeSlashes);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_WithSingleAsteriskCatchAll_AndConstraints_AndDefaultValue_IsParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var constraintContent = "regex(^(/[^/ ]*)+/?$)";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var parameterPart = ParseParameter($"*path:{constraintContent}=a/b/c");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("path", parameterPart.Name);
|
||||||
|
Assert.True(parameterPart.IsCatchAll);
|
||||||
|
Assert.Equal(RoutePatternParameterKind.CatchAll, parameterPart.ParameterKind);
|
||||||
|
var constraintReference = Assert.Single(parameterPart.Constraints);
|
||||||
|
Assert.Equal(constraintContent, constraintReference.Content);
|
||||||
|
Assert.NotNull(parameterPart.Default);
|
||||||
|
Assert.Equal("a/b/c", parameterPart.Default.ToString());
|
||||||
|
Assert.True(parameterPart.EncodeSlashes);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_WithDoubleAsteriskCatchAll_IsParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var parameterPart = ParseParameter("**path");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("path", parameterPart.Name);
|
||||||
|
Assert.True(parameterPart.IsCatchAll);
|
||||||
|
Assert.False(parameterPart.EncodeSlashes);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_WithDoubleAsteriskCatchAll_AndDefaultValue_IsParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var parameterPart = ParseParameter("**path=a/b/c");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("path", parameterPart.Name);
|
||||||
|
Assert.True(parameterPart.IsCatchAll);
|
||||||
|
Assert.NotNull(parameterPart.Default);
|
||||||
|
Assert.Equal("a/b/c", parameterPart.Default.ToString());
|
||||||
|
Assert.False(parameterPart.EncodeSlashes);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_WithDoubleAsteriskCatchAll_AndConstraints_IsParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var constraintContent = "regex(^(/[^/ ]*)+/?$)";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var parameterPart = ParseParameter($"**path:{constraintContent}");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("path", parameterPart.Name);
|
||||||
|
Assert.True(parameterPart.IsCatchAll);
|
||||||
|
Assert.False(parameterPart.EncodeSlashes);
|
||||||
|
var constraintReference = Assert.Single(parameterPart.Constraints);
|
||||||
|
Assert.Equal(constraintContent, constraintReference.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_WithDoubleAsteriskCatchAll_AndConstraints_AndDefaultValue_IsParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var constraintContent = "regex(^(/[^/ ]*)+/?$)";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var parameterPart = ParseParameter($"**path:{constraintContent}=a/b/c");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("path", parameterPart.Name);
|
||||||
|
Assert.True(parameterPart.IsCatchAll);
|
||||||
|
Assert.False(parameterPart.EncodeSlashes);
|
||||||
|
var constraintReference = Assert.Single(parameterPart.Constraints);
|
||||||
|
Assert.Equal(constraintContent, constraintReference.Content);
|
||||||
|
Assert.NotNull(parameterPart.Default);
|
||||||
|
Assert.Equal("a/b/c", parameterPart.Default.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
private RoutePatternParameterPart ParseParameter(string routeParameter)
|
private RoutePatternParameterPart ParseParameter(string routeParameter)
|
||||||
{
|
{
|
||||||
// See: #475 - these tests don't pass the 'whole' text.
|
// See: #475 - these tests don't pass the 'whole' text.
|
||||||
|
|
|
||||||
|
|
@ -470,7 +470,6 @@ namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("{**}", "*")]
|
|
||||||
[InlineData("{a*}", "a*")]
|
[InlineData("{a*}", "a*")]
|
||||||
[InlineData("{*a*}", "a*")]
|
[InlineData("{*a*}", "a*")]
|
||||||
[InlineData("{*a*:int}", "a*")]
|
[InlineData("{*a*:int}", "a*")]
|
||||||
|
|
|
||||||
|
|
@ -626,7 +626,6 @@ namespace Microsoft.AspNetCore.Routing.Template.Tests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("{**}", "*")]
|
|
||||||
[InlineData("{a*}", "a*")]
|
[InlineData("{a*}", "a*")]
|
||||||
[InlineData("{*a*}", "a*")]
|
[InlineData("{*a*}", "a*")]
|
||||||
[InlineData("{*a*:int}", "a*")]
|
[InlineData("{*a*:int}", "a*")]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue