diff --git a/scenarios/goldilocks.benchmarks.yml b/scenarios/goldilocks.benchmarks.yml
index 55f9a16c5..2ac1cf3fc 100644
--- a/scenarios/goldilocks.benchmarks.yml
+++ b/scenarios/goldilocks.benchmarks.yml
@@ -111,7 +111,7 @@ scenarios:
- "/p:PublishAot=true"
- "/p:StripSymbols=true"
environmentVariables:
- CONNECTIONSTRINGS__TODODB: Server={{databaseServer}};Database=hello_world;User Id=benchmarkdbuser;Password=benchmarkdbpass;Maximum Pool Size=256;NoResetOnClose=true;Enlist=false;Max Auto Prepare=4
+ APPSETTINGS__CONNECTIONSTRING: Server={{databaseServer}};Database=hello_world;User Id=benchmarkdbuser;Password=benchmarkdbpass;Maximum Pool Size=256;NoResetOnClose=true;Enlist=false;Max Auto Prepare=4
load:
job: wrk
variables:
@@ -131,7 +131,7 @@ scenarios:
- "/p:TrimMode=full"
- "/p:EnableRequestDelegateGenerator=true"
environmentVariables:
- CONNECTIONSTRINGS__TODODB: Server={{databaseServer}};Database=hello_world;User Id=benchmarkdbuser;Password=benchmarkdbpass;Maximum Pool Size=256;NoResetOnClose=true;Enlist=false;Max Auto Prepare=4
+ APPSETTINGS__CONNECTIONSTRING: Server={{databaseServer}};Database=hello_world;User Id=benchmarkdbuser;Password=benchmarkdbpass;Maximum Pool Size=256;NoResetOnClose=true;Enlist=false;Max Auto Prepare=4
load:
job: wrk
variables:
@@ -147,7 +147,7 @@ scenarios:
- "/p:PublishAot=false"
- "/p:EnableRequestDelegateGenerator=false"
environmentVariables:
- CONNECTIONSTRINGS__TODODB: Server={{databaseServer}};Database=hello_world;User Id=benchmarkdbuser;Password=benchmarkdbpass;Maximum Pool Size=256;NoResetOnClose=true;Enlist=false;Max Auto Prepare=4
+ APPSETTINGS__CONNECTIONSTRING: Server={{databaseServer}};Database=hello_world;User Id=benchmarkdbuser;Password=benchmarkdbpass;Maximum Pool Size=256;NoResetOnClose=true;Enlist=false;Max Auto Prepare=4
load:
job: wrk
variables:
diff --git a/src/BenchmarksApps.sln b/src/BenchmarksApps.sln
index 5800ec3c7..386c31c2f 100644
--- a/src/BenchmarksApps.sln
+++ b/src/BenchmarksApps.sln
@@ -55,6 +55,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorUnited", "BenchmarksA
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodosApi", "BenchmarksApps\TodosApi\TodosApi.csproj", "{8E1A1F61-43E4-4629-A25B-7E5FA82697D0}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetCore.OpenApi", "BenchmarksApps\AspNetCore.OpenApi\AspNetCore.OpenApi.csproj", "{28F432A6-1328-4996-91DD-BB1C87F45BF2}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug_Database|Any CPU = Debug_Database|Any CPU
@@ -207,6 +209,14 @@ Global
{8E1A1F61-43E4-4629-A25B-7E5FA82697D0}.Release_Database|Any CPU.Build.0 = Release_Database|Any CPU
{8E1A1F61-43E4-4629-A25B-7E5FA82697D0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8E1A1F61-43E4-4629-A25B-7E5FA82697D0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {28F432A6-1328-4996-91DD-BB1C87F45BF2}.Debug_Database|Any CPU.ActiveCfg = Debug_Database|Any CPU
+ {28F432A6-1328-4996-91DD-BB1C87F45BF2}.Debug_Database|Any CPU.Build.0 = Debug_Database|Any CPU
+ {28F432A6-1328-4996-91DD-BB1C87F45BF2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {28F432A6-1328-4996-91DD-BB1C87F45BF2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {28F432A6-1328-4996-91DD-BB1C87F45BF2}.Release_Database|Any CPU.ActiveCfg = Release_Database|Any CPU
+ {28F432A6-1328-4996-91DD-BB1C87F45BF2}.Release_Database|Any CPU.Build.0 = Release_Database|Any CPU
+ {28F432A6-1328-4996-91DD-BB1C87F45BF2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {28F432A6-1328-4996-91DD-BB1C87F45BF2}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/src/BenchmarksApps/AspNetCore.OpenApi/AspNetCore.OpenApi.csproj b/src/BenchmarksApps/AspNetCore.OpenApi/AspNetCore.OpenApi.csproj
new file mode 100644
index 000000000..7f3d0cb1f
--- /dev/null
+++ b/src/BenchmarksApps/AspNetCore.OpenApi/AspNetCore.OpenApi.csproj
@@ -0,0 +1,17 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/BenchmarksApps/AspNetCore.OpenApi/ILLink.Substitutions.xml b/src/BenchmarksApps/AspNetCore.OpenApi/ILLink.Substitutions.xml
new file mode 100644
index 000000000..50c2c47a3
--- /dev/null
+++ b/src/BenchmarksApps/AspNetCore.OpenApi/ILLink.Substitutions.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/BenchmarksApps/AspNetCore.OpenApi/OpenApiFeature.cs b/src/BenchmarksApps/AspNetCore.OpenApi/OpenApiFeature.cs
new file mode 100644
index 000000000..3f5fd4b7b
--- /dev/null
+++ b/src/BenchmarksApps/AspNetCore.OpenApi/OpenApiFeature.cs
@@ -0,0 +1,17 @@
+namespace Microsoft.AspNetCore.OpenApi;
+
+public static class OpenApiFeature
+{
+ ///
+ /// Indicates whether APIs related to OpenAPI/Swagger functionality are enabled.
+ ///
+ ///
+ /// The value of the property is backed by the "Microsoft.AspNetCore.OpenApi.OpenApiFeature.IsEnabled"
+ /// setting and defaults to if unset.
+ ///
+ public static bool IsEnabled { get; } =
+ AppContext.TryGetSwitch(
+ switchName: "Microsoft.AspNetCore.OpenApi.OpenApiFeature.IsEnabled",
+ isEnabled: out var value)
+ ? value : true;
+}
diff --git a/src/BenchmarksApps/AspNetCore.OpenApi/build/AspNetCore.OpenApi.targets b/src/BenchmarksApps/AspNetCore.OpenApi/build/AspNetCore.OpenApi.targets
new file mode 100644
index 000000000..1b1c52ce7
--- /dev/null
+++ b/src/BenchmarksApps/AspNetCore.OpenApi/build/AspNetCore.OpenApi.targets
@@ -0,0 +1,14 @@
+
+
+
+ false
+
+
+
+
+
+
+
diff --git a/src/BenchmarksApps/BasicMinimalApi/Properties/launchSettings.json b/src/BenchmarksApps/BasicMinimalApi/Properties/launchSettings.json
index b5267280f..5ecae91f9 100644
--- a/src/BenchmarksApps/BasicMinimalApi/Properties/launchSettings.json
+++ b/src/BenchmarksApps/BasicMinimalApi/Properties/launchSettings.json
@@ -1,4 +1,5 @@
{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"BasicMinimalApi": {
"commandName": "Project",
diff --git a/src/BenchmarksApps/DistributedCache/Properties/launchSettings.json b/src/BenchmarksApps/DistributedCache/Properties/launchSettings.json
new file mode 100644
index 000000000..9344b26f8
--- /dev/null
+++ b/src/BenchmarksApps/DistributedCache/Properties/launchSettings.json
@@ -0,0 +1,13 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "DistributedCache": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "applicationUrl": "https://localhost:53684;http://localhost:53687"
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BenchmarksApps/Grpc/BasicGrpc/Properties/launchSettings.json b/src/BenchmarksApps/Grpc/BasicGrpc/Properties/launchSettings.json
index 754a65a9b..bafcc5f4d 100644
--- a/src/BenchmarksApps/Grpc/BasicGrpc/Properties/launchSettings.json
+++ b/src/BenchmarksApps/Grpc/BasicGrpc/Properties/launchSettings.json
@@ -1,4 +1,5 @@
{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"BasicGrpc": {
"commandName": "Project",
diff --git a/src/BenchmarksApps/Grpc/GrpcHttpApiServer/Server/Properties/launchSettings.json b/src/BenchmarksApps/Grpc/GrpcHttpApiServer/Server/Properties/launchSettings.json
new file mode 100644
index 000000000..97e58a11b
--- /dev/null
+++ b/src/BenchmarksApps/Grpc/GrpcHttpApiServer/Server/Properties/launchSettings.json
@@ -0,0 +1,13 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "Server": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "applicationUrl": "https://localhost:53695;http://localhost:53696"
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BenchmarksApps/HelloWorldMiddleware/Properties/launchSettings.json b/src/BenchmarksApps/HelloWorldMiddleware/Properties/launchSettings.json
new file mode 100644
index 000000000..96a3a5667
--- /dev/null
+++ b/src/BenchmarksApps/HelloWorldMiddleware/Properties/launchSettings.json
@@ -0,0 +1,13 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "HelloWorldMiddleware": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "applicationUrl": "https://localhost:53681;http://localhost:53682"
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BenchmarksApps/HelloWorldMvc/Properties/launchSettings.json b/src/BenchmarksApps/HelloWorldMvc/Properties/launchSettings.json
new file mode 100644
index 000000000..1c1aabc32
--- /dev/null
+++ b/src/BenchmarksApps/HelloWorldMvc/Properties/launchSettings.json
@@ -0,0 +1,13 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "HelloWorldMvc": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "applicationUrl": "https://localhost:53675;http://localhost:53678"
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BenchmarksApps/MapAction/Properties/launchSettings.json b/src/BenchmarksApps/MapAction/Properties/launchSettings.json
new file mode 100644
index 000000000..281196e09
--- /dev/null
+++ b/src/BenchmarksApps/MapAction/Properties/launchSettings.json
@@ -0,0 +1,13 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "MapAction": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "applicationUrl": "https://localhost:53676;http://localhost:53679"
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BenchmarksApps/Mvc/Properties/launchSettings.json b/src/BenchmarksApps/Mvc/Properties/launchSettings.json
new file mode 100644
index 000000000..b57c05603
--- /dev/null
+++ b/src/BenchmarksApps/Mvc/Properties/launchSettings.json
@@ -0,0 +1,13 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "Mvc": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "applicationUrl": "https://localhost:53683;http://localhost:53685"
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BenchmarksApps/SignalR/Properties/launchSettings.json b/src/BenchmarksApps/SignalR/Properties/launchSettings.json
new file mode 100644
index 000000000..e7607ff84
--- /dev/null
+++ b/src/BenchmarksApps/SignalR/Properties/launchSettings.json
@@ -0,0 +1,13 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "BenchmarkServer": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "applicationUrl": "https://localhost:53697;http://localhost:53698"
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BenchmarksApps/StaticFiles/Properties/launchSettings.json b/src/BenchmarksApps/StaticFiles/Properties/launchSettings.json
new file mode 100644
index 000000000..305b18d63
--- /dev/null
+++ b/src/BenchmarksApps/StaticFiles/Properties/launchSettings.json
@@ -0,0 +1,13 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "StaticFiles": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "applicationUrl": "https://localhost:53686;http://localhost:53688"
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BenchmarksApps/TcpEcho/Properties/launchSettings.json b/src/BenchmarksApps/TcpEcho/Properties/launchSettings.json
new file mode 100644
index 000000000..a96142ba3
--- /dev/null
+++ b/src/BenchmarksApps/TcpEcho/Properties/launchSettings.json
@@ -0,0 +1,13 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "TcpEcho": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "applicationUrl": "https://localhost:53701;http://localhost:53702"
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BenchmarksApps/TechEmpower/BlazorUnited/Properties/launchSettings.json b/src/BenchmarksApps/TechEmpower/BlazorUnited/Properties/launchSettings.json
new file mode 100644
index 000000000..21158c08c
--- /dev/null
+++ b/src/BenchmarksApps/TechEmpower/BlazorUnited/Properties/launchSettings.json
@@ -0,0 +1,13 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "BlazorUnited": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "applicationUrl": "https://localhost:53703;http://localhost:53704"
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BenchmarksApps/TechEmpower/Minimal/Properties/launchSettings.json b/src/BenchmarksApps/TechEmpower/Minimal/Properties/launchSettings.json
new file mode 100644
index 000000000..060102075
--- /dev/null
+++ b/src/BenchmarksApps/TechEmpower/Minimal/Properties/launchSettings.json
@@ -0,0 +1,13 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "Minimal": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "applicationUrl": "https://localhost:53691;http://localhost:53692"
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BenchmarksApps/TechEmpower/Mvc/Properties/launchSettings.json b/src/BenchmarksApps/TechEmpower/Mvc/Properties/launchSettings.json
new file mode 100644
index 000000000..629716fce
--- /dev/null
+++ b/src/BenchmarksApps/TechEmpower/Mvc/Properties/launchSettings.json
@@ -0,0 +1,13 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "Mvc": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "applicationUrl": "https://localhost:53693;http://localhost:53694"
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BenchmarksApps/TechEmpower/PlatformBenchmarks/Properties/launchSettings.json b/src/BenchmarksApps/TechEmpower/PlatformBenchmarks/Properties/launchSettings.json
new file mode 100644
index 000000000..89d1445f7
--- /dev/null
+++ b/src/BenchmarksApps/TechEmpower/PlatformBenchmarks/Properties/launchSettings.json
@@ -0,0 +1,13 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "PlatformBenchmarks": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "applicationUrl": "https://localhost:53705;http://localhost:53706"
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BenchmarksApps/TodosApi/AppSettings.cs b/src/BenchmarksApps/TodosApi/AppSettings.cs
new file mode 100644
index 000000000..2bf7c4679
--- /dev/null
+++ b/src/BenchmarksApps/TodosApi/AppSettings.cs
@@ -0,0 +1,61 @@
+using Microsoft.Extensions.Options;
+
+namespace TodosApi;
+
+internal class AppSettings
+{
+ public required string ConnectionString { get; set; }
+
+ public string? JwtSigningKey { get; set; }
+
+ public bool SuppressDbInitialization { get; set; }
+}
+
+// Change to using ValidateDataAnnotations once https://github.com/dotnet/runtime/issues/77412 is complete
+internal class AppSettingsValidator : IValidateOptions
+{
+ public ValidateOptionsResult Validate(string? name, AppSettings options)
+ {
+ if (string.IsNullOrEmpty(options.ConnectionString))
+ {
+ return ValidateOptionsResult.Fail("""
+ Connection string not found.
+ If running locally, set the connection string in user secrets for key 'AppSettings:ConnectionString'.
+ If running after deployment, set the connection string via the environment variable 'APPSETTINGS__CONNECTIONSTRING'.
+ """);
+ }
+
+ return ValidateOptionsResult.Success;
+ }
+}
+
+internal static class AppSettingsExtensions
+{
+ public static IServiceCollection ConfigureAppSettings(this IServiceCollection services, IConfigurationRoot configurationRoot, IHostEnvironment hostEnvironment)
+ {
+ // Can't use the configuration binding source generator due to bug where it emits non-compiling code right now
+ // https://github.com/dotnet/runtime/issues/83600
+ var optionsBuilder = services.Configure(configurationRoot.GetSection(nameof(AppSettings)))
+ .AddOptions();
+
+ if (!hostEnvironment.IsBuild())
+ {
+ services.AddSingleton, AppSettingsValidator>();
+ optionsBuilder.ValidateOnStart();
+ }
+
+ // Change to using BindConfiguration once https://github.com/dotnet/runtime/issues/83600 is complete
+ //services.AddSingleton, AppSettingsValidator>()
+ // .AddOptions()
+ // .BindConfiguration(nameof(AppSettings))
+ // .ValidateOnStart();
+
+ // Change to using ValidateDataAnnotations once https://github.com/dotnet/runtime/issues/77412 is complete
+ //services.AddOptions()
+ // .BindConfiguration(nameof(AppSettings))
+ // .ValidateDataAnnotations()
+ // .ValidateOnStart();
+
+ return services;
+ }
+}
diff --git a/src/BenchmarksApps/TodosApi/DataExtensions.cs b/src/BenchmarksApps/TodosApi/DataExtensions.cs
index f3595d0d1..723c4e0f8 100644
--- a/src/BenchmarksApps/TodosApi/DataExtensions.cs
+++ b/src/BenchmarksApps/TodosApi/DataExtensions.cs
@@ -1,8 +1,12 @@
using System.Data;
using System.Runtime.CompilerServices;
+using Microsoft.Extensions.Options;
+using TodosApi;
namespace Npgsql;
+// Replace this with https://www.nuget.org/packages/Nanorm once it's ready
+
internal static class DataExtensions
{
public static async Task ExecuteAsync(this NpgsqlDataSource dataSource, string commandText, CancellationToken cancellationToken = default)
diff --git a/src/BenchmarksApps/TodosApi/Database.cs b/src/BenchmarksApps/TodosApi/Database.cs
deleted file mode 100644
index 48dc1d0c1..000000000
--- a/src/BenchmarksApps/TodosApi/Database.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-using Npgsql;
-
-namespace TodosApi;
-
-internal static class Database
-{
- public static async Task Initialize(IServiceProvider services, ILogger logger, CancellationToken cancellationToken = default)
- {
- var db = services.GetRequiredService();
-
- if (Environment.GetEnvironmentVariable("SUPPRESS_DB_INIT") != "true")
- {
- // NOTE: Npgsql removes the password from the connection string
- logger.LogInformation("Ensuring database exists and is up to date at connection string '{connectionString}'", db.ConnectionString);
-
- var sql = $"""
- CREATE TABLE IF NOT EXISTS public.todos
- (
- {nameof(Todo.Id)} SERIAL PRIMARY KEY,
- {nameof(Todo.Title)} text NOT NULL,
- {nameof(Todo.DueBy)} date NULL,
- {nameof(Todo.IsComplete)} boolean NOT NULL DEFAULT false
- );
- DELETE FROM public.todos;
- INSERT INTO
- public.todos ({nameof(Todo.Title)}, {nameof(Todo.DueBy)}, {nameof(Todo.IsComplete)})
- VALUES
- ('Wash the dishes.', CURRENT_DATE, true),
- ('Dry the dishes.', CURRENT_DATE, true),
- ('Turn the dishes over.', CURRENT_DATE, false),
- ('Walk the kangaroo.', CURRENT_DATE + INTERVAL '1 day', false),
- ('Call Grandma.', CURRENT_DATE + INTERVAL '1 day', false);
- """;
- await db.ExecuteAsync(sql, cancellationToken);
- }
- else
- {
- logger.LogInformation("Database initialization disabled for connection string '{connectionString}'", db.ConnectionString);
- }
- }
-}
diff --git a/src/BenchmarksApps/TodosApi/DatabaseConfiguration.cs b/src/BenchmarksApps/TodosApi/DatabaseConfiguration.cs
new file mode 100644
index 000000000..52e6ef410
--- /dev/null
+++ b/src/BenchmarksApps/TodosApi/DatabaseConfiguration.cs
@@ -0,0 +1,24 @@
+using Microsoft.Extensions.Options;
+using Npgsql;
+using TodosApi;
+
+namespace Microsoft.Extensions.Hosting;
+
+internal static class DatabaseConfiguration
+{
+ public static IServiceCollection AddDatabase(this IServiceCollection services)
+ {
+ services.AddSingleton(static sp =>
+ {
+ var appSettings = sp.GetRequiredService>().Value;
+ var hostEnvironment = sp.GetRequiredService();
+ var db = hostEnvironment.IsBuild()
+ ? default!
+ : new NpgsqlSlimDataSourceBuilder(appSettings.ConnectionString).Build();
+
+ return db;
+ });
+ services.AddHostedService();
+ return services;
+ }
+}
diff --git a/src/BenchmarksApps/TodosApi/DatabaseHealthCheck.cs b/src/BenchmarksApps/TodosApi/DatabaseHealthCheck.cs
index f80bc78c4..870d7e331 100644
--- a/src/BenchmarksApps/TodosApi/DatabaseHealthCheck.cs
+++ b/src/BenchmarksApps/TodosApi/DatabaseHealthCheck.cs
@@ -3,7 +3,7 @@
namespace TodosApi;
-public class DatabaseHealthCheck : IHealthCheck
+internal class DatabaseHealthCheck : IHealthCheck
{
private readonly NpgsqlDataSource _dataSource;
diff --git a/src/BenchmarksApps/TodosApi/DatabaseInitializer.cs b/src/BenchmarksApps/TodosApi/DatabaseInitializer.cs
new file mode 100644
index 000000000..3cafb55c2
--- /dev/null
+++ b/src/BenchmarksApps/TodosApi/DatabaseInitializer.cs
@@ -0,0 +1,64 @@
+using Microsoft.Extensions.Options;
+using Npgsql;
+
+namespace TodosApi;
+
+internal class DatabaseInitializer : IHostedService
+{
+ private readonly NpgsqlDataSource _db;
+ private readonly ILogger _logger;
+ private readonly bool _initDatabase;
+
+ public DatabaseInitializer(NpgsqlDataSource db, IOptions appSettings, IHostEnvironment hostEnvironment, ILogger logger)
+ {
+ _db = db;
+ _logger = logger;
+ _initDatabase = !(hostEnvironment.IsBuild() || appSettings.Value.SuppressDbInitialization);
+ }
+
+ public Task StartAsync(CancellationToken cancellationToken)
+ {
+ if (_initDatabase)
+ {
+ return Initialize(cancellationToken);
+ }
+
+ if (_logger.IsEnabled(LogLevel.Information))
+ {
+ _logger.LogInformation("Database initialization is disabled");
+ }
+
+ return Task.CompletedTask;
+ }
+
+ public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+
+ private async Task Initialize(CancellationToken cancellationToken = default)
+ {
+ // NOTE: Npgsql removes the password from the connection string
+ if (_logger.IsEnabled(LogLevel.Information))
+ {
+ _logger.LogInformation("Ensuring database exists and is up to date");
+ }
+
+ var sql = $"""
+ CREATE TABLE IF NOT EXISTS public.todos
+ (
+ {nameof(Todo.Id)} SERIAL PRIMARY KEY,
+ {nameof(Todo.Title)} text NOT NULL,
+ {nameof(Todo.DueBy)} date NULL,
+ {nameof(Todo.IsComplete)} boolean NOT NULL DEFAULT false
+ );
+ DELETE FROM public.todos;
+ INSERT INTO
+ public.todos ({nameof(Todo.Title)}, {nameof(Todo.DueBy)}, {nameof(Todo.IsComplete)})
+ VALUES
+ ('Wash the dishes.', CURRENT_DATE, true),
+ ('Dry the dishes.', CURRENT_DATE, true),
+ ('Turn the dishes over.', CURRENT_DATE, false),
+ ('Walk the kangaroo.', CURRENT_DATE + INTERVAL '1 day', false),
+ ('Call Grandma.', CURRENT_DATE + INTERVAL '1 day', false);
+ """;
+ await _db.ExecuteAsync(sql, cancellationToken);
+ }
+}
diff --git a/src/BenchmarksApps/TodosApi/HostEnvironmentExtensions.cs b/src/BenchmarksApps/TodosApi/HostEnvironmentExtensions.cs
new file mode 100644
index 000000000..88c42c881
--- /dev/null
+++ b/src/BenchmarksApps/TodosApi/HostEnvironmentExtensions.cs
@@ -0,0 +1,6 @@
+namespace Microsoft.Extensions.Hosting;
+
+internal static class HostEnvironmentExtensions
+{
+ public static bool IsBuild(this IHostEnvironment hostEnvironment) => hostEnvironment.IsEnvironment("Build");
+}
diff --git a/src/BenchmarksApps/TodosApi/JwtConfiguration.cs b/src/BenchmarksApps/TodosApi/JwtConfiguration.cs
index 8b6df4cd7..b54f03672 100644
--- a/src/BenchmarksApps/TodosApi/JwtConfiguration.cs
+++ b/src/BenchmarksApps/TodosApi/JwtConfiguration.cs
@@ -1,32 +1,34 @@
using Microsoft.AspNetCore.Authentication.JwtBearer;
+using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
+using TodosApi;
namespace Microsoft.AspNetCore.Builder;
-internal static class JwtConfiguration
+internal class JwtConfiguration : IConfigureOptions
{
- ///
- /// Configures JWT Bearer to load the signing key from an environment variable when not running in Development.
- ///
- ///
- ///
- /// Thrown when the signing key is not found in non-Development environments.
- public static Action ConfigureJwtBearer(WebApplicationBuilder builder)
+ private readonly IHostEnvironment _hostEnvironment;
+ private readonly AppSettings _appSettings;
+
+ public JwtConfiguration(IHostEnvironment hostEnvironment, IOptions appSettings)
+ {
+ _hostEnvironment = hostEnvironment;
+ _appSettings = appSettings.Value;
+ }
+
+ public void Configure(JwtBearerOptions options)
{
- return options =>
+ if (!_hostEnvironment.IsDevelopment())
{
- if (!builder.Environment.IsDevelopment())
- {
- // When not running in development configure the JWT signing key from environment variable
- var jwtKeyMaterialValue = builder.Configuration["JWT_SIGNING_KEY"];
+ // When not running in development configure the JWT signing key from environment variable
+ var jwtKeyMaterialValue = _appSettings.JwtSigningKey;
- if (!string.IsNullOrEmpty(jwtKeyMaterialValue))
- {
- var jwtKeyMaterial = Convert.FromBase64String(jwtKeyMaterialValue);
- var jwtSigningKey = new SymmetricSecurityKey(jwtKeyMaterial);
- options.TokenValidationParameters.IssuerSigningKey = jwtSigningKey;
- }
+ if (!string.IsNullOrEmpty(jwtKeyMaterialValue))
+ {
+ var jwtKeyMaterial = Convert.FromBase64String(jwtKeyMaterialValue);
+ var jwtSigningKey = new SymmetricSecurityKey(jwtKeyMaterial);
+ options.TokenValidationParameters.IssuerSigningKey = jwtSigningKey;
}
- };
+ }
}
}
diff --git a/src/BenchmarksApps/TodosApi/OpenApiExtensions.cs b/src/BenchmarksApps/TodosApi/OpenApiExtensions.cs
new file mode 100644
index 000000000..40d475bee
--- /dev/null
+++ b/src/BenchmarksApps/TodosApi/OpenApiExtensions.cs
@@ -0,0 +1,20 @@
+using Microsoft.AspNetCore.OpenApi;
+using Microsoft.OpenApi.Models;
+
+namespace Microsoft.Extensions.Hosting;
+
+internal static class OpenApiExtensions
+{
+ public static IServiceCollection AddOpenApi(this IServiceCollection services)
+ {
+ if (OpenApiFeature.IsEnabled)
+ {
+ services.AddEndpointsApiExplorer();
+ services.AddSwaggerGen(c =>
+ {
+ c.SwaggerDoc("v1", new OpenApiInfo { Title = "Todos API", Version = "v1" });
+ });
+ }
+ return services;
+ }
+}
diff --git a/src/BenchmarksApps/TodosApi/Program.cs b/src/BenchmarksApps/TodosApi/Program.cs
index a2ba94f12..ac77414db 100644
--- a/src/BenchmarksApps/TodosApi/Program.cs
+++ b/src/BenchmarksApps/TodosApi/Program.cs
@@ -1,4 +1,3 @@
-using Npgsql;
using TodosApi;
var builder = WebApplication.CreateSlimBuilder(args);
@@ -7,20 +6,16 @@
builder.Logging.ClearProviders();
#endif
-// Configure authentication & authorization
-builder.Services.AddAuthentication()
- .AddJwtBearer(JwtConfiguration.ConfigureJwtBearer(builder));
+// Bind app settings from configuration & validate
+builder.Services.ConfigureAppSettings(builder.Configuration, builder.Environment);
+// Configure authentication & authorization
+builder.Services.AddAuthentication().AddJwtBearer();
+builder.Services.ConfigureOptions();
builder.Services.AddAuthorization();
// Configure data access
-var connectionString = builder.Configuration.GetConnectionString("TodoDb")
- ?? throw new InvalidOperationException("""
- Connection string not found.
- If running locally, set the connection string in user secrets for key 'ConnectionStrings:TodoDb'.
- If running after deployment, set the connection string via the environment variable 'CONNECTIONSTRINGS__TODODB'.
- """);
-builder.Services.AddSingleton(_ => new NpgsqlSlimDataSourceBuilder(connectionString).Build());
+builder.Services.AddDatabase();
// Configure JSON serialization
builder.Services.ConfigureHttpJsonOptions(options =>
@@ -36,16 +31,20 @@ Connection string not found.
// Problem details
builder.Services.AddProblemDetails();
-var app = builder.Build();
+// OpenAPI
+builder.Services.AddOpenApi();
-await Database.Initialize(app.Services, app.Logger);
+var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler();
}
+app.MapShortCircuit(StatusCodes.Status404NotFound, "/favicon.ico");
+
app.MapHealthChecks("/health");
+
// Enables testing request exception handling behavior
app.MapGet("/throw", void () => throw new InvalidOperationException("You hit the throw endpoint"));
diff --git a/src/BenchmarksApps/TodosApi/TodosApi.csproj b/src/BenchmarksApps/TodosApi/TodosApi.csproj
index 4c928434d..48a562e54 100644
--- a/src/BenchmarksApps/TodosApi/TodosApi.csproj
+++ b/src/BenchmarksApps/TodosApi/TodosApi.csproj
@@ -9,12 +9,31 @@
preview
b8ffb8d3-b768-460b-ac1f-ef267c954c85
true
- true
+ .\
+
+
+ false
+ true
$(DefineConstants);ENABLE_LOGGING
-
+
-
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
diff --git a/src/BenchmarksApps/TodosApi/TodosApi.http b/src/BenchmarksApps/TodosApi/TodosApi.http
index d9969348b..c9b5ccf1d 100644
--- a/src/BenchmarksApps/TodosApi/TodosApi.http
+++ b/src/BenchmarksApps/TodosApi/TodosApi.http
@@ -22,7 +22,7 @@ Accept: application/json
###
-Get {{TodosApi_HostAddress}}/api/todos/find
+Get {{TodosApi_HostAddress}}/api/todos/find?title=wash+the+dishes.
Accept: application/json
###
diff --git a/src/BenchmarksApps/TodosApi/TodosApi.json b/src/BenchmarksApps/TodosApi/TodosApi.json
new file mode 100644
index 000000000..75336a737
--- /dev/null
+++ b/src/BenchmarksApps/TodosApi/TodosApi.json
@@ -0,0 +1,352 @@
+{
+ "openapi": "3.0.1",
+ "info": {
+ "title": "Todos API",
+ "version": "v1"
+ },
+ "paths": {
+ "/throw": {
+ "get": {
+ "tags": [
+ "TodosApi"
+ ],
+ "responses": {
+ "200": {
+ "description": "Success"
+ }
+ }
+ }
+ },
+ "/api/todos": {
+ "get": {
+ "tags": [
+ "TodosApi"
+ ],
+ "operationId": "GetAllTodos",
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Todo"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "post": {
+ "tags": [
+ "TodosApi"
+ ],
+ "operationId": "CreateTodo",
+ "requestBody": {
+ "content": { },
+ "required": true
+ },
+ "responses": {
+ "400": {
+ "description": "Bad Request",
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/HttpValidationProblemDetails"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/todos/complete": {
+ "get": {
+ "tags": [
+ "TodosApi"
+ ],
+ "operationId": "GetCompleteTodos",
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Todo"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/todos/incomplete": {
+ "get": {
+ "tags": [
+ "TodosApi"
+ ],
+ "operationId": "GetIncompleteTodos",
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Todo"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/todos/{id}": {
+ "get": {
+ "tags": [
+ "TodosApi"
+ ],
+ "operationId": "GetTodoById",
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "style": "simple",
+ "schema": {
+ "type": "integer",
+ "format": "int32"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Success"
+ }
+ }
+ },
+ "put": {
+ "tags": [
+ "TodosApi"
+ ],
+ "operationId": "UpdateTodo",
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "style": "simple",
+ "schema": {
+ "type": "integer",
+ "format": "int32"
+ }
+ }
+ ],
+ "requestBody": {
+ "content": { },
+ "required": true
+ },
+ "responses": {
+ "400": {
+ "description": "Bad Request",
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/HttpValidationProblemDetails"
+ }
+ }
+ }
+ }
+ }
+ },
+ "delete": {
+ "tags": [
+ "TodosApi"
+ ],
+ "operationId": "DeleteTodo",
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "style": "simple",
+ "schema": {
+ "type": "integer",
+ "format": "int32"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Success"
+ }
+ }
+ }
+ },
+ "/api/todos/find": {
+ "get": {
+ "tags": [
+ "TodosApi"
+ ],
+ "operationId": "FindTodo",
+ "parameters": [
+ {
+ "name": "title",
+ "in": "query",
+ "required": true,
+ "style": "form",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "isComplete",
+ "in": "query",
+ "style": "form",
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Success"
+ }
+ }
+ }
+ },
+ "/api/todos/{id}/mark-complete": {
+ "put": {
+ "tags": [
+ "TodosApi"
+ ],
+ "operationId": "MarkComplete",
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "style": "simple",
+ "schema": {
+ "type": "integer",
+ "format": "int32"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Success"
+ }
+ }
+ }
+ },
+ "/api/todos/{id}/mark-incomplete": {
+ "put": {
+ "tags": [
+ "TodosApi"
+ ],
+ "operationId": "MarkIncomplete",
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "style": "simple",
+ "schema": {
+ "type": "integer",
+ "format": "int32"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Success"
+ }
+ }
+ }
+ },
+ "/api/todos/delete-all": {
+ "delete": {
+ "tags": [
+ "TodosApi"
+ ],
+ "operationId": "DeleteAll",
+ "responses": {
+ "200": {
+ "description": "Success"
+ }
+ }
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "HttpValidationProblemDetails": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "nullable": true
+ },
+ "title": {
+ "type": "string",
+ "nullable": true
+ },
+ "status": {
+ "type": "integer",
+ "format": "int32",
+ "nullable": true
+ },
+ "detail": {
+ "type": "string",
+ "nullable": true
+ },
+ "instance": {
+ "type": "string",
+ "nullable": true
+ },
+ "errors": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "nullable": true
+ }
+ },
+ "additionalProperties": { }
+ },
+ "Todo": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "format": "int32"
+ },
+ "title": {
+ "type": "string",
+ "nullable": true
+ },
+ "dueBy": {
+ "type": "string",
+ "format": "date",
+ "nullable": true
+ },
+ "isComplete": {
+ "type": "boolean"
+ }
+ },
+ "additionalProperties": false
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BenchmarksApps/TodosApi/appsettings.json b/src/BenchmarksApps/TodosApi/appsettings.json
index 6b6ae8970..14aee9c59 100644
--- a/src/BenchmarksApps/TodosApi/appsettings.json
+++ b/src/BenchmarksApps/TodosApi/appsettings.json
@@ -5,7 +5,6 @@
"Microsoft.AspNetCore": "Warning"
}
},
- "AllowedHosts": "*",
"Authentication": {
"Schemes": {
"Bearer": {
@@ -16,5 +15,6 @@
"ValidIssuer": "dotnet-user-jwts"
}
}
- }
+ },
+ "AllowedHosts": "*"
}
\ No newline at end of file
diff --git a/src/BenchmarksApps/Websocket/Properties/launchSettings.json b/src/BenchmarksApps/Websocket/Properties/launchSettings.json
new file mode 100644
index 000000000..7169c8001
--- /dev/null
+++ b/src/BenchmarksApps/Websocket/Properties/launchSettings.json
@@ -0,0 +1,13 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "BenchmarkServer": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "applicationUrl": "https://localhost:53699;http://localhost:53700"
+ }
+ }
+}
\ No newline at end of file