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