diff --git a/Routing.sln b/Routing.sln
index 851dea0a..408a81c3 100644
--- a/Routing.sln
+++ b/Routing.sln
@@ -1,4 +1,4 @@
-Microsoft Visual Studio Solution File, Format Version 12.00
+Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27106.3000
MinimumVisualStudioVersion = 15.0.26730.03
@@ -22,6 +22,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RoutingSample.Web", "sample
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C430C499-382D-47BD-B351-CF8F89C08CD2}"
ProjectSection(SolutionItems) = preProject
+ build\dependencies.props = build\dependencies.props
global.json = global.json
NuGet.config = NuGet.config
EndProjectSection
@@ -51,6 +52,8 @@ 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("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Swaggatherer", "benchmarks\Swaggatherer\Swaggatherer.csproj", "{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -151,18 +154,6 @@ Global
{F3D86714-4E64-41A6-9B36-A47B3683CF5D}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{F3D86714-4E64-41A6-9B36-A47B3683CF5D}.Release|x86.ActiveCfg = Release|Any CPU
{F3D86714-4E64-41A6-9B36-A47B3683CF5D}.Release|x86.Build.0 = Release|Any CPU
- {91F47A60-9A78-4968-B10D-157D9BFAC37F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {91F47A60-9A78-4968-B10D-157D9BFAC37F}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {91F47A60-9A78-4968-B10D-157D9BFAC37F}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
- {91F47A60-9A78-4968-B10D-157D9BFAC37F}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
- {91F47A60-9A78-4968-B10D-157D9BFAC37F}.Debug|x86.ActiveCfg = Debug|Any CPU
- {91F47A60-9A78-4968-B10D-157D9BFAC37F}.Debug|x86.Build.0 = Debug|Any CPU
- {91F47A60-9A78-4968-B10D-157D9BFAC37F}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {91F47A60-9A78-4968-B10D-157D9BFAC37F}.Release|Any CPU.Build.0 = Release|Any CPU
- {91F47A60-9A78-4968-B10D-157D9BFAC37F}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
- {91F47A60-9A78-4968-B10D-157D9BFAC37F}.Release|Mixed Platforms.Build.0 = Release|Any CPU
- {91F47A60-9A78-4968-B10D-157D9BFAC37F}.Release|x86.ActiveCfg = Release|Any CPU
- {91F47A60-9A78-4968-B10D-157D9BFAC37F}.Release|x86.Build.0 = Release|Any CPU
{4EBE7A6F-3183-4A7D-B3D7-A6A9EC3867A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4EBE7A6F-3183-4A7D-B3D7-A6A9EC3867A5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4EBE7A6F-3183-4A7D-B3D7-A6A9EC3867A5}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
@@ -175,6 +166,30 @@ Global
{4EBE7A6F-3183-4A7D-B3D7-A6A9EC3867A5}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{4EBE7A6F-3183-4A7D-B3D7-A6A9EC3867A5}.Release|x86.ActiveCfg = Release|Any CPU
{4EBE7A6F-3183-4A7D-B3D7-A6A9EC3867A5}.Release|x86.Build.0 = Release|Any CPU
+ {91F47A60-9A78-4968-B10D-157D9BFAC37F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {91F47A60-9A78-4968-B10D-157D9BFAC37F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {91F47A60-9A78-4968-B10D-157D9BFAC37F}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {91F47A60-9A78-4968-B10D-157D9BFAC37F}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {91F47A60-9A78-4968-B10D-157D9BFAC37F}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {91F47A60-9A78-4968-B10D-157D9BFAC37F}.Debug|x86.Build.0 = Debug|Any CPU
+ {91F47A60-9A78-4968-B10D-157D9BFAC37F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {91F47A60-9A78-4968-B10D-157D9BFAC37F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {91F47A60-9A78-4968-B10D-157D9BFAC37F}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {91F47A60-9A78-4968-B10D-157D9BFAC37F}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {91F47A60-9A78-4968-B10D-157D9BFAC37F}.Release|x86.ActiveCfg = Release|Any CPU
+ {91F47A60-9A78-4968-B10D-157D9BFAC37F}.Release|x86.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
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -188,8 +203,9 @@ Global
{741B0B05-CE96-473B-B962-6B0A347DF79A} = {95359B4B-4C85-4B44-A75B-0621905C4CF6}
{5C73140B-41F3-466F-A07B-3614E4D80DF9} = {95359B4B-4C85-4B44-A75B-0621905C4CF6}
{F3D86714-4E64-41A6-9B36-A47B3683CF5D} = {D5F39F59-5725-4127-82E7-67028D006185}
- {91F47A60-9A78-4968-B10D-157D9BFAC37F} = {7F5914E2-C63F-4759-898E-462804357C90}
{4EBE7A6F-3183-4A7D-B3D7-A6A9EC3867A5} = {C3ADD55B-B9C7-4061-8AD4-6A70D1AE3B2E}
+ {91F47A60-9A78-4968-B10D-157D9BFAC37F} = {7F5914E2-C63F-4759-898E-462804357C90}
+ {990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D} = {D5F39F59-5725-4127-82E7-67028D006185}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {36C8D815-B7F1-479D-894B-E606FB8DECDA}
diff --git a/benchmarks/Swaggatherer/Program.cs b/benchmarks/Swaggatherer/Program.cs
new file mode 100644
index 00000000..6aa30e25
--- /dev/null
+++ b/benchmarks/Swaggatherer/Program.cs
@@ -0,0 +1,14 @@
+// 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 Swaggatherer
+{
+ internal static class Program
+ {
+ public static void Main(string[] args)
+ {
+ var application = new SwaggathererApplication();
+ application.Execute(args);
+ }
+ }
+}
diff --git a/benchmarks/Swaggatherer/README.md b/benchmarks/Swaggatherer/README.md
new file mode 100644
index 00000000..aed8bce6
--- /dev/null
+++ b/benchmarks/Swaggatherer/README.md
@@ -0,0 +1,23 @@
+# Swaggatherer (Swagger + Gatherer)
+
+This is a cli tool that can generate a routing benchmark using a Swagger 2.0 spec as an input.
+
+## Usage
+
+Generate a benchmark from a swagger file:
+```
+dotnet run -- -i swagger.json -o MyGeneratedBenchark.generated.cs
+```
+
+Generate a benchmark from a directory of swagger files:
+```
+dotnet run -- -d /some/directory -o MyGeneratedBenchark.generated.cs
+```
+
+The directory mode will recursively search for `.json` files.
+
+## Resources
+
+A big repository of swagger docs: https://github.com/APIs-guru/openapi-directory
+Swagger editor + yaml <-> json conversion tool: https://editor2.swagger.io
+Azure's official swagger docs: https://github.com/Azure/azure-rest-api-specs
\ No newline at end of file
diff --git a/benchmarks/Swaggatherer/RouteEntry.cs b/benchmarks/Swaggatherer/RouteEntry.cs
new file mode 100644
index 00000000..6aea6f32
--- /dev/null
+++ b/benchmarks/Swaggatherer/RouteEntry.cs
@@ -0,0 +1,15 @@
+// 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.Template;
+
+namespace Swaggatherer
+{
+ internal class RouteEntry
+ {
+ public RouteTemplate Template { get; set; }
+ public string Method { get; set; }
+ public decimal Precedence { get; set; }
+ public string RequestUrl { get; set; }
+ }
+}
diff --git a/benchmarks/Swaggatherer/Swaggatherer.csproj b/benchmarks/Swaggatherer/Swaggatherer.csproj
new file mode 100644
index 00000000..6874d1d5
--- /dev/null
+++ b/benchmarks/Swaggatherer/Swaggatherer.csproj
@@ -0,0 +1,17 @@
+
+
+
+ Exe
+ netcoreapp2.0
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/benchmarks/Swaggatherer/SwaggathererApplication.cs b/benchmarks/Swaggatherer/SwaggathererApplication.cs
new file mode 100644
index 00000000..570ec6d3
--- /dev/null
+++ b/benchmarks/Swaggatherer/SwaggathererApplication.cs
@@ -0,0 +1,250 @@
+// 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.IO;
+using System.Text;
+using Microsoft.AspNetCore.Routing.Template;
+using Microsoft.Extensions.CommandLineUtils;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Swaggatherer
+{
+ internal class SwaggathererApplication : CommandLineApplication
+ {
+ public SwaggathererApplication()
+ {
+ Invoke = InvokeCore;
+
+ Input = Option("-i", "input swagger 2.0 JSON file", CommandOptionType.MultipleValue);
+ InputDirectory = Option("-d", "input directory", CommandOptionType.SingleValue);
+ Output = Option("-o", "output", CommandOptionType.SingleValue);
+
+ HelpOption("-h|--help");
+ }
+
+ public CommandOption Input { get; }
+
+ public CommandOption InputDirectory { get; }
+
+ public CommandOption Output { get; }
+
+ private int InvokeCore()
+ {
+ if (!Input.HasValue() && !InputDirectory.HasValue())
+ {
+ ShowHelp();
+ return 1;
+ }
+
+ if (Input.HasValue() && InputDirectory.HasValue())
+ {
+ ShowHelp();
+ return 1;
+ }
+
+ if (!Output.HasValue())
+ {
+ Output.Values.Add("Out.generated.cs");
+ }
+
+ if (InputDirectory.HasValue())
+ {
+ Input.Values.AddRange(Directory.EnumerateFiles(InputDirectory.Value(), "*.json", SearchOption.AllDirectories));
+ }
+
+ var entries = new List();
+ for (var i = 0; i < Input.Values.Count; i++)
+ {
+ var input = ReadInput(Input.Values[i]);
+ ParseEntries(input, entries);
+ }
+
+ // We don't yet want to support complex segments.
+ for (var i = entries.Count - 1; i >= 0; i--)
+ {
+ if (HasComplexSegment(entries[i]))
+ {
+ Out.WriteLine("Skipping route with complex segment: " + entries[i].Template.TemplateText);
+ entries.RemoveAt(i);
+ break;
+ }
+ }
+
+ // The data that we're provided by might be unambiguous.
+ // Remove any routes that would be ambiguous in our system.
+ var routesByPrecedence = new Dictionary>();
+ for (var i = entries.Count - 1; i >= 0; i--)
+ {
+ var entry = entries[i];
+ var precedence = RoutePrecedence.ComputeInbound(entries[i].Template);
+
+ if (!routesByPrecedence.TryGetValue(precedence, out var matches))
+ {
+ matches = new List();
+ routesByPrecedence.Add(precedence, matches);
+ }
+
+ if (IsDuplicateTemplate(entry, matches))
+ {
+ Out.WriteLine("Duplicate route template: " + entries[i].Template.TemplateText);
+ entries.RemoveAt(i);
+ continue;
+ }
+
+ matches.Add(entry);
+ }
+
+ // We're not too sophisticated with how we generate parameter values, just hoping for
+ // the best. For parameters we generate a segment that is the same length as the parameter name
+ // but with a minimum of 5 characters to avoid collisions.
+ for (var i = entries.Count - 1; i >= 0; i--)
+ {
+ entries[i].RequestUrl = GenerateRequestUrl(entries[i].Template);
+ if (entries[i].RequestUrl == null)
+ {
+ Out.WriteLine("Failed to create a request for: " + entries[i].Template.TemplateText);
+ entries.RemoveAt(i);
+ continue;
+ }
+ }
+
+ Sort(entries);
+
+ var text = Template.Execute(entries);
+ File.WriteAllText(Output.Value(), text);
+ return 0;
+ }
+
+ private JObject ReadInput(string input)
+ {
+ using (var reader = File.OpenText(input))
+ {
+ try
+ {
+ return JObject.Load(new JsonTextReader(reader));
+ }
+ catch (JsonReaderException ex)
+ {
+ Out.WriteLine("Error reading: {0}");
+ Out.WriteLine(ex);
+ return new JObject();
+ }
+ }
+ }
+
+ private static void ParseEntries(JObject input, List entries)
+ {
+ var basePath = "";
+ if (input["basePath"] is JProperty basePathProperty)
+ {
+ basePath = basePathProperty.Value();
+ }
+
+ if (input["paths"] is JObject paths)
+ {
+ foreach (var path in paths.Properties())
+ {
+ foreach (var method in ((JObject)path.Value).Properties())
+ {
+ var template = basePath + path.Name;
+ var parsed = TemplateParser.Parse(template);
+ entries.Add(new RouteEntry()
+ {
+ Method = method.Name,
+ Template = parsed,
+ Precedence = RoutePrecedence.ComputeInbound(parsed),
+ });
+ }
+ }
+ }
+ }
+
+ private bool HasComplexSegment(RouteEntry entry)
+ {
+ for (var i = 0; i < entry.Template.Segments.Count; i++)
+ {
+ if (!entry.Template.Segments[i].IsSimple)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static bool IsDuplicateTemplate(RouteEntry entry, List others)
+ {
+ for (var j = 0; j < others.Count; j++)
+ {
+ // This is another route with the same precedence. It is guaranteed to have the same number of segments
+ // of the same kinds and in the same order. We just need to check the literals.
+ var other = others[j];
+
+ var isSame = true;
+ for (var k = 0; k < entry.Template.Segments.Count; k++)
+ {
+ if (!string.Equals(
+ entry.Template.Segments[k].Parts[0].Text,
+ other.Template.Segments[k].Parts[0].Text,
+ StringComparison.OrdinalIgnoreCase))
+ {
+ isSame = false;
+ break;
+ }
+ }
+
+ if (isSame)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static void Sort(List entries)
+ {
+ // We need to sort these in precedence order for the linear matchers.
+ entries.Sort((x, y) =>
+ {
+ var comparison = RoutePrecedence.ComputeInbound(x.Template).CompareTo(RoutePrecedence.ComputeInbound(y.Template));
+ if (comparison != 0)
+ {
+ return comparison;
+ }
+
+ return x.Template.TemplateText.CompareTo(y.Template.TemplateText);
+ });
+ }
+
+ private static string GenerateRequestUrl(RouteTemplate template)
+ {
+ if (template.Segments.Count == 0)
+ {
+ return "/";
+ }
+
+ var url = new StringBuilder();
+ for (var i = 0; i < template.Segments.Count; i++)
+ {
+ // We don't yet handle complex segments
+ var part = template.Segments[i].Parts[0];
+
+ url.Append("/");
+ url.Append(part.IsLiteral ? part.Text : GenerateParameterValue(part));
+ }
+
+ return url.ToString();
+ }
+
+ private static string GenerateParameterValue(TemplatePart part)
+ {
+ var text = Guid.NewGuid().ToString();
+ var length = Math.Min(text.Length, Math.Max(5, part.Name.Length));
+ return text.Substring(0, length);
+ }
+ }
+}
diff --git a/benchmarks/Swaggatherer/Template.cs b/benchmarks/Swaggatherer/Template.cs
new file mode 100644
index 00000000..172ea559
--- /dev/null
+++ b/benchmarks/Swaggatherer/Template.cs
@@ -0,0 +1,72 @@
+// 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 entries)
+ {
+ var setupEndpointsLines = new List();
+ for (var i = 0; i < entries.Count; i++)
+ {
+ setupEndpointsLines.Add($" _endpoints[{i}] = CreateEndpoint(\"{entries[i].Template.TemplateText}\");");
+ }
+
+ var setupRequestsLines = new List();
+ 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();
+ for (var i = 0; i < entries.Count; i++)
+ {
+ setupMatcherLines.Add($" builder.AddEntry(\"{entries[i].Template.TemplateText}\", _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);
+ }
+ }
+}
diff --git a/build/dependencies.props b/build/dependencies.props
index 10f6d31b..65ddba59 100644
--- a/build/dependencies.props
+++ b/build/dependencies.props
@@ -16,6 +16,7 @@
2.2.0-preview1-34373
2.2.0-preview1-34373
2.2.0-preview1-34373
+ 2.2.0-preview1-34373
2.2.0-preview1-34373
2.2.0-preview1-34373
2.2.0-preview1-34373
@@ -36,6 +37,7 @@
15.6.1
4.7.49
2.0.3
+ 11.0.2
0.8.0
2.3.1
2.4.0-beta.1.build3945