Skip to content

Commit d37a877

Browse files
Add benchmarks (#22)
- Add benchmarks for rendering an OpenAPI document using ASP.NET Core OpenAPI, NSwag and Swashbuckle.AspNetCore. - Fix `AddSchemaDescriptionsTransformer` not being a singleton, meaning the cache was being bypassed.
1 parent 76bd313 commit d37a877

File tree

12 files changed

+250
-47
lines changed

12 files changed

+250
-47
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ _reports
99
_UpgradeReport_Files/
1010
artifacts/
1111
Backup*/
12+
BenchmarkDotNet*
1213
bin
1314
Bin
1415
coverage

TodoApp.sln

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{
6363
.github\workflows\lint.yml = .github\workflows\lint.yml
6464
EndProjectSection
6565
EndProject
66+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "perf", "perf", "{87734002-38FC-4F18-BB83-E1992C7B98D4}"
67+
EndProject
68+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoApp.Benchmarks", "perf\TodoApp.Benchmarks\TodoApp.Benchmarks.csproj", "{422C806D-1B71-4DDC-B62E-B53F18E72449}"
69+
EndProject
6670
Global
6771
GlobalSection(SolutionConfigurationPlatforms) = preSolution
6872
Debug|Any CPU = Debug|Any CPU
@@ -77,6 +81,10 @@ Global
7781
{4C240082-6C0D-4033-AF81-3CBF2B2777AB}.Debug|Any CPU.Build.0 = Debug|Any CPU
7882
{4C240082-6C0D-4033-AF81-3CBF2B2777AB}.Release|Any CPU.ActiveCfg = Release|Any CPU
7983
{4C240082-6C0D-4033-AF81-3CBF2B2777AB}.Release|Any CPU.Build.0 = Release|Any CPU
84+
{422C806D-1B71-4DDC-B62E-B53F18E72449}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
85+
{422C806D-1B71-4DDC-B62E-B53F18E72449}.Debug|Any CPU.Build.0 = Debug|Any CPU
86+
{422C806D-1B71-4DDC-B62E-B53F18E72449}.Release|Any CPU.ActiveCfg = Release|Any CPU
87+
{422C806D-1B71-4DDC-B62E-B53F18E72449}.Release|Any CPU.Build.0 = Release|Any CPU
8088
EndGlobalSection
8189
GlobalSection(SolutionProperties) = preSolution
8290
HideSolutionNode = FALSE
@@ -88,6 +96,7 @@ Global
8896
{62E14D54-FE91-4A24-A7D2-82661587A2DC} = {813B2DEF-0737-4242-8AB4-CF0725752CD0}
8997
{25891391-7F49-4621-8A74-EEAB11D4A778} = {62E14D54-FE91-4A24-A7D2-82661587A2DC}
9098
{86E6F5AB-6537-4A16-B5FD-2B6070F99EE0} = {62E14D54-FE91-4A24-A7D2-82661587A2DC}
99+
{422C806D-1B71-4DDC-B62E-B53F18E72449} = {87734002-38FC-4F18-BB83-E1992C7B98D4}
91100
EndGlobalSection
92101
GlobalSection(ExtensibilityGlobals) = postSolution
93102
SolutionGuid = {242240D8-68EA-4CBF-8E67-D955866F20ED}

global.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"sdk": {
3-
"version": "9.0.100-preview.7.24380.2",
3+
"version": "9.0.100-preview.7.24406.8",
44
"allowPrerelease": false,
55
"rollForward": "latestMajor"
66
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// Copyright (c) Martin Costello, 2024. All rights reserved.
2+
// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3+
4+
using BenchmarkDotNet.Attributes;
5+
using BenchmarkDotNet.Diagnosers;
6+
using Microsoft.AspNetCore.Builder;
7+
using Microsoft.AspNetCore.Hosting;
8+
using Microsoft.AspNetCore.Hosting.Server;
9+
using Microsoft.AspNetCore.Hosting.Server.Features;
10+
using Microsoft.Extensions.DependencyInjection;
11+
using Microsoft.Extensions.Logging;
12+
13+
namespace TodoApp;
14+
15+
[EventPipeProfiler(EventPipeProfile.CpuSampling)]
16+
[MemoryDiagnoser]
17+
[ShortRunJob]
18+
public class OpenApiBenchmarks : IAsyncDisposable
19+
{
20+
private WebApplication? _app;
21+
private HttpClient? _client;
22+
private bool _disposed;
23+
24+
public OpenApiBenchmarks()
25+
{
26+
var builder = WebApplication.CreateBuilder([$"--contentRoot={GetContentRoot()}"]);
27+
28+
builder.Logging.ClearProviders();
29+
builder.WebHost.UseUrls("https://127.0.0.1:0");
30+
31+
builder.AddTodoApp();
32+
33+
_app = builder.Build();
34+
_app.UseTodoApp();
35+
}
36+
37+
[GlobalSetup]
38+
public async Task StartServer()
39+
{
40+
if (_app is { } app)
41+
{
42+
await app.StartAsync();
43+
44+
var server = app.Services.GetRequiredService<IServer>();
45+
var addresses = server.Features.Get<IServerAddressesFeature>();
46+
47+
var baseAddress = addresses!.Addresses
48+
.Select((p) => new Uri(p))
49+
.Last();
50+
51+
var handler = new HttpClientHandler
52+
{
53+
ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
54+
};
55+
56+
#pragma warning disable CA5400
57+
_client = new(handler, disposeHandler: true) { BaseAddress = baseAddress };
58+
#pragma warning restore CA5400
59+
}
60+
}
61+
62+
[GlobalCleanup]
63+
public async Task StopServer()
64+
{
65+
if (_app is { } app)
66+
{
67+
await app.StopAsync();
68+
_app = null;
69+
}
70+
}
71+
72+
[Benchmark]
73+
public async Task<string> AspNetCore() => await _client!.GetStringAsync("/openapi/v1.json");
74+
75+
[Benchmark]
76+
public async Task<string> NSwag() => await _client!.GetStringAsync("/nswag/v1.json");
77+
78+
[Benchmark]
79+
public async Task<string> Swashbuckle() => await _client!.GetStringAsync("/swagger/v1/swagger.json");
80+
81+
public async ValueTask DisposeAsync()
82+
{
83+
GC.SuppressFinalize(this);
84+
85+
if (!_disposed)
86+
{
87+
_client?.Dispose();
88+
89+
if (_app is not null)
90+
{
91+
await _app.DisposeAsync();
92+
}
93+
}
94+
95+
_disposed = true;
96+
}
97+
98+
private static string GetContentRoot()
99+
{
100+
string contentRoot = string.Empty;
101+
var directoryInfo = new DirectoryInfo(Path.GetDirectoryName(typeof(OpenApiBenchmarks).Assembly.Location)!);
102+
103+
do
104+
{
105+
string? solutionPath = Directory.EnumerateFiles(directoryInfo.FullName, "TodoApp.sln").FirstOrDefault();
106+
107+
if (solutionPath is not null)
108+
{
109+
contentRoot = Path.GetFullPath(Path.Combine(directoryInfo.FullName, "src", "TodoApp"));
110+
break;
111+
}
112+
113+
directoryInfo = directoryInfo.Parent;
114+
}
115+
while (directoryInfo is not null);
116+
117+
return contentRoot;
118+
}
119+
}

perf/TodoApp.Benchmarks/Program.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright (c) Martin Costello, 2024. All rights reserved.
2+
// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3+
4+
using BenchmarkDotNet.Running;
5+
using TodoApp;
6+
7+
if (args.SequenceEqual(["--test"]))
8+
{
9+
await using var benchmarks = new OpenApiBenchmarks();
10+
await benchmarks.StartServer();
11+
12+
try
13+
{
14+
_ = await benchmarks.AspNetCore();
15+
_ = await benchmarks.NSwag();
16+
_ = await benchmarks.Swashbuckle();
17+
}
18+
finally
19+
{
20+
await benchmarks.StopServer();
21+
}
22+
}
23+
else
24+
{
25+
BenchmarkRunner.Run<OpenApiBenchmarks>(args: args);
26+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"profiles": {
3+
"TodoApp.Benchmarks": {
4+
"commandName": "Project",
5+
"commandLineArgs": ""
6+
}
7+
}
8+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<OutputType>Exe</OutputType>
4+
<RootNamespace>TodoApp</RootNamespace>
5+
<TargetFramework>net9.0</TargetFramework>
6+
</PropertyGroup>
7+
<ItemGroup>
8+
<ProjectReference Include="..\..\src\TodoApp\TodoApp.csproj" />
9+
</ItemGroup>
10+
<ItemGroup>
11+
<FrameworkReference Include="Microsoft.AspNetCore.App" />
12+
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
13+
</ItemGroup>
14+
</Project>

src/TodoApp/OpenApi/AspNetCore/AspNetCoreOpenApiEndpoints.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Martin Costello, 2024. All rights reserved.
22
// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
33

4+
using Microsoft.AspNetCore.OpenApi;
45
using Microsoft.OpenApi.Models;
56

67
namespace TodoApp.OpenApi.AspNetCore;
@@ -9,6 +10,7 @@ public static class AspNetCoreOpenApiEndpoints
910
{
1011
public static IServiceCollection AddAspNetCoreOpenApi(this IServiceCollection services)
1112
{
13+
services.AddSingleton<IOpenApiSchemaTransformer, AddSchemaDescriptionsTransformer>();
1214
services.AddOpenApi(options =>
1315
{
1416
// Add a document transformer to customise the generated OpenAPI document

src/TodoApp/Program.cs

Lines changed: 4 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,19 @@
11
// Copyright (c) Martin Costello, 2024. All rights reserved.
22
// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
33

4-
using Microsoft.AspNetCore.HttpOverrides;
54
using TodoApp;
65

76
// Create the default web application builder
87
var builder = WebApplication.CreateBuilder(args);
98

10-
// Configure the Todo repository and associated services
11-
builder.Services.AddTodoApi();
12-
13-
// Add Razor Pages to render the UI
14-
builder.Services.AddRazorPages();
15-
16-
// Configure OpenAPI documentation for the Todo API
17-
builder.Services.AddOpenApiServices();
18-
19-
if (string.Equals(builder.Configuration["CODESPACES"], "true", StringComparison.OrdinalIgnoreCase))
20-
{
21-
// When running in GitHub Codespaces, X-Forwarded-Host also needs to be set
22-
builder.Services.Configure<ForwardedHeadersOptions>(
23-
options => options.ForwardedHeaders |= ForwardedHeaders.XForwardedHost);
24-
}
9+
// Add TodoApp into the web application builder
10+
builder.AddTodoApp();
2511

2612
// Create the app
2713
var app = builder.Build();
2814

29-
// Configure error handling
30-
if (!app.Environment.IsDevelopment())
31-
{
32-
app.UseExceptionHandler("/error");
33-
}
34-
35-
app.UseStatusCodePagesWithReExecute("/error", "?id={0}");
36-
37-
// Require use of HTTPS in production
38-
if (!app.Environment.IsDevelopment())
39-
{
40-
app.UseHsts();
41-
app.UseHttpsRedirection();
42-
}
43-
44-
// Add static files for JavaScript, CSS and OpenAPI
45-
app.UseStaticFiles();
46-
47-
// Add endpoints for OpenAPI
48-
app.UseOpenApiEndpoints();
49-
50-
// Add the HTTP endpoints
51-
app.MapTodoApiRoutes();
52-
53-
// Add Razor Pages for the UI
54-
app.MapRazorPages();
15+
// Use TodoApp middleware and endpoints with the web application
16+
app.UseTodoApp();
5517

5618
// Run the application
5719
app.Run();

src/TodoApp/TodoApp.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@
1414
<TypeScriptToolsVersion>latest</TypeScriptToolsVersion>
1515
</PropertyGroup>
1616
<ItemGroup>
17-
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0-preview.7.24379.2" />
18-
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0-preview.7.24377.1" />
19-
<PackageReference Include="Microsoft.Extensions.ApiDescription.Server" Version="9.0.0-preview.7.24379.2" PrivateAssets="all" />
17+
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0-preview.7.24406.2" />
18+
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0-preview.7.24405.3" />
19+
<PackageReference Include="Microsoft.Extensions.ApiDescription.Server" Version="9.0.0-preview.7.24406.2" PrivateAssets="all" />
2020
<PackageReference Include="Microsoft.OpenApi" Version="1.6.17" />
2121
<PackageReference Include="Microsoft.TypeScript.MSBuild" Version="5.5.3" PrivateAssets="all" />
2222
<PackageReference Include="NSwag.AspNetCore" Version="14.1.0" />

0 commit comments

Comments
 (0)