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