From d473ec91f1074a255eaba3d35137c62e43736d89 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Thu, 30 Mar 2023 10:54:58 -0700 Subject: [PATCH 01/26] Add goldilocks stage 2 TodosApp --- src/BenchmarksApps.sln | 10 + src/BenchmarksApps/TodosApi/DataExtensions.cs | 340 ++++++++++++++++++ src/BenchmarksApps/TodosApi/Database.cs | 45 +++ .../TodosApi/JwtConfiguration.cs | 94 +++++ src/BenchmarksApps/TodosApi/Program.cs | 47 +++ src/BenchmarksApps/TodosApi/Todo.cs | 24 ++ src/BenchmarksApps/TodosApi/TodoApi.cs | 99 +++++ src/BenchmarksApps/TodosApi/TodosApi.csproj | 17 + .../TodosApi/appsettings.Development.json | 18 + src/BenchmarksApps/TodosApi/appsettings.json | 19 + 10 files changed, 713 insertions(+) create mode 100644 src/BenchmarksApps/TodosApi/DataExtensions.cs create mode 100644 src/BenchmarksApps/TodosApi/Database.cs create mode 100644 src/BenchmarksApps/TodosApi/JwtConfiguration.cs create mode 100644 src/BenchmarksApps/TodosApi/Program.cs create mode 100644 src/BenchmarksApps/TodosApi/Todo.cs create mode 100644 src/BenchmarksApps/TodosApi/TodoApi.cs create mode 100644 src/BenchmarksApps/TodosApi/TodosApi.csproj create mode 100644 src/BenchmarksApps/TodosApi/appsettings.Development.json create mode 100644 src/BenchmarksApps/TodosApi/appsettings.json diff --git a/src/BenchmarksApps.sln b/src/BenchmarksApps.sln index 19fdde424..37c4bcbd4 100644 --- a/src/BenchmarksApps.sln +++ b/src/BenchmarksApps.sln @@ -52,6 +52,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TcpEcho", "BenchmarksApps\T EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorUnited", "BenchmarksApps\TechEmpower\BlazorUnited\BlazorUnited.csproj", "{FE3606FF-CBC9-421A-A0B5-836E312E7719}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TodosApi", "BenchmarksApps\TodosApi\TodosApi.csproj", "{8E1A1F61-43E4-4629-A25B-7E5FA82697D0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug_Database|Any CPU = Debug_Database|Any CPU @@ -196,6 +198,14 @@ Global {FE3606FF-CBC9-421A-A0B5-836E312E7719}.Release_Database|Any CPU.Build.0 = Release_Database|Any CPU {FE3606FF-CBC9-421A-A0B5-836E312E7719}.Release|Any CPU.ActiveCfg = Release|Any CPU {FE3606FF-CBC9-421A-A0B5-836E312E7719}.Release|Any CPU.Build.0 = Release|Any CPU + {8E1A1F61-43E4-4629-A25B-7E5FA82697D0}.Debug_Database|Any CPU.ActiveCfg = Debug_Database|Any CPU + {8E1A1F61-43E4-4629-A25B-7E5FA82697D0}.Debug_Database|Any CPU.Build.0 = Debug_Database|Any CPU + {8E1A1F61-43E4-4629-A25B-7E5FA82697D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8E1A1F61-43E4-4629-A25B-7E5FA82697D0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8E1A1F61-43E4-4629-A25B-7E5FA82697D0}.Release_Database|Any CPU.ActiveCfg = Release_Database|Any CPU + {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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/BenchmarksApps/TodosApi/DataExtensions.cs b/src/BenchmarksApps/TodosApi/DataExtensions.cs new file mode 100644 index 000000000..4e157b6eb --- /dev/null +++ b/src/BenchmarksApps/TodosApi/DataExtensions.cs @@ -0,0 +1,340 @@ +using System.Data; +using System.Runtime.CompilerServices; + +namespace Npgsql; + +public static class DataExtensions +{ + public static async ValueTask OpenIfClosedAsync(this NpgsqlConnection connection) + { + if (connection.State == ConnectionState.Closed) + { + await connection.OpenAsync(); + } + } + + public static async Task ExecuteAsync(this NpgsqlConnection connection, string commandText) + { + await using var cmd = connection.CreateCommand(commandText); + + await connection.OpenIfClosedAsync(); + return await cmd.ExecuteNonQueryAsync(); + } + + public static async Task ExecuteAsync(this NpgsqlDataSource dataSource, string commandText) + { + await using var cmd = dataSource.CreateCommand(commandText); + + return await cmd.ExecuteNonQueryAsync(); + } + + public static async Task ExecuteAsync(this NpgsqlConnection connection, string commandText, params NpgsqlParameter[] parameters) + { + await using var cmd = connection.CreateCommand(commandText, parameters); + + await connection.OpenIfClosedAsync(); + return await cmd.ExecuteNonQueryAsync(); + } + + public static async Task ExecuteAsync(this NpgsqlDataSource dataSource, string commandText, params NpgsqlParameter[] parameters) + { + await using var cmd = dataSource.CreateCommand(commandText, parameters); + + return await cmd.ExecuteNonQueryAsync(); + } + + public static async Task ExecuteAsync(this NpgsqlConnection connection, string commandText, Action configureParameters) + { + await using var cmd = connection.CreateCommand(commandText, configureParameters); + + await connection.OpenIfClosedAsync(); + return await cmd.ExecuteNonQueryAsync(); + } + + public static async Task ExecuteAsync(this NpgsqlDataSource dataSource, string commandText, Action configureParameters) + { + await using var cmd = dataSource.CreateCommand(commandText, configureParameters); + + return await cmd.ExecuteNonQueryAsync(); + } + + public static async Task QuerySingleAsync(this NpgsqlConnection connection, string commandText, params NpgsqlParameter[] parameters) + where T : IDataReaderMapper + { + await connection.OpenIfClosedAsync(); + await using var reader = await connection.QuerySingleAsync(commandText, parameters); + + return await reader.MapSingleAsync(); + } + + public static async Task QuerySingleAsync(this NpgsqlDataSource dataSource, string commandText, params NpgsqlParameter[] parameters) + where T : IDataReaderMapper + { + await using var reader = await dataSource.QuerySingleAsync(commandText, parameters); + + return await reader.MapSingleAsync(); + } + + public static async Task QuerySingleAsync(this NpgsqlConnection connection, string commandText, Action? configureParameters = null) + where T : IDataReaderMapper + { + await using var cmd = connection.CreateCommand(commandText, configureParameters); + + await using var reader = await connection.QuerySingleAsync(cmd); + + return await reader.MapSingleAsync(); + } + + public static async Task QuerySingleAsync(this NpgsqlDataSource dataSource, string commandText, Action? configureParameters = null) + where T : IDataReaderMapper + { + await using var cmd = dataSource.CreateCommand(commandText, configureParameters); + + await using var reader = await cmd.QuerySingleAsync(); + + return await reader.MapSingleAsync(); + } + + public static async IAsyncEnumerable QueryAsync(this NpgsqlConnection connection, string commandText, params NpgsqlParameter[] parameters) + where T : IDataReaderMapper + { + var query = connection.QueryAsync(commandText, parameterCollection => parameterCollection.AddRange(parameters)); + + await foreach (var item in query) + { + yield return item; + } + } + + public static async IAsyncEnumerable QueryAsync(this NpgsqlDataSource dataSource, string commandText, params NpgsqlParameter[] parameters) + where T : IDataReaderMapper + { + var query = dataSource.QueryAsync(commandText, parameterCollection => parameterCollection.AddRange(parameters)); + + await foreach (var item in query) + { + yield return item; + } + } + + public static async IAsyncEnumerable QueryAsync(this NpgsqlConnection connection, string commandText, Action? configureParameters = null) + where T : IDataReaderMapper + { + await using var cmd = connection.CreateCommand(commandText, configureParameters); + + await connection.OpenIfClosedAsync(); + + await using var reader = await connection.QueryAsync(cmd); + + await foreach (var item in MapAsync(reader)) + { + yield return item; + } + } + + public static async IAsyncEnumerable QueryAsync(this NpgsqlDataSource dataSource, string commandText, Action? configureParameters = null) + where T : IDataReaderMapper + { + await using var cmd = dataSource.CreateCommand(commandText, configureParameters); + + await using var reader = await cmd.QueryAsync(); + + await foreach (var item in MapAsync(reader)) + { + yield return item; + } + } + + + public static Task MapSingleAsync(this NpgsqlDataReader reader) + where T : IDataReaderMapper + => MapSingleAsync(reader, T.Map); + + public static async Task MapSingleAsync(this NpgsqlDataReader reader, Func mapper) + { + if (!reader.HasRows) + { + return default; + } + + await reader.ReadAsync(); + + return mapper(reader); + } + + public static IAsyncEnumerable MapAsync(this NpgsqlDataReader reader) + where T : IDataReaderMapper + => MapAsync(reader, T.Map); + + public static async IAsyncEnumerable MapAsync(this NpgsqlDataReader reader, Func mapper) + { + if (!reader.HasRows) + { + yield break; + } + + while (await reader.ReadAsync()) + { + yield return mapper(reader); + } + } + + public static Task QuerySingleAsync(this NpgsqlConnection connection, string commandText, params NpgsqlParameter[] parameters) + => QueryAsync(connection, commandText, CommandBehavior.SingleResult | CommandBehavior.SingleRow, parameters); + + public static Task QuerySingleAsync(this NpgsqlDataSource dataSource, string commandText, params NpgsqlParameter[] parameters) + => QueryAsync(dataSource, commandText, CommandBehavior.SingleResult | CommandBehavior.SingleRow, parameters); + + public static Task QuerySingleAsync(this NpgsqlConnection connection, NpgsqlCommand command) + => QueryAsync(connection, command, CommandBehavior.SingleResult | CommandBehavior.SingleRow); + + public static Task QuerySingleAsync(this NpgsqlCommand command) + => QueryAsync(command, CommandBehavior.SingleResult | CommandBehavior.SingleRow); + + public static async Task QueryAsync(this NpgsqlConnection connection, string commandText, CommandBehavior commandBehavior, params NpgsqlParameter[] parameters) + { + await using var cmd = connection.CreateCommand(commandText, parameters); + + await connection.OpenIfClosedAsync(); + return await cmd.ExecuteReaderAsync(commandBehavior); + } + + public static async Task QueryAsync(this NpgsqlDataSource dataSource, string commandText, CommandBehavior commandBehavior, params NpgsqlParameter[] parameters) + { + await using var cmd = dataSource.CreateCommand(commandText, parameters); + + return await cmd.ExecuteReaderAsync(commandBehavior); + } + + public static Task QueryAsync(this NpgsqlConnection connection, NpgsqlCommand command) + => QueryAsync(connection, command, CommandBehavior.Default); + + public static Task QueryAsync(this NpgsqlCommand command) + => QueryAsync(command, CommandBehavior.Default); + + public static async Task QueryAsync(this NpgsqlConnection connection, NpgsqlCommand command, CommandBehavior commandBehavior) + { + await connection.OpenIfClosedAsync(); + return await command.ExecuteReaderAsync(commandBehavior); + } + + public static async Task QueryAsync(this NpgsqlCommand command, CommandBehavior commandBehavior) + { + return await command.ExecuteReaderAsync(commandBehavior); + } + + public static async Task> ToListAsync(this IAsyncEnumerable enumerable, int? initialCapacity = null) + { + var list = initialCapacity.HasValue ? new List(initialCapacity.Value) : new List(); + + await foreach (var item in enumerable) + { + list.Add(item); + } + + return list; + } + + public static NpgsqlParameterCollection AddTyped(this NpgsqlParameterCollection parameters, T? value) + { + parameters.Add(new NpgsqlParameter + { + TypedValue = value + }); + return parameters; + } + + public static NpgsqlParameter AsTypedDbParameter(this T value) + { + var parameter = new NpgsqlParameter + { + TypedValue = value + }; + + return parameter; + } + + public static (string Name, object? Value) AsNamedDbParameter(this string? value, [CallerArgumentExpression(nameof(value))] string name = null!) => + AsNamedDbParameter((object?)value, name); + + public static (string Name, object? Value) AsNamedDbParameter(this DateTime? value, [CallerArgumentExpression(nameof(value))] string name = null!) => + AsNamedDbParameter((object?)value, name); + + public static (string Name, object? Value) AsNamedDbParameter(this DateTimeOffset? value, [CallerArgumentExpression(nameof(value))] string name = null!) => + AsNamedDbParameter((object?)value, name); + + public static (string Name, object? Value) AsNamedDbParameter(this bool value, [CallerArgumentExpression(nameof(value))] string name = null!) => + AsNamedDbParameter((object)value, name); + + public static (string Name, object? Value) AsNamedDbParameter(this bool? value, [CallerArgumentExpression(nameof(value))] string name = null!) => + AsNamedDbParameter((object?)value, name); + + public static (string Name, object? Value) AsNamedDbParameter(this int value, [CallerArgumentExpression(nameof(value))] string name = null!) => + AsNamedDbParameter((object)value, name); + + public static (string Name, object? Value) AsNamedDbParameter(this int? value, [CallerArgumentExpression(nameof(value))] string name = null!) => + AsNamedDbParameter((object?)value, name); + + public static (string Name, object? Value) AsNamedDbParameter(this long value, [CallerArgumentExpression(nameof(value))] string name = null!) => + AsNamedDbParameter((object)value, name); + + public static (string Name, object? Value) AsNamedDbParameter(this long? value, [CallerArgumentExpression(nameof(value))] string name = null!) => + AsNamedDbParameter((object?)value, name); + + public static (string Name, object? Value) AsNamedDbParameter(this double value, [CallerArgumentExpression(nameof(value))] string name = null!) => + AsNamedDbParameter((object)value, name); + + public static (string Name, object? Value) AsNamedDbParameter(this double? value, [CallerArgumentExpression(nameof(value))] string name = null!) => + AsNamedDbParameter((object?)value, name); + + private static (string Name, object? Value) AsNamedDbParameter(this object? value, [CallerArgumentExpression(nameof(value))] string name = null!) + { + ArgumentException.ThrowIfNullOrEmpty(name); + return (CleanParameterName(name), value ?? DBNull.Value); + } + + private static NpgsqlCommand CreateCommand(this NpgsqlConnection connection, string commandText, params NpgsqlParameter[] parameters) => + ConfigureCommand(connection.CreateCommand(), commandText, parameters); + + private static NpgsqlCommand CreateCommand(this NpgsqlDataSource dataSource, string commandText, params NpgsqlParameter[] parameters) => + ConfigureCommand(dataSource.CreateCommand(), commandText, parameters); + + private static NpgsqlCommand ConfigureCommand(NpgsqlCommand cmd, string commandText, NpgsqlParameter[] parameters) + { + cmd.CommandText = commandText; + + for (var i = 0; i < parameters.Length; i++) + { + cmd.Parameters.Add(parameters[i]); + } + + return cmd; + } + + private static NpgsqlCommand CreateCommand(this NpgsqlConnection connection, string commandText, Action? configureParameters = null) => + ConfigureCommand(connection.CreateCommand(commandText), configureParameters); + + private static NpgsqlCommand CreateCommand(this NpgsqlDataSource dataSource, string commandText, Action? configureParameters = null) => + ConfigureCommand(dataSource.CreateCommand(commandText), configureParameters); + + private static NpgsqlCommand ConfigureCommand(NpgsqlCommand cmd, Action? configureParameters = null) + { + if (configureParameters is not null) + { + configureParameters(cmd.Parameters); + } + + return cmd; + } + + private static string CleanParameterName(string name) + { + var lastIndexOfPeriod = name.LastIndexOf('.'); + return lastIndexOfPeriod > 0 ? name[(lastIndexOfPeriod + 1)..] : name; + } +} + +public interface IDataReaderMapper where T : IDataReaderMapper +{ + abstract static T Map(NpgsqlDataReader dataReader); +} \ No newline at end of file diff --git a/src/BenchmarksApps/TodosApi/Database.cs b/src/BenchmarksApps/TodosApi/Database.cs new file mode 100644 index 000000000..0f762973d --- /dev/null +++ b/src/BenchmarksApps/TodosApi/Database.cs @@ -0,0 +1,45 @@ +using Npgsql; + +namespace TodosApi; + +internal static class Database +{ + public static async Task Initialize(IServiceProvider services, ILogger logger) + { + var db = services.GetRequiredService(); + + if (Environment.GetEnvironmentVariable("SUPPRESS_DB_INIT") != "true") + { + logger.LogInformation("Ensuring database exists and is up to date at connection string '{connectionString}'", ObscurePassword(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.IsComplete)} boolean NOT NULL DEFAULT false + ); + ALTER TABLE IF EXISTS public.todos + OWNER to "TodosApp"; + DELETE FROM public.todos; + """; + await db.ExecuteAsync(sql); + } + else + { + logger.LogInformation("Database initialization disabled for connection string '{connectionString}'", ObscurePassword(db.ConnectionString)); + } + + string ObscurePassword(string connectionString) + { + var passwordKey = "Password="; + var passwordIndex = connectionString.IndexOf(passwordKey, 0, StringComparison.OrdinalIgnoreCase); + if (passwordIndex < 0) + { + return connectionString; + } + var semiColonIndex = connectionString.IndexOf(";", passwordIndex, StringComparison.OrdinalIgnoreCase); + return string.Concat(connectionString.AsSpan(0, passwordIndex + passwordKey.Length), "*****", semiColonIndex >= 0 ? connectionString[semiColonIndex..] : ""); + } + } +} diff --git a/src/BenchmarksApps/TodosApi/JwtConfiguration.cs b/src/BenchmarksApps/TodosApi/JwtConfiguration.cs new file mode 100644 index 000000000..d261ab40e --- /dev/null +++ b/src/BenchmarksApps/TodosApi/JwtConfiguration.cs @@ -0,0 +1,94 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; + +namespace Microsoft.AspNetCore.Builder; + +internal static class JwtConfiguration +{ + /// + /// 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) + { + return options => + { + if (!builder.Environment.IsDevelopment()) + { + // When not running in development configure the JWT signing key from environment variable + var jwtKeyMaterialValue = builder.Configuration["JWT_SIGNING_KEY"]; + + if (string.IsNullOrEmpty(jwtKeyMaterialValue)) + { + throw new InvalidOperationException("JWT signing key not found!"); + } + + var jwtKeyMaterial = Convert.FromBase64String(jwtKeyMaterialValue); + var jwtSigningKey = new SymmetricSecurityKey(jwtKeyMaterial); + options.TokenValidationParameters.IssuerSigningKey = jwtSigningKey; + } + + // Validate the JWT options + builder.Services.AddOptions(JwtBearerDefaults.AuthenticationScheme) + .Validate(ValidateJwtOptions, + "JWT options are not configured. Run 'dotnet user-jwts create' in project directory to configure JWT.") + .ValidateOnStart(); + }; + } + + private const string JwtOptionsLogMessage = "JwtBearerAuthentication options configuration: {JwtOptions}"; + + /// + /// Validates that JWT Bearer authentication has been configured correctly. + /// + /// + /// + /// + /// true if required JWT Bearer settings are loaded, otherwise false. + public static bool ValidateJwtOptions(JwtBearerOptions options, IHostEnvironment hostEnvironment, ILoggerFactory loggerFactory) + { + var relevantOptions = new JwtOptionsSummary + { + Audience = options.Audience, + ClaimsIssuer = options.ClaimsIssuer, + Audiences = options.TokenValidationParameters?.ValidAudiences, + Issuers = options.TokenValidationParameters?.ValidIssuers, + IssuerSigningKey = options.TokenValidationParameters?.IssuerSigningKey, + IssuerSigningKeys = options.TokenValidationParameters?.IssuerSigningKeys + }; + + var logger = loggerFactory.CreateLogger(hostEnvironment.ApplicationName ?? nameof(Program)); + var jwtOptionsJson = JsonSerializer.Serialize(relevantOptions, JwtOptionsJsonSerializerContext.Default.JwtOptionsSummary); + + if ((string.IsNullOrEmpty(relevantOptions.Audience) && relevantOptions.Audiences?.Any() != true) + || (relevantOptions.ClaimsIssuer is null && relevantOptions.Issuers?.Any() != true) + || (relevantOptions.IssuerSigningKey is null && relevantOptions.IssuerSigningKeys?.Any() != true)) + { + logger.LogError(JwtOptionsLogMessage, jwtOptionsJson); + return false; + } + + logger.LogInformation(JwtOptionsLogMessage, jwtOptionsJson); + return true; + } +} + + +internal class JwtOptionsSummary +{ + public string? Audience { get; set; } + public string? ClaimsIssuer { get; set; } + public IEnumerable? Audiences { get; set; } + public IEnumerable? Issuers { get; set; } + public SecurityKey? IssuerSigningKey { get; set; } + public IEnumerable? IssuerSigningKeys { get; set; } +} + +[JsonSerializable(typeof(JwtOptionsSummary))] +internal partial class JwtOptionsJsonSerializerContext : JsonSerializerContext +{ +} \ No newline at end of file diff --git a/src/BenchmarksApps/TodosApi/Program.cs b/src/BenchmarksApps/TodosApi/Program.cs new file mode 100644 index 000000000..9f009923e --- /dev/null +++ b/src/BenchmarksApps/TodosApi/Program.cs @@ -0,0 +1,47 @@ +using Microsoft.Extensions.Logging.Configuration; +using Npgsql; +using TodosApi; + +var builder = WebApplication.CreateSlimBuilder(args); + +// Load custom configuration +var settingsFiles = new[] { "appsettings.json", $"appsettings.{builder.Environment.EnvironmentName}.json" }; +foreach (var settingsFile in settingsFiles) +{ + builder.Configuration.AddJsonFile(builder.Environment.ContentRootFileProvider, settingsFile, optional: false, reloadOnChange: true); +} +#if DEBUG || DEBUG_DATABASE +builder.Configuration.AddUserSecrets(); +#endif + +// Configure logging +builder.Logging + .AddConfiguration(builder.Configuration) + .AddSimpleConsole(); + +// Configure authentication & authorization +builder.Services.AddAuthentication() + .AddJwtBearer(JwtConfiguration.ConfigureJwtBearer(builder)); + +builder.Services.AddAuthorization(); + +// Configure data access +var connectionString = builder.Configuration.GetConnectionString("TodoDb") + ?? builder.Configuration["CONNECTION_STRING"] + ?? throw new InvalidOperationException("Connection string not found. If running locally, set the connection string in user secrets for key 'ConnectionStrings:TodoDb'."); +builder.Services.AddSingleton(_ => new NpgsqlSlimDataSourceBuilder(connectionString).Build()); + +// Configure JSON serialization +builder.Services.ConfigureHttpJsonOptions(options => +{ + options.SerializerOptions.TypeInfoResolverChain.Insert(0, JwtOptionsJsonSerializerContext.Default); + options.SerializerOptions.TypeInfoResolverChain.Insert(0, TodoApiJsonSerializerContext.Default); +}); + +var app = builder.Build(); + +await Database.Initialize(app.Services, app.Logger); + +app.MapTodoApi(); + +app.Run(); diff --git a/src/BenchmarksApps/TodosApi/Todo.cs b/src/BenchmarksApps/TodosApi/Todo.cs new file mode 100644 index 000000000..6bbf8b970 --- /dev/null +++ b/src/BenchmarksApps/TodosApi/Todo.cs @@ -0,0 +1,24 @@ +using Npgsql; + +namespace TodosApi; + +sealed class Todo : IDataReaderMapper +{ + public int Id { get; set; } + + public string Title { get; set; } = default!; + + public DateOnly? DueBy { get; set; } + + public bool IsComplete { get; set; } + + public static Todo Map(NpgsqlDataReader dataReader) + { + return !dataReader.HasRows ? new() : new() + { + Id = dataReader.GetInt32(dataReader.GetOrdinal(nameof(Id))), + Title = dataReader.GetString(dataReader.GetOrdinal(nameof(Title))), + IsComplete = dataReader.GetBoolean(dataReader.GetOrdinal(nameof(IsComplete))) + }; + } +} diff --git a/src/BenchmarksApps/TodosApi/TodoApi.cs b/src/BenchmarksApps/TodosApi/TodoApi.cs new file mode 100644 index 000000000..a1e08a745 --- /dev/null +++ b/src/BenchmarksApps/TodosApi/TodoApi.cs @@ -0,0 +1,99 @@ +using System.Text.Json.Serialization; +using Microsoft.AspNetCore.Http.HttpResults; +using Npgsql; +using TodosApi; + +namespace Microsoft.AspNetCore.Routing; + +public static class TodoApi +{ + public static RouteGroupBuilder MapTodoApi(this IEndpointRouteBuilder routes) + { + var group = routes.MapGroup("/api/todos"); + + group.WithTags("Todos"); + + group.MapGet("/", (NpgsqlDataSource db) => db.QueryAsync("SELECT * FROM Todos")) + .WithName("GetAllTodos"); + + group.MapGet("/complete", (NpgsqlDataSource db) => db.QueryAsync("SELECT * FROM Todos WHERE IsComplete = true")) + .WithName("GetCompleteTodos"); + + group.MapGet("/incomplete", (NpgsqlDataSource db) => db.QueryAsync("SELECT * FROM Todos WHERE IsComplete = false")) + .WithName("GetIncompleteTodos"); + + group.MapGet("/{id:int}", async Task, NotFound>> (int id, NpgsqlDataSource db) => + await db.QuerySingleAsync("SELECT * FROM Todos WHERE Id = $1", id.AsTypedDbParameter()) + is Todo todo + ? TypedResults.Ok(todo) + : TypedResults.NotFound()) + .WithName("GetTodoById"); + + group.MapGet("/find", async Task, NotFound>> (string title, bool? isComplete, NpgsqlDataSource db) => + await db.QuerySingleAsync("SELECT * FROM Todos WHERE LOWER(Title) = LOWER($1) AND ($2 is NULL OR IsComplete = $2)", + title.AsTypedDbParameter(), + isComplete.AsTypedDbParameter()) + is Todo todo + ? TypedResults.Ok(todo) + : TypedResults.NotFound()) + .WithName("FindTodo"); + + group.MapPost("/", async Task, ValidationProblem>> (Todo todo, NpgsqlDataSource db) => + { + var createdTodo = await db.QuerySingleAsync( + "INSERT INTO Todos(Title, IsComplete) Values($1, $2) RETURNING *", + todo.Title.AsTypedDbParameter(), + todo.IsComplete.AsTypedDbParameter()); + + return TypedResults.Created($"/todos/{createdTodo?.Id}", createdTodo); + }) + .WithName("CreateTodo"); + + group.MapPut("/{id}", async Task> (int id, Todo inputTodo, NpgsqlDataSource db) => + { + inputTodo.Id = id; + + return await db.ExecuteAsync("UPDATE Todos SET Title = $1, IsComplete = $2 WHERE Id = $3", + inputTodo.Title.AsTypedDbParameter(), + inputTodo.IsComplete.AsTypedDbParameter(), + id.AsTypedDbParameter()) == 1 + ? TypedResults.NoContent() + : TypedResults.NotFound(); + }) + .WithName("UpdateTodo"); + + group.MapPut("/{id}/mark-complete", async Task> (int id, NpgsqlDataSource db) => + await db.ExecuteAsync("UPDATE Todos SET IsComplete = true WHERE Id = $1", id.AsTypedDbParameter()) == 1 + ? TypedResults.NoContent() + : TypedResults.NotFound()) + .WithName("MarkComplete"); + + group.MapPut("/{id}/mark-incomplete", async Task> (int id, NpgsqlDataSource db) => + await db.ExecuteAsync("UPDATE Todos SET IsComplete = false WHERE Id = $1", id.AsTypedDbParameter()) == 1 + ? TypedResults.NoContent() + : TypedResults.NotFound()) + .WithName("MarkIncomplete"); + + group.MapDelete("/{id}", async Task> (int id, NpgsqlDataSource db) => + await db.ExecuteAsync("DELETE FROM Todos WHERE Id = $1", id.AsTypedDbParameter()) == 1 + ? TypedResults.NoContent() + : TypedResults.NotFound()) + .WithName("DeleteTodo"); + + group.MapDelete("/delete-all", async (NpgsqlDataSource db) => TypedResults.Ok(await db.ExecuteAsync("DELETE FROM Todos"))) + .WithName("DeleteAll") + .RequireAuthorization(policy => policy.RequireAuthenticatedUser().RequireRole("admin")); + + return group; + } +} + + + +[JsonSerializable(typeof(Todo))] +[JsonSerializable(typeof(IAsyncEnumerable))] +[JsonSerializable(typeof(IEnumerable))] +internal partial class TodoApiJsonSerializerContext : JsonSerializerContext +{ + +} diff --git a/src/BenchmarksApps/TodosApi/TodosApi.csproj b/src/BenchmarksApps/TodosApi/TodosApi.csproj new file mode 100644 index 000000000..0b355c897 --- /dev/null +++ b/src/BenchmarksApps/TodosApi/TodosApi.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + enable + false + true + b8ffb8d3-b768-460b-ac1f-ef267c954c85 + true + + + + + + + diff --git a/src/BenchmarksApps/TodosApi/appsettings.Development.json b/src/BenchmarksApps/TodosApi/appsettings.Development.json new file mode 100644 index 000000000..b52a21dc2 --- /dev/null +++ b/src/BenchmarksApps/TodosApi/appsettings.Development.json @@ -0,0 +1,18 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "Microsoft.AspNetCore": "Debug" + } + }, + "Authentication": { + "Schemes": { + "Bearer": { + "ValidAudiences": [ + "http://localhost:5054" + ], + "ValidIssuer": "dotnet-user-jwts" + } + } + } +} \ No newline at end of file diff --git a/src/BenchmarksApps/TodosApi/appsettings.json b/src/BenchmarksApps/TodosApi/appsettings.json new file mode 100644 index 000000000..4e5652984 --- /dev/null +++ b/src/BenchmarksApps/TodosApi/appsettings.json @@ -0,0 +1,19 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "Authentication": { + "Schemes": { + "Bearer": { + "ValidAudiences": [ + "http://localhost:5054" + ], + "ValidIssuer": "dotnet-user-jwts" + } + } + } +} \ No newline at end of file From 18f67f33f663b09d028236565baf5adbd114909b Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Thu, 30 Mar 2023 11:04:41 -0700 Subject: [PATCH 02/26] Fix logging configuration & set to publish release --- src/BenchmarksApps/TodosApi/Program.cs | 5 ++--- src/BenchmarksApps/TodosApi/TodosApi.csproj | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/BenchmarksApps/TodosApi/Program.cs b/src/BenchmarksApps/TodosApi/Program.cs index 9f009923e..72012f271 100644 --- a/src/BenchmarksApps/TodosApi/Program.cs +++ b/src/BenchmarksApps/TodosApi/Program.cs @@ -8,7 +8,7 @@ var settingsFiles = new[] { "appsettings.json", $"appsettings.{builder.Environment.EnvironmentName}.json" }; foreach (var settingsFile in settingsFiles) { - builder.Configuration.AddJsonFile(builder.Environment.ContentRootFileProvider, settingsFile, optional: false, reloadOnChange: true); + builder.Configuration.AddJsonFile(builder.Environment.ContentRootFileProvider, settingsFile, optional: true, reloadOnChange: true); } #if DEBUG || DEBUG_DATABASE builder.Configuration.AddUserSecrets(); @@ -16,7 +16,7 @@ // Configure logging builder.Logging - .AddConfiguration(builder.Configuration) + .AddConfiguration(builder.Configuration.GetSection("Logging")) .AddSimpleConsole(); // Configure authentication & authorization @@ -34,7 +34,6 @@ // Configure JSON serialization builder.Services.ConfigureHttpJsonOptions(options => { - options.SerializerOptions.TypeInfoResolverChain.Insert(0, JwtOptionsJsonSerializerContext.Default); options.SerializerOptions.TypeInfoResolverChain.Insert(0, TodoApiJsonSerializerContext.Default); }); diff --git a/src/BenchmarksApps/TodosApi/TodosApi.csproj b/src/BenchmarksApps/TodosApi/TodosApi.csproj index 0b355c897..b95da2398 100644 --- a/src/BenchmarksApps/TodosApi/TodosApi.csproj +++ b/src/BenchmarksApps/TodosApi/TodosApi.csproj @@ -8,6 +8,7 @@ true b8ffb8d3-b768-460b-ac1f-ef267c954c85 true + true From e6795f43299cd8acebfab59833bf1bf5eb2354a0 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Thu, 30 Mar 2023 11:43:36 -0700 Subject: [PATCH 03/26] Seed database on initialization --- src/BenchmarksApps/TodosApi/DataExtensions.cs | 71 +++---------------- src/BenchmarksApps/TodosApi/Database.cs | 9 +++ src/BenchmarksApps/TodosApi/Todo.cs | 2 +- src/BenchmarksApps/TodosApi/TodoApi.cs | 32 +++++---- 4 files changed, 39 insertions(+), 75 deletions(-) diff --git a/src/BenchmarksApps/TodosApi/DataExtensions.cs b/src/BenchmarksApps/TodosApi/DataExtensions.cs index 4e157b6eb..0a3b1b2c0 100644 --- a/src/BenchmarksApps/TodosApi/DataExtensions.cs +++ b/src/BenchmarksApps/TodosApi/DataExtensions.cs @@ -1,9 +1,8 @@ using System.Data; -using System.Runtime.CompilerServices; namespace Npgsql; -public static class DataExtensions +internal static class DataExtensions { public static async ValueTask OpenIfClosedAsync(this NpgsqlConnection connection) { @@ -254,62 +253,20 @@ public static NpgsqlParameter AsTypedDbParameter(this T value) return parameter; } - public static (string Name, object? Value) AsNamedDbParameter(this string? value, [CallerArgumentExpression(nameof(value))] string name = null!) => - AsNamedDbParameter((object?)value, name); - - public static (string Name, object? Value) AsNamedDbParameter(this DateTime? value, [CallerArgumentExpression(nameof(value))] string name = null!) => - AsNamedDbParameter((object?)value, name); - - public static (string Name, object? Value) AsNamedDbParameter(this DateTimeOffset? value, [CallerArgumentExpression(nameof(value))] string name = null!) => - AsNamedDbParameter((object?)value, name); - - public static (string Name, object? Value) AsNamedDbParameter(this bool value, [CallerArgumentExpression(nameof(value))] string name = null!) => - AsNamedDbParameter((object)value, name); - - public static (string Name, object? Value) AsNamedDbParameter(this bool? value, [CallerArgumentExpression(nameof(value))] string name = null!) => - AsNamedDbParameter((object?)value, name); - - public static (string Name, object? Value) AsNamedDbParameter(this int value, [CallerArgumentExpression(nameof(value))] string name = null!) => - AsNamedDbParameter((object)value, name); - - public static (string Name, object? Value) AsNamedDbParameter(this int? value, [CallerArgumentExpression(nameof(value))] string name = null!) => - AsNamedDbParameter((object?)value, name); - - public static (string Name, object? Value) AsNamedDbParameter(this long value, [CallerArgumentExpression(nameof(value))] string name = null!) => - AsNamedDbParameter((object)value, name); - - public static (string Name, object? Value) AsNamedDbParameter(this long? value, [CallerArgumentExpression(nameof(value))] string name = null!) => - AsNamedDbParameter((object?)value, name); - - public static (string Name, object? Value) AsNamedDbParameter(this double value, [CallerArgumentExpression(nameof(value))] string name = null!) => - AsNamedDbParameter((object)value, name); - - public static (string Name, object? Value) AsNamedDbParameter(this double? value, [CallerArgumentExpression(nameof(value))] string name = null!) => - AsNamedDbParameter((object?)value, name); - - private static (string Name, object? Value) AsNamedDbParameter(this object? value, [CallerArgumentExpression(nameof(value))] string name = null!) - { - ArgumentException.ThrowIfNullOrEmpty(name); - return (CleanParameterName(name), value ?? DBNull.Value); - } - private static NpgsqlCommand CreateCommand(this NpgsqlConnection connection, string commandText, params NpgsqlParameter[] parameters) => - ConfigureCommand(connection.CreateCommand(), commandText, parameters); + ConfigureCommand(connection.CreateCommand(commandText), parameters); private static NpgsqlCommand CreateCommand(this NpgsqlDataSource dataSource, string commandText, params NpgsqlParameter[] parameters) => - ConfigureCommand(dataSource.CreateCommand(), commandText, parameters); - - private static NpgsqlCommand ConfigureCommand(NpgsqlCommand cmd, string commandText, NpgsqlParameter[] parameters) - { - cmd.CommandText = commandText; + ConfigureCommand(dataSource.CreateCommand(commandText), parameters); - for (var i = 0; i < parameters.Length; i++) + private static NpgsqlCommand ConfigureCommand(NpgsqlCommand cmd, NpgsqlParameter[] parameters) => + ConfigureCommand(cmd, parameterCollection => { - cmd.Parameters.Add(parameters[i]); - } - - return cmd; - } + for (var i = 0; i < parameters.Length; i++) + { + parameterCollection.Add(parameters[i]); + } + }); private static NpgsqlCommand CreateCommand(this NpgsqlConnection connection, string commandText, Action? configureParameters = null) => ConfigureCommand(connection.CreateCommand(commandText), configureParameters); @@ -326,15 +283,9 @@ private static NpgsqlCommand ConfigureCommand(NpgsqlCommand cmd, Action 0 ? name[(lastIndexOfPeriod + 1)..] : name; - } } -public interface IDataReaderMapper where T : IDataReaderMapper +internal interface IDataReaderMapper where T : IDataReaderMapper { abstract static T Map(NpgsqlDataReader dataReader); } \ No newline at end of file diff --git a/src/BenchmarksApps/TodosApi/Database.cs b/src/BenchmarksApps/TodosApi/Database.cs index 0f762973d..191d953a2 100644 --- a/src/BenchmarksApps/TodosApi/Database.cs +++ b/src/BenchmarksApps/TodosApi/Database.cs @@ -17,11 +17,20 @@ 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 ); ALTER TABLE IF EXISTS public.todos OWNER to "TodosApp"; 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); } diff --git a/src/BenchmarksApps/TodosApi/Todo.cs b/src/BenchmarksApps/TodosApi/Todo.cs index 6bbf8b970..5e76782f2 100644 --- a/src/BenchmarksApps/TodosApi/Todo.cs +++ b/src/BenchmarksApps/TodosApi/Todo.cs @@ -2,7 +2,7 @@ namespace TodosApi; -sealed class Todo : IDataReaderMapper +internal sealed class Todo : IDataReaderMapper { public int Id { get; set; } diff --git a/src/BenchmarksApps/TodosApi/TodoApi.cs b/src/BenchmarksApps/TodosApi/TodoApi.cs index a1e08a745..4d9cfb51f 100644 --- a/src/BenchmarksApps/TodosApi/TodoApi.cs +++ b/src/BenchmarksApps/TodosApi/TodoApi.cs @@ -5,14 +5,12 @@ namespace Microsoft.AspNetCore.Routing; -public static class TodoApi +internal static class TodoApi { public static RouteGroupBuilder MapTodoApi(this IEndpointRouteBuilder routes) { var group = routes.MapGroup("/api/todos"); - group.WithTags("Todos"); - group.MapGet("/", (NpgsqlDataSource db) => db.QueryAsync("SELECT * FROM Todos")) .WithName("GetAllTodos"); @@ -23,14 +21,16 @@ public static RouteGroupBuilder MapTodoApi(this IEndpointRouteBuilder routes) .WithName("GetIncompleteTodos"); group.MapGet("/{id:int}", async Task, NotFound>> (int id, NpgsqlDataSource db) => - await db.QuerySingleAsync("SELECT * FROM Todos WHERE Id = $1", id.AsTypedDbParameter()) + await db.QuerySingleAsync( + "SELECT * FROM Todos WHERE Id = $1", id.AsTypedDbParameter()) is Todo todo ? TypedResults.Ok(todo) : TypedResults.NotFound()) .WithName("GetTodoById"); group.MapGet("/find", async Task, NotFound>> (string title, bool? isComplete, NpgsqlDataSource db) => - await db.QuerySingleAsync("SELECT * FROM Todos WHERE LOWER(Title) = LOWER($1) AND ($2 is NULL OR IsComplete = $2)", + await db.QuerySingleAsync( + "SELECT * FROM Todos WHERE LOWER(Title) = LOWER($1) AND ($2 is NULL OR IsComplete = $2)", title.AsTypedDbParameter(), isComplete.AsTypedDbParameter()) is Todo todo @@ -53,29 +53,33 @@ is Todo todo { inputTodo.Id = id; - return await db.ExecuteAsync("UPDATE Todos SET Title = $1, IsComplete = $2 WHERE Id = $3", - inputTodo.Title.AsTypedDbParameter(), - inputTodo.IsComplete.AsTypedDbParameter(), - id.AsTypedDbParameter()) == 1 - ? TypedResults.NoContent() - : TypedResults.NotFound(); + return await db.ExecuteAsync( + "UPDATE Todos SET Title = $1, IsComplete = $2 WHERE Id = $3", + inputTodo.Title.AsTypedDbParameter(), + inputTodo.IsComplete.AsTypedDbParameter(), + id.AsTypedDbParameter()) == 1 + ? TypedResults.NoContent() + : TypedResults.NotFound(); }) .WithName("UpdateTodo"); group.MapPut("/{id}/mark-complete", async Task> (int id, NpgsqlDataSource db) => - await db.ExecuteAsync("UPDATE Todos SET IsComplete = true WHERE Id = $1", id.AsTypedDbParameter()) == 1 + await db.ExecuteAsync( + "UPDATE Todos SET IsComplete = true WHERE Id = $1", id.AsTypedDbParameter()) == 1 ? TypedResults.NoContent() : TypedResults.NotFound()) .WithName("MarkComplete"); group.MapPut("/{id}/mark-incomplete", async Task> (int id, NpgsqlDataSource db) => - await db.ExecuteAsync("UPDATE Todos SET IsComplete = false WHERE Id = $1", id.AsTypedDbParameter()) == 1 + await db.ExecuteAsync( + "UPDATE Todos SET IsComplete = false WHERE Id = $1", id.AsTypedDbParameter()) == 1 ? TypedResults.NoContent() : TypedResults.NotFound()) .WithName("MarkIncomplete"); group.MapDelete("/{id}", async Task> (int id, NpgsqlDataSource db) => - await db.ExecuteAsync("DELETE FROM Todos WHERE Id = $1", id.AsTypedDbParameter()) == 1 + await db.ExecuteAsync( + "DELETE FROM Todos WHERE Id = $1", id.AsTypedDbParameter()) == 1 ? TypedResults.NoContent() : TypedResults.NotFound()) .WithName("DeleteTodo"); From db3b88086368a74c7c627cd8a68f1145665cb031 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Thu, 30 Mar 2023 12:05:48 -0700 Subject: [PATCH 04/26] More logging config & remove unneeded code --- src/BenchmarksApps/TodosApi/Database.cs | 17 +++-------------- src/BenchmarksApps/TodosApi/Program.cs | 15 ++++++++++++--- src/BenchmarksApps/TodosApi/TodosApi.csproj | 2 ++ 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/BenchmarksApps/TodosApi/Database.cs b/src/BenchmarksApps/TodosApi/Database.cs index 191d953a2..291e616f0 100644 --- a/src/BenchmarksApps/TodosApi/Database.cs +++ b/src/BenchmarksApps/TodosApi/Database.cs @@ -10,7 +10,8 @@ public static async Task Initialize(IServiceProvider services, ILogger logger) if (Environment.GetEnvironmentVariable("SUPPRESS_DB_INIT") != "true") { - logger.LogInformation("Ensuring database exists and is up to date at connection string '{connectionString}'", ObscurePassword(db.ConnectionString)); + // 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 @@ -36,19 +37,7 @@ INSERT INTO } else { - logger.LogInformation("Database initialization disabled for connection string '{connectionString}'", ObscurePassword(db.ConnectionString)); - } - - string ObscurePassword(string connectionString) - { - var passwordKey = "Password="; - var passwordIndex = connectionString.IndexOf(passwordKey, 0, StringComparison.OrdinalIgnoreCase); - if (passwordIndex < 0) - { - return connectionString; - } - var semiColonIndex = connectionString.IndexOf(";", passwordIndex, StringComparison.OrdinalIgnoreCase); - return string.Concat(connectionString.AsSpan(0, passwordIndex + passwordKey.Length), "*****", semiColonIndex >= 0 ? connectionString[semiColonIndex..] : ""); + logger.LogInformation("Database initialization disabled for connection string '{connectionString}'", db.ConnectionString); } } } diff --git a/src/BenchmarksApps/TodosApi/Program.cs b/src/BenchmarksApps/TodosApi/Program.cs index 72012f271..0896a7b45 100644 --- a/src/BenchmarksApps/TodosApi/Program.cs +++ b/src/BenchmarksApps/TodosApi/Program.cs @@ -4,20 +4,20 @@ var builder = WebApplication.CreateSlimBuilder(args); +#if ENABLE_LOGGING // Load custom configuration var settingsFiles = new[] { "appsettings.json", $"appsettings.{builder.Environment.EnvironmentName}.json" }; foreach (var settingsFile in settingsFiles) { builder.Configuration.AddJsonFile(builder.Environment.ContentRootFileProvider, settingsFile, optional: true, reloadOnChange: true); } -#if DEBUG || DEBUG_DATABASE builder.Configuration.AddUserSecrets(); -#endif // Configure logging builder.Logging .AddConfiguration(builder.Configuration.GetSection("Logging")) .AddSimpleConsole(); +#endif // Configure authentication & authorization builder.Services.AddAuthentication() @@ -28,7 +28,11 @@ // Configure data access var connectionString = builder.Configuration.GetConnectionString("TodoDb") ?? builder.Configuration["CONNECTION_STRING"] - ?? throw new InvalidOperationException("Connection string not found. If running locally, set the connection string in user secrets for key 'ConnectionStrings: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()); // Configure JSON serialization @@ -43,4 +47,9 @@ app.MapTodoApi(); +#if !ENABLE_LOGGING +app.Lifetime.ApplicationStarted.Register(() => Console.WriteLine("Application started. Press Ctrl+C to shut down.")); +app.Lifetime.ApplicationStopping.Register(() => Console.WriteLine("Application is shutting down...")); +#endif + app.Run(); diff --git a/src/BenchmarksApps/TodosApi/TodosApi.csproj b/src/BenchmarksApps/TodosApi/TodosApi.csproj index b95da2398..f456a36bc 100644 --- a/src/BenchmarksApps/TodosApi/TodosApi.csproj +++ b/src/BenchmarksApps/TodosApi/TodosApi.csproj @@ -9,6 +9,8 @@ b8ffb8d3-b768-460b-ac1f-ef267c954c85 true true + true + $(DefineConstants);ENABLE_LOGGING From fcd53720bfbf7e096c9d8839fe59efe087115f34 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Thu, 30 Mar 2023 12:37:17 -0700 Subject: [PATCH 05/26] Add health checks --- src/BenchmarksApps/TodosApi/DataExtensions.cs | 7 +++ .../TodosApi/DatabaseHealthCheck.cs | 24 ++++++++ .../TodosApi/JwtConfiguration.cs | 42 -------------- src/BenchmarksApps/TodosApi/JwtHealthCheck.cs | 57 +++++++++++++++++++ src/BenchmarksApps/TodosApi/Program.cs | 7 +++ 5 files changed, 95 insertions(+), 42 deletions(-) create mode 100644 src/BenchmarksApps/TodosApi/DatabaseHealthCheck.cs create mode 100644 src/BenchmarksApps/TodosApi/JwtHealthCheck.cs diff --git a/src/BenchmarksApps/TodosApi/DataExtensions.cs b/src/BenchmarksApps/TodosApi/DataExtensions.cs index 0a3b1b2c0..b9a09edf3 100644 --- a/src/BenchmarksApps/TodosApi/DataExtensions.cs +++ b/src/BenchmarksApps/TodosApi/DataExtensions.cs @@ -27,6 +27,13 @@ public static async Task ExecuteAsync(this NpgsqlDataSource dataSource, str return await cmd.ExecuteNonQueryAsync(); } + public static async Task ExecuteScalarAsync(this NpgsqlDataSource dataSource, string commandText) + { + await using var cmd = dataSource.CreateCommand(commandText); + + return await cmd.ExecuteScalarAsync(CancellationToken.None); + } + public static async Task ExecuteAsync(this NpgsqlConnection connection, string commandText, params NpgsqlParameter[] parameters) { await using var cmd = connection.CreateCommand(commandText, parameters); diff --git a/src/BenchmarksApps/TodosApi/DatabaseHealthCheck.cs b/src/BenchmarksApps/TodosApi/DatabaseHealthCheck.cs new file mode 100644 index 000000000..89bfcd3f4 --- /dev/null +++ b/src/BenchmarksApps/TodosApi/DatabaseHealthCheck.cs @@ -0,0 +1,24 @@ +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Npgsql; + +namespace TodosApi; + +public class DatabaseHealthCheck : IHealthCheck +{ + private readonly NpgsqlDataSource _dataSource; + + public DatabaseHealthCheck(NpgsqlDataSource dataSource) + { + _dataSource = dataSource; + } + + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + var result = await _dataSource.ExecuteScalarAsync("SELECT COUNT(id) FROM public.todos"); + return result switch + { + long count when count >= 0 => HealthCheckResult.Healthy(), + _ => HealthCheckResult.Unhealthy() + }; + } +} diff --git a/src/BenchmarksApps/TodosApi/JwtConfiguration.cs b/src/BenchmarksApps/TodosApi/JwtConfiguration.cs index d261ab40e..ad8cdc160 100644 --- a/src/BenchmarksApps/TodosApi/JwtConfiguration.cs +++ b/src/BenchmarksApps/TodosApi/JwtConfiguration.cs @@ -31,50 +31,8 @@ public static Action ConfigureJwtBearer(WebApplicationBuilder var jwtSigningKey = new SymmetricSecurityKey(jwtKeyMaterial); options.TokenValidationParameters.IssuerSigningKey = jwtSigningKey; } - - // Validate the JWT options - builder.Services.AddOptions(JwtBearerDefaults.AuthenticationScheme) - .Validate(ValidateJwtOptions, - "JWT options are not configured. Run 'dotnet user-jwts create' in project directory to configure JWT.") - .ValidateOnStart(); }; } - - private const string JwtOptionsLogMessage = "JwtBearerAuthentication options configuration: {JwtOptions}"; - - /// - /// Validates that JWT Bearer authentication has been configured correctly. - /// - /// - /// - /// - /// true if required JWT Bearer settings are loaded, otherwise false. - public static bool ValidateJwtOptions(JwtBearerOptions options, IHostEnvironment hostEnvironment, ILoggerFactory loggerFactory) - { - var relevantOptions = new JwtOptionsSummary - { - Audience = options.Audience, - ClaimsIssuer = options.ClaimsIssuer, - Audiences = options.TokenValidationParameters?.ValidAudiences, - Issuers = options.TokenValidationParameters?.ValidIssuers, - IssuerSigningKey = options.TokenValidationParameters?.IssuerSigningKey, - IssuerSigningKeys = options.TokenValidationParameters?.IssuerSigningKeys - }; - - var logger = loggerFactory.CreateLogger(hostEnvironment.ApplicationName ?? nameof(Program)); - var jwtOptionsJson = JsonSerializer.Serialize(relevantOptions, JwtOptionsJsonSerializerContext.Default.JwtOptionsSummary); - - if ((string.IsNullOrEmpty(relevantOptions.Audience) && relevantOptions.Audiences?.Any() != true) - || (relevantOptions.ClaimsIssuer is null && relevantOptions.Issuers?.Any() != true) - || (relevantOptions.IssuerSigningKey is null && relevantOptions.IssuerSigningKeys?.Any() != true)) - { - logger.LogError(JwtOptionsLogMessage, jwtOptionsJson); - return false; - } - - logger.LogInformation(JwtOptionsLogMessage, jwtOptionsJson); - return true; - } } diff --git a/src/BenchmarksApps/TodosApi/JwtHealthCheck.cs b/src/BenchmarksApps/TodosApi/JwtHealthCheck.cs new file mode 100644 index 000000000..00bd5c862 --- /dev/null +++ b/src/BenchmarksApps/TodosApi/JwtHealthCheck.cs @@ -0,0 +1,57 @@ +using System.Text.Json; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Options; + +namespace TodosApi; + +public class JwtHealthCheck : IHealthCheck +{ + private readonly IOptionsMonitor _jwtOptions; + private readonly IHostEnvironment _hostEnvironment; + private readonly ILogger _logger; + + public JwtHealthCheck(IOptionsMonitor jwtOptions, IHostEnvironment hostEnvironment, ILogger logger) + { + _jwtOptions = jwtOptions; + _hostEnvironment = hostEnvironment; + _logger = logger; + } + + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + var valid = ValidateJwtOptions(_jwtOptions.CurrentValue, _hostEnvironment); + var status = valid + ? HealthCheckResult.Healthy("") + : HealthCheckResult.Degraded("JWT options are not configured. Run 'dotnet user-jwts create' in project directory to configure JWT."); + return Task.FromResult(status); + } + + private const string JwtOptionsLogMessage = "JwtBearerAuthentication options configuration: {JwtOptions}"; + + private bool ValidateJwtOptions(JwtBearerOptions options, IHostEnvironment hostEnvironment) + { + var relevantOptions = new JwtOptionsSummary + { + Audience = options.Audience, + ClaimsIssuer = options.ClaimsIssuer, + Audiences = options.TokenValidationParameters?.ValidAudiences, + Issuers = options.TokenValidationParameters?.ValidIssuers, + IssuerSigningKey = options.TokenValidationParameters?.IssuerSigningKey, + IssuerSigningKeys = options.TokenValidationParameters?.IssuerSigningKeys + }; + + var jwtOptionsJson = JsonSerializer.Serialize(relevantOptions, JwtOptionsJsonSerializerContext.Default.JwtOptionsSummary); + + if ((string.IsNullOrEmpty(relevantOptions.Audience) && relevantOptions.Audiences?.Any() != true) + || (relevantOptions.ClaimsIssuer is null && relevantOptions.Issuers?.Any() != true) + || (relevantOptions.IssuerSigningKey is null && relevantOptions.IssuerSigningKeys?.Any() != true)) + { + _logger.LogWarning(JwtOptionsLogMessage, jwtOptionsJson); + return false; + } + + _logger.LogInformation(JwtOptionsLogMessage, jwtOptionsJson); + return true; + } +} diff --git a/src/BenchmarksApps/TodosApi/Program.cs b/src/BenchmarksApps/TodosApi/Program.cs index 0896a7b45..7147f214f 100644 --- a/src/BenchmarksApps/TodosApi/Program.cs +++ b/src/BenchmarksApps/TodosApi/Program.cs @@ -41,10 +41,17 @@ Connection string not found. options.SerializerOptions.TypeInfoResolverChain.Insert(0, TodoApiJsonSerializerContext.Default); }); +// Configure health checks +builder.Services.AddHealthChecks() + .AddCheck("Database") + .AddCheck("JwtAuthentication"); + var app = builder.Build(); await Database.Initialize(app.Services, app.Logger); +app.MapHealthChecks("/health"); + app.MapTodoApi(); #if !ENABLE_LOGGING From c2294dfc4ceb82cbb4528ce6565ff429723cf23d Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Thu, 30 Mar 2023 12:49:19 -0700 Subject: [PATCH 06/26] Add exception handling middleware --- src/BenchmarksApps/TodosApi/JwtHealthCheck.cs | 8 +++----- src/BenchmarksApps/TodosApi/Program.cs | 11 +++++++++++ 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/BenchmarksApps/TodosApi/JwtHealthCheck.cs b/src/BenchmarksApps/TodosApi/JwtHealthCheck.cs index 00bd5c862..332bbd37a 100644 --- a/src/BenchmarksApps/TodosApi/JwtHealthCheck.cs +++ b/src/BenchmarksApps/TodosApi/JwtHealthCheck.cs @@ -8,19 +8,17 @@ namespace TodosApi; public class JwtHealthCheck : IHealthCheck { private readonly IOptionsMonitor _jwtOptions; - private readonly IHostEnvironment _hostEnvironment; private readonly ILogger _logger; - public JwtHealthCheck(IOptionsMonitor jwtOptions, IHostEnvironment hostEnvironment, ILogger logger) + public JwtHealthCheck(IOptionsMonitor jwtOptions, ILogger logger) { _jwtOptions = jwtOptions; - _hostEnvironment = hostEnvironment; _logger = logger; } public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { - var valid = ValidateJwtOptions(_jwtOptions.CurrentValue, _hostEnvironment); + var valid = ValidateJwtOptions(_jwtOptions.CurrentValue); var status = valid ? HealthCheckResult.Healthy("") : HealthCheckResult.Degraded("JWT options are not configured. Run 'dotnet user-jwts create' in project directory to configure JWT."); @@ -29,7 +27,7 @@ public Task CheckHealthAsync(HealthCheckContext context, Canc private const string JwtOptionsLogMessage = "JwtBearerAuthentication options configuration: {JwtOptions}"; - private bool ValidateJwtOptions(JwtBearerOptions options, IHostEnvironment hostEnvironment) + private bool ValidateJwtOptions(JwtBearerOptions options) { var relevantOptions = new JwtOptionsSummary { diff --git a/src/BenchmarksApps/TodosApi/Program.cs b/src/BenchmarksApps/TodosApi/Program.cs index 7147f214f..1b3489f16 100644 --- a/src/BenchmarksApps/TodosApi/Program.cs +++ b/src/BenchmarksApps/TodosApi/Program.cs @@ -46,14 +46,25 @@ Connection string not found. .AddCheck("Database") .AddCheck("JwtAuthentication"); +// Problem details +builder.Services.AddProblemDetails(); + var app = builder.Build(); await Database.Initialize(app.Services, app.Logger); +if (!app.Environment.IsDevelopment()) +{ + app.UseExceptionHandler(); +} + app.MapHealthChecks("/health"); app.MapTodoApi(); +// Enables testing request exception handling behavior +app.MapGet("/throw", (HttpContext httpContext) => throw new InvalidOperationException("You hit the throw endpoint")); + #if !ENABLE_LOGGING app.Lifetime.ApplicationStarted.Register(() => Console.WriteLine("Application started. Press Ctrl+C to shut down.")); app.Lifetime.ApplicationStopping.Register(() => Console.WriteLine("Application is shutting down...")); From 2c0c1614cc390f85ddc14f057d557a4c838db0b2 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Thu, 30 Mar 2023 13:36:39 -0700 Subject: [PATCH 07/26] PR feedback --- src/BenchmarksApps/TodosApi/DataExtensions.cs | 106 ------------------ src/BenchmarksApps/TodosApi/TodosApi.csproj | 3 +- 2 files changed, 1 insertion(+), 108 deletions(-) diff --git a/src/BenchmarksApps/TodosApi/DataExtensions.cs b/src/BenchmarksApps/TodosApi/DataExtensions.cs index b9a09edf3..10d4ffcd0 100644 --- a/src/BenchmarksApps/TodosApi/DataExtensions.cs +++ b/src/BenchmarksApps/TodosApi/DataExtensions.cs @@ -4,22 +4,6 @@ namespace Npgsql; internal static class DataExtensions { - public static async ValueTask OpenIfClosedAsync(this NpgsqlConnection connection) - { - if (connection.State == ConnectionState.Closed) - { - await connection.OpenAsync(); - } - } - - public static async Task ExecuteAsync(this NpgsqlConnection connection, string commandText) - { - await using var cmd = connection.CreateCommand(commandText); - - await connection.OpenIfClosedAsync(); - return await cmd.ExecuteNonQueryAsync(); - } - public static async Task ExecuteAsync(this NpgsqlDataSource dataSource, string commandText) { await using var cmd = dataSource.CreateCommand(commandText); @@ -34,14 +18,6 @@ public static async Task ExecuteAsync(this NpgsqlDataSource dataSource, str return await cmd.ExecuteScalarAsync(CancellationToken.None); } - public static async Task ExecuteAsync(this NpgsqlConnection connection, string commandText, params NpgsqlParameter[] parameters) - { - await using var cmd = connection.CreateCommand(commandText, parameters); - - await connection.OpenIfClosedAsync(); - return await cmd.ExecuteNonQueryAsync(); - } - public static async Task ExecuteAsync(this NpgsqlDataSource dataSource, string commandText, params NpgsqlParameter[] parameters) { await using var cmd = dataSource.CreateCommand(commandText, parameters); @@ -49,14 +25,6 @@ public static async Task ExecuteAsync(this NpgsqlDataSource dataSource, str return await cmd.ExecuteNonQueryAsync(); } - public static async Task ExecuteAsync(this NpgsqlConnection connection, string commandText, Action configureParameters) - { - await using var cmd = connection.CreateCommand(commandText, configureParameters); - - await connection.OpenIfClosedAsync(); - return await cmd.ExecuteNonQueryAsync(); - } - public static async Task ExecuteAsync(this NpgsqlDataSource dataSource, string commandText, Action configureParameters) { await using var cmd = dataSource.CreateCommand(commandText, configureParameters); @@ -64,15 +32,6 @@ public static async Task ExecuteAsync(this NpgsqlDataSource dataSource, str return await cmd.ExecuteNonQueryAsync(); } - public static async Task QuerySingleAsync(this NpgsqlConnection connection, string commandText, params NpgsqlParameter[] parameters) - where T : IDataReaderMapper - { - await connection.OpenIfClosedAsync(); - await using var reader = await connection.QuerySingleAsync(commandText, parameters); - - return await reader.MapSingleAsync(); - } - public static async Task QuerySingleAsync(this NpgsqlDataSource dataSource, string commandText, params NpgsqlParameter[] parameters) where T : IDataReaderMapper { @@ -81,16 +40,6 @@ public static async Task ExecuteAsync(this NpgsqlDataSource dataSource, str return await reader.MapSingleAsync(); } - public static async Task QuerySingleAsync(this NpgsqlConnection connection, string commandText, Action? configureParameters = null) - where T : IDataReaderMapper - { - await using var cmd = connection.CreateCommand(commandText, configureParameters); - - await using var reader = await connection.QuerySingleAsync(cmd); - - return await reader.MapSingleAsync(); - } - public static async Task QuerySingleAsync(this NpgsqlDataSource dataSource, string commandText, Action? configureParameters = null) where T : IDataReaderMapper { @@ -101,17 +50,6 @@ public static async Task ExecuteAsync(this NpgsqlDataSource dataSource, str return await reader.MapSingleAsync(); } - public static async IAsyncEnumerable QueryAsync(this NpgsqlConnection connection, string commandText, params NpgsqlParameter[] parameters) - where T : IDataReaderMapper - { - var query = connection.QueryAsync(commandText, parameterCollection => parameterCollection.AddRange(parameters)); - - await foreach (var item in query) - { - yield return item; - } - } - public static async IAsyncEnumerable QueryAsync(this NpgsqlDataSource dataSource, string commandText, params NpgsqlParameter[] parameters) where T : IDataReaderMapper { @@ -123,21 +61,6 @@ public static async IAsyncEnumerable QueryAsync(this NpgsqlDataSource data } } - public static async IAsyncEnumerable QueryAsync(this NpgsqlConnection connection, string commandText, Action? configureParameters = null) - where T : IDataReaderMapper - { - await using var cmd = connection.CreateCommand(commandText, configureParameters); - - await connection.OpenIfClosedAsync(); - - await using var reader = await connection.QueryAsync(cmd); - - await foreach (var item in MapAsync(reader)) - { - yield return item; - } - } - public static async IAsyncEnumerable QueryAsync(this NpgsqlDataSource dataSource, string commandText, Action? configureParameters = null) where T : IDataReaderMapper { @@ -185,26 +108,12 @@ public static async IAsyncEnumerable MapAsync(this NpgsqlDataReader reader } } - public static Task QuerySingleAsync(this NpgsqlConnection connection, string commandText, params NpgsqlParameter[] parameters) - => QueryAsync(connection, commandText, CommandBehavior.SingleResult | CommandBehavior.SingleRow, parameters); - public static Task QuerySingleAsync(this NpgsqlDataSource dataSource, string commandText, params NpgsqlParameter[] parameters) => QueryAsync(dataSource, commandText, CommandBehavior.SingleResult | CommandBehavior.SingleRow, parameters); - public static Task QuerySingleAsync(this NpgsqlConnection connection, NpgsqlCommand command) - => QueryAsync(connection, command, CommandBehavior.SingleResult | CommandBehavior.SingleRow); - public static Task QuerySingleAsync(this NpgsqlCommand command) => QueryAsync(command, CommandBehavior.SingleResult | CommandBehavior.SingleRow); - public static async Task QueryAsync(this NpgsqlConnection connection, string commandText, CommandBehavior commandBehavior, params NpgsqlParameter[] parameters) - { - await using var cmd = connection.CreateCommand(commandText, parameters); - - await connection.OpenIfClosedAsync(); - return await cmd.ExecuteReaderAsync(commandBehavior); - } - public static async Task QueryAsync(this NpgsqlDataSource dataSource, string commandText, CommandBehavior commandBehavior, params NpgsqlParameter[] parameters) { await using var cmd = dataSource.CreateCommand(commandText, parameters); @@ -212,18 +121,9 @@ public static async Task QueryAsync(this NpgsqlDataSource data return await cmd.ExecuteReaderAsync(commandBehavior); } - public static Task QueryAsync(this NpgsqlConnection connection, NpgsqlCommand command) - => QueryAsync(connection, command, CommandBehavior.Default); - public static Task QueryAsync(this NpgsqlCommand command) => QueryAsync(command, CommandBehavior.Default); - public static async Task QueryAsync(this NpgsqlConnection connection, NpgsqlCommand command, CommandBehavior commandBehavior) - { - await connection.OpenIfClosedAsync(); - return await command.ExecuteReaderAsync(commandBehavior); - } - public static async Task QueryAsync(this NpgsqlCommand command, CommandBehavior commandBehavior) { return await command.ExecuteReaderAsync(commandBehavior); @@ -260,9 +160,6 @@ public static NpgsqlParameter AsTypedDbParameter(this T value) return parameter; } - private static NpgsqlCommand CreateCommand(this NpgsqlConnection connection, string commandText, params NpgsqlParameter[] parameters) => - ConfigureCommand(connection.CreateCommand(commandText), parameters); - private static NpgsqlCommand CreateCommand(this NpgsqlDataSource dataSource, string commandText, params NpgsqlParameter[] parameters) => ConfigureCommand(dataSource.CreateCommand(commandText), parameters); @@ -275,9 +172,6 @@ private static NpgsqlCommand ConfigureCommand(NpgsqlCommand cmd, NpgsqlParameter } }); - private static NpgsqlCommand CreateCommand(this NpgsqlConnection connection, string commandText, Action? configureParameters = null) => - ConfigureCommand(connection.CreateCommand(commandText), configureParameters); - private static NpgsqlCommand CreateCommand(this NpgsqlDataSource dataSource, string commandText, Action? configureParameters = null) => ConfigureCommand(dataSource.CreateCommand(commandText), configureParameters); diff --git a/src/BenchmarksApps/TodosApi/TodosApi.csproj b/src/BenchmarksApps/TodosApi/TodosApi.csproj index f456a36bc..98831945a 100644 --- a/src/BenchmarksApps/TodosApi/TodosApi.csproj +++ b/src/BenchmarksApps/TodosApi/TodosApi.csproj @@ -7,8 +7,7 @@ false true b8ffb8d3-b768-460b-ac1f-ef267c954c85 - true - true + true true $(DefineConstants);ENABLE_LOGGING From 53096813e8aa250e11b50cbdbb88471222a237ca Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Thu, 30 Mar 2023 13:39:43 -0700 Subject: [PATCH 08/26] Ensure file-based config is loaded all the time (needed for JWT) --- src/BenchmarksApps/TodosApi/Program.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/BenchmarksApps/TodosApi/Program.cs b/src/BenchmarksApps/TodosApi/Program.cs index 1b3489f16..6be29e39e 100644 --- a/src/BenchmarksApps/TodosApi/Program.cs +++ b/src/BenchmarksApps/TodosApi/Program.cs @@ -4,15 +4,18 @@ var builder = WebApplication.CreateSlimBuilder(args); -#if ENABLE_LOGGING // Load custom configuration var settingsFiles = new[] { "appsettings.json", $"appsettings.{builder.Environment.EnvironmentName}.json" }; foreach (var settingsFile in settingsFiles) { builder.Configuration.AddJsonFile(builder.Environment.ContentRootFileProvider, settingsFile, optional: true, reloadOnChange: true); } + +#if DEBUG || DEBUG_DATABASE builder.Configuration.AddUserSecrets(); +#endif +#if ENABLE_LOGGING // Configure logging builder.Logging .AddConfiguration(builder.Configuration.GetSection("Logging")) From 8931a01ba2100894b0d7c37bd4f4e6130ffc2387 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Thu, 30 Mar 2023 14:32:33 -0700 Subject: [PATCH 09/26] PR feedback & cleanup restore warnings --- build/dependencies.props | 16 +++++++++------- src/BenchmarksApps.sln | 5 +---- src/BenchmarksApps/Directory.Build.props | 6 ------ .../DistributedCache/DistributedCache.csproj | 7 +++++-- src/BenchmarksApps/Mvc/Mvc.csproj | 4 ++++ .../SignalR/BenchmarkServer.csproj | 4 ++++ .../StaticFiles/StaticFiles.csproj | 2 +- src/BenchmarksApps/TodosApi/Program.cs | 1 - src/BenchmarksApps/TodosApi/TodosApi.csproj | 2 +- .../TodosApi/appsettings.Development.json | 10 ---------- src/BenchmarksApps/TodosApi/appsettings.json | 3 ++- src/Directory.Build.props | 4 ++++ 12 files changed, 31 insertions(+), 33 deletions(-) delete mode 100644 src/BenchmarksApps/Directory.Build.props diff --git a/build/dependencies.props b/build/dependencies.props index bb945fe90..82ac74996 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -66,24 +66,26 @@ 5.0.17 6.0.14 8.0.0-preview.1.23111.4 + + 8.0.0-preview.3.23177.8 - + 2.1.3 - + 2.2.0 - + 3.0.0 - + 3.1.0 - + 6.0.14 - - 8.0.0-preview.1.23112.2 + + $(MicrosoftAspNetCoreAppPackageVersion80) diff --git a/src/BenchmarksApps.sln b/src/BenchmarksApps.sln index 37c4bcbd4..d3cb0961f 100644 --- a/src/BenchmarksApps.sln +++ b/src/BenchmarksApps.sln @@ -34,9 +34,6 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlatformBenchmarks", "BenchmarksApps\TechEmpower\PlatformBenchmarks\PlatformBenchmarks.csproj", "{ACA43671-AD28-4F72-AAAB-6C32B388C2F0}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{689C58F6-8DF0-4565-887F-C9A9BDA757D8}" - ProjectSection(SolutionItems) = preProject - BenchmarksApps\Directory.Build.props = BenchmarksApps\Directory.Build.props - EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BuildPerformance", "BenchmarksApps\BuildPerformance\BuildPerformance.csproj", "{2E953AFB-4900-4B5D-9E78-819E950CD365}" EndProject @@ -52,7 +49,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TcpEcho", "BenchmarksApps\T EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorUnited", "BenchmarksApps\TechEmpower\BlazorUnited\BlazorUnited.csproj", "{FE3606FF-CBC9-421A-A0B5-836E312E7719}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TodosApi", "BenchmarksApps\TodosApi\TodosApi.csproj", "{8E1A1F61-43E4-4629-A25B-7E5FA82697D0}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodosApi", "BenchmarksApps\TodosApi\TodosApi.csproj", "{8E1A1F61-43E4-4629-A25B-7E5FA82697D0}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/src/BenchmarksApps/Directory.Build.props b/src/BenchmarksApps/Directory.Build.props deleted file mode 100644 index 4ed01906f..000000000 --- a/src/BenchmarksApps/Directory.Build.props +++ /dev/null @@ -1,6 +0,0 @@ - - - Debug;Release;Debug_Database;Release_Database - true - - diff --git a/src/BenchmarksApps/DistributedCache/DistributedCache.csproj b/src/BenchmarksApps/DistributedCache/DistributedCache.csproj index 75e95981e..f8bd62c83 100644 --- a/src/BenchmarksApps/DistributedCache/DistributedCache.csproj +++ b/src/BenchmarksApps/DistributedCache/DistributedCache.csproj @@ -1,10 +1,13 @@ - net7.0 + net7.0 Exe enable enable - $(TargetFramework.Substring(3,3)).* + + + $(TargetFramework.Substring(3,3)).* + $(TargetFramework.Substring(3,3)).* diff --git a/src/BenchmarksApps/Mvc/Mvc.csproj b/src/BenchmarksApps/Mvc/Mvc.csproj index 435996f36..aae1589d7 100644 --- a/src/BenchmarksApps/Mvc/Mvc.csproj +++ b/src/BenchmarksApps/Mvc/Mvc.csproj @@ -2,6 +2,10 @@ net7.0 + + + $(TargetFramework.Substring(3,3)).* + $(TargetFramework.Substring(3,3)).* diff --git a/src/BenchmarksApps/SignalR/BenchmarkServer.csproj b/src/BenchmarksApps/SignalR/BenchmarkServer.csproj index 60beb1ef5..dfd3436d4 100644 --- a/src/BenchmarksApps/SignalR/BenchmarkServer.csproj +++ b/src/BenchmarksApps/SignalR/BenchmarkServer.csproj @@ -2,6 +2,10 @@ net7.0 + + + $(TargetFramework.Substring(3,3)).* + $(TargetFramework.Substring(3,3)).* diff --git a/src/BenchmarksApps/StaticFiles/StaticFiles.csproj b/src/BenchmarksApps/StaticFiles/StaticFiles.csproj index 3e645b221..a7163ba1a 100644 --- a/src/BenchmarksApps/StaticFiles/StaticFiles.csproj +++ b/src/BenchmarksApps/StaticFiles/StaticFiles.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1;net5.0 + net6.0;net7.0;net8.0 diff --git a/src/BenchmarksApps/TodosApi/Program.cs b/src/BenchmarksApps/TodosApi/Program.cs index 6be29e39e..27a761a02 100644 --- a/src/BenchmarksApps/TodosApi/Program.cs +++ b/src/BenchmarksApps/TodosApi/Program.cs @@ -30,7 +30,6 @@ // Configure data access var connectionString = builder.Configuration.GetConnectionString("TodoDb") - ?? builder.Configuration["CONNECTION_STRING"] ?? throw new InvalidOperationException(""" Connection string not found. If running locally, set the connection string in user secrets for key 'ConnectionStrings:TodoDb'. diff --git a/src/BenchmarksApps/TodosApi/TodosApi.csproj b/src/BenchmarksApps/TodosApi/TodosApi.csproj index 98831945a..92e12aa37 100644 --- a/src/BenchmarksApps/TodosApi/TodosApi.csproj +++ b/src/BenchmarksApps/TodosApi/TodosApi.csproj @@ -13,7 +13,7 @@ - + diff --git a/src/BenchmarksApps/TodosApi/appsettings.Development.json b/src/BenchmarksApps/TodosApi/appsettings.Development.json index b52a21dc2..dc89e4ec9 100644 --- a/src/BenchmarksApps/TodosApi/appsettings.Development.json +++ b/src/BenchmarksApps/TodosApi/appsettings.Development.json @@ -4,15 +4,5 @@ "Default": "Debug", "Microsoft.AspNetCore": "Debug" } - }, - "Authentication": { - "Schemes": { - "Bearer": { - "ValidAudiences": [ - "http://localhost:5054" - ], - "ValidIssuer": "dotnet-user-jwts" - } - } } } \ No newline at end of file diff --git a/src/BenchmarksApps/TodosApi/appsettings.json b/src/BenchmarksApps/TodosApi/appsettings.json index 4e5652984..6b6ae8970 100644 --- a/src/BenchmarksApps/TodosApi/appsettings.json +++ b/src/BenchmarksApps/TodosApi/appsettings.json @@ -10,7 +10,8 @@ "Schemes": { "Bearer": { "ValidAudiences": [ - "http://localhost:5054" + "http://localhost:5054", + "http://localhost:5000" ], "ValidIssuer": "dotnet-user-jwts" } diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 346650d63..4788ae7c5 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -12,6 +12,10 @@ false + + + Debug;Release;Debug_Database;Release_Database + true From 519d27ef428604642e6ddb6ace98d248ea7cc738 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Thu, 30 Mar 2023 14:52:38 -0700 Subject: [PATCH 10/26] Update DB health check query & disable PublishAot for now --- src/BenchmarksApps/TodosApi/DatabaseHealthCheck.cs | 4 ++-- src/BenchmarksApps/TodosApi/TodosApi.csproj | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/BenchmarksApps/TodosApi/DatabaseHealthCheck.cs b/src/BenchmarksApps/TodosApi/DatabaseHealthCheck.cs index 89bfcd3f4..f2443268c 100644 --- a/src/BenchmarksApps/TodosApi/DatabaseHealthCheck.cs +++ b/src/BenchmarksApps/TodosApi/DatabaseHealthCheck.cs @@ -14,10 +14,10 @@ public DatabaseHealthCheck(NpgsqlDataSource dataSource) public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { - var result = await _dataSource.ExecuteScalarAsync("SELECT COUNT(id) FROM public.todos"); + var result = await _dataSource.ExecuteScalarAsync("SELECT id FROM public.todos LIMIT 1"); return result switch { - long count when count >= 0 => HealthCheckResult.Healthy(), + null or int => HealthCheckResult.Healthy(), _ => HealthCheckResult.Unhealthy() }; } diff --git a/src/BenchmarksApps/TodosApi/TodosApi.csproj b/src/BenchmarksApps/TodosApi/TodosApi.csproj index 92e12aa37..649fb1d8e 100644 --- a/src/BenchmarksApps/TodosApi/TodosApi.csproj +++ b/src/BenchmarksApps/TodosApi/TodosApi.csproj @@ -7,7 +7,7 @@ false true b8ffb8d3-b768-460b-ac1f-ef267c954c85 - true + false true $(DefineConstants);ENABLE_LOGGING From 1385ee352997eee7fd795dd4f7e93f7fe475587a Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Thu, 30 Mar 2023 14:56:23 -0700 Subject: [PATCH 11/26] Update Program.cs --- src/BenchmarksApps/TodosApi/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BenchmarksApps/TodosApi/Program.cs b/src/BenchmarksApps/TodosApi/Program.cs index 27a761a02..964a252f1 100644 --- a/src/BenchmarksApps/TodosApi/Program.cs +++ b/src/BenchmarksApps/TodosApi/Program.cs @@ -65,7 +65,7 @@ Connection string not found. app.MapTodoApi(); // Enables testing request exception handling behavior -app.MapGet("/throw", (HttpContext httpContext) => throw new InvalidOperationException("You hit the throw endpoint")); +app.MapGet("/throw", (HttpContext _) => throw new InvalidOperationException("You hit the throw endpoint")); #if !ENABLE_LOGGING app.Lifetime.ApplicationStarted.Register(() => Console.WriteLine("Application started. Press Ctrl+C to shut down.")); From 4b3a8344a1c5da82c0cc46b2ca0d7df137f3e393 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Thu, 30 Mar 2023 14:57:07 -0700 Subject: [PATCH 12/26] Update Program.cs --- src/BenchmarksApps/TodosApi/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BenchmarksApps/TodosApi/Program.cs b/src/BenchmarksApps/TodosApi/Program.cs index 964a252f1..783694ff1 100644 --- a/src/BenchmarksApps/TodosApi/Program.cs +++ b/src/BenchmarksApps/TodosApi/Program.cs @@ -65,7 +65,7 @@ Connection string not found. app.MapTodoApi(); // Enables testing request exception handling behavior -app.MapGet("/throw", (HttpContext _) => throw new InvalidOperationException("You hit the throw endpoint")); +app.MapGet("/throw", Task () => throw new InvalidOperationException("You hit the throw endpoint")); #if !ENABLE_LOGGING app.Lifetime.ApplicationStarted.Register(() => Console.WriteLine("Application started. Press Ctrl+C to shut down.")); From c024b148ec6d5782eb5a02e7658c9b7f05b02f5b Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Thu, 30 Mar 2023 14:59:28 -0700 Subject: [PATCH 13/26] Update Program.cs --- src/BenchmarksApps/TodosApi/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BenchmarksApps/TodosApi/Program.cs b/src/BenchmarksApps/TodosApi/Program.cs index 783694ff1..eb974c67d 100644 --- a/src/BenchmarksApps/TodosApi/Program.cs +++ b/src/BenchmarksApps/TodosApi/Program.cs @@ -65,7 +65,7 @@ Connection string not found. app.MapTodoApi(); // Enables testing request exception handling behavior -app.MapGet("/throw", Task () => throw new InvalidOperationException("You hit the throw endpoint")); +app.MapGet("/throw", void () => throw new InvalidOperationException("You hit the throw endpoint")); #if !ENABLE_LOGGING app.Lifetime.ApplicationStarted.Register(() => Console.WriteLine("Application started. Press Ctrl+C to shut down.")); From 8ef37cfd05b75e38420327e1f03eb8584e05a8a8 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Thu, 30 Mar 2023 15:14:14 -0700 Subject: [PATCH 14/26] Add stage 2 scenarios to yml files --- build/nativeaot-scenarios.yml | 26 +++++++++++++++ scenarios/goldilocks.benchmarks.yml | 51 +++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/build/nativeaot-scenarios.yml b/build/nativeaot-scenarios.yml index 651ecbd35..9efd03fbc 100644 --- a/build/nativeaot-scenarios.yml +++ b/build/nativeaot-scenarios.yml @@ -40,6 +40,32 @@ parameters: arguments: --scenario basicminimalapipublishaot $(goldilocksJobs) --property scenario=Stage1AotServerGC --property publish=nativeaot --application.packageReferences \"Microsoft.Dotnet.ILCompiler=$(MicrosoftNETCoreAppPackageVersion)\" --application.buildArguments \"/p:ServerGarbageCollection=true\" condition: 'true' + - displayName: Goldilocks Stage 2 (CoreCLR) + arguments: --scenario todosapivanilla $(goldilocksJobs) --property scenario=Stage2 --property publish=coreclr + condition: 'true' + + - displayName: Goldilocks Stage 2 (CoreCLR - Server GC) + arguments: --scenario todosapivanilla $(goldilocksJobs) --property scenario=Stage2ServerGC --property publish=coreclr --application.buildArguments \"/p:ServerGarbageCollection=true\" + condition: 'true' + + - displayName: Goldilocks Stage 2 (CoreCLR - PGO) + arguments: --scenario todosapivanilla $(goldilocksJobs) --property scenario=Stage2Pgo --property publish=coreclr --application.environmentVariables DOTNET_TieredPGO=1 + condition: Math.round(Date.now() / 43200000) % 6 == 1 # once every 6 half-days (43200000 ms per half-day) + + - displayName: Goldilocks Stage 2 (CoreCLR - Trim R2R SingleFile) + arguments: --scenario todosapipublishtrimr2rsinglefile $(goldilocksJobs) --property scenario=Stage2TrimR2RSingleFile --property publish=coreclr + condition: 'true' + + - displayName: Goldilocks Stage 2 (NativeAOT - Workstation GC) + # workaround https://github.com/dotnet/runtime/issues/81382 by explicitly referencing a Microsoft.Dotnet.ILCompiler version + arguments: --scenario todosapipublishaot $(goldilocksJobs) --property scenario=Stage2Aot --property publish=nativeaot --application.packageReferences \"Microsoft.Dotnet.ILCompiler=$(MicrosoftNETCoreAppPackageVersion)\" + condition: 'true' + + - displayName: Goldilocks Stage 2 (NativeAOT - Server GC) + # workaround https://github.com/dotnet/runtime/issues/81382 by explicitly referencing a Microsoft.Dotnet.ILCompiler version + arguments: --scenario todosapipublishaot $(goldilocksJobs) --property scenario=Stage2AotServerGC --property publish=nativeaot --application.packageReferences \"Microsoft.Dotnet.ILCompiler=$(MicrosoftNETCoreAppPackageVersion)\" --application.buildArguments \"/p:ServerGarbageCollection=true\" + condition: 'true' + - displayName: Goldilocks gRPC Stage 1 (CoreCLR) arguments: --scenario basicgrpcvanilla $(goldilocksJobs) --property scenario=Stage1Grpc --property publish=coreclr condition: 'true' diff --git a/scenarios/goldilocks.benchmarks.yml b/scenarios/goldilocks.benchmarks.yml index a407980fe..5f8481758 100644 --- a/scenarios/goldilocks.benchmarks.yml +++ b/scenarios/goldilocks.benchmarks.yml @@ -21,6 +21,16 @@ jobs: serverScheme: http serverPort: 5000 arguments: "--urls {{serverScheme}}://{{serverAddress}}:{{serverPort}}" + todosapiaspnetbenchmarks: + source: + repository: https://github.com/aspnet/benchmarks.git + branchOrCommit: main + project: src/BenchmarksApps/TodosApi/TodosApi.csproj + readyStateText: Application started. + variables: + serverScheme: http + serverPort: 5000 + arguments: "--urls {{serverScheme}}://{{serverAddress}}:{{serverPort}}" basicgrpcaspnetbenchmarks: source: repository: https://github.com/aspnet/benchmarks.git @@ -82,6 +92,47 @@ scenarios: presetHeaders: json path: /todos + todosapipublishaot: + application: + job: todosapiaspnetbenchmarks + buildArguments: + - "/p:PublishAot=true" + - "/p:StripSymbols=true" + - "/p:EnableRequestDelegateGenerator=true" + load: + job: wrk + variables: + presetHeaders: json + path: /api/todos + + todosapipublishtrimr2rsinglefile: + application: + job: todosapiaspnetbenchmarks + buildArguments: + - "/p:PublishAot=false" + - "/p:PublishTrimmed=true" + - "/p:PublishReadyToRun=true" + - "/p:PublishSingleFile=true" + - "/p:TrimMode=full" + - "/p:EnableRequestDelegateGenerator=true" + load: + job: wrk + variables: + presetHeaders: json + path: /api/todos + + todosapivanilla: + application: + job: todosapiaspnetbenchmarks + buildArguments: + - "/p:PublishAot=false" + - "/p:EnableRequestDelegateGenerator=false" + load: + job: wrk + variables: + presetHeaders: json + path: /api/todos + basicgrpcpublishaot: application: job: basicgrpcaspnetbenchmarks From 8fbfd628a9289c039fa0162e9ef8754122e9e440 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Thu, 30 Mar 2023 15:17:11 -0700 Subject: [PATCH 15/26] Update health checks to be more descriptive --- .../TodosApi/DatabaseHealthCheck.cs | 16 +++++++++++++--- src/BenchmarksApps/TodosApi/JwtHealthCheck.cs | 2 +- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/BenchmarksApps/TodosApi/DatabaseHealthCheck.cs b/src/BenchmarksApps/TodosApi/DatabaseHealthCheck.cs index f2443268c..7733a8475 100644 --- a/src/BenchmarksApps/TodosApi/DatabaseHealthCheck.cs +++ b/src/BenchmarksApps/TodosApi/DatabaseHealthCheck.cs @@ -14,11 +14,21 @@ public DatabaseHealthCheck(NpgsqlDataSource dataSource) public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { - var result = await _dataSource.ExecuteScalarAsync("SELECT id FROM public.todos LIMIT 1"); + object? result = null; + Exception? exception = null; + try + { + result = await _dataSource.ExecuteScalarAsync("SELECT id FROM public.todos LIMIT 1"); + } + catch (Exception ex) + { + exception = ex; + } + return result switch { - null or int => HealthCheckResult.Healthy(), - _ => HealthCheckResult.Unhealthy() + null or int => HealthCheckResult.Healthy("Database health verified successfully"), + _ => HealthCheckResult.Unhealthy("Error occurred when checking database health", exception: exception) }; } } diff --git a/src/BenchmarksApps/TodosApi/JwtHealthCheck.cs b/src/BenchmarksApps/TodosApi/JwtHealthCheck.cs index 332bbd37a..8e79ec46d 100644 --- a/src/BenchmarksApps/TodosApi/JwtHealthCheck.cs +++ b/src/BenchmarksApps/TodosApi/JwtHealthCheck.cs @@ -20,7 +20,7 @@ public Task CheckHealthAsync(HealthCheckContext context, Canc { var valid = ValidateJwtOptions(_jwtOptions.CurrentValue); var status = valid - ? HealthCheckResult.Healthy("") + ? HealthCheckResult.Healthy("JWT options configured correctly") : HealthCheckResult.Degraded("JWT options are not configured. Run 'dotnet user-jwts create' in project directory to configure JWT."); return Task.FromResult(status); } From 638fc670b7fbcde4e06c49faa8440495d403bfc5 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Thu, 30 Mar 2023 15:39:32 -0700 Subject: [PATCH 16/26] Flow cancellation tokens through to database calls --- src/BenchmarksApps/TodosApi/DataExtensions.cs | 72 ++++++++++++------- src/BenchmarksApps/TodosApi/Database.cs | 4 +- .../TodosApi/DatabaseHealthCheck.cs | 2 +- src/BenchmarksApps/TodosApi/Program.cs | 3 +- src/BenchmarksApps/TodosApi/TodoApi.cs | 33 +++++---- 5 files changed, 68 insertions(+), 46 deletions(-) diff --git a/src/BenchmarksApps/TodosApi/DataExtensions.cs b/src/BenchmarksApps/TodosApi/DataExtensions.cs index 10d4ffcd0..4daade5f8 100644 --- a/src/BenchmarksApps/TodosApi/DataExtensions.cs +++ b/src/BenchmarksApps/TodosApi/DataExtensions.cs @@ -1,59 +1,71 @@ using System.Data; +using System.Runtime.CompilerServices; namespace Npgsql; internal static class DataExtensions { - public static async Task ExecuteAsync(this NpgsqlDataSource dataSource, string commandText) + public static async Task ExecuteAsync(this NpgsqlDataSource dataSource, string commandText, CancellationToken cancellationToken = default) { await using var cmd = dataSource.CreateCommand(commandText); - return await cmd.ExecuteNonQueryAsync(); + return await cmd.ExecuteNonQueryAsync(cancellationToken); } - public static async Task ExecuteScalarAsync(this NpgsqlDataSource dataSource, string commandText) + public static async Task ExecuteScalarAsync(this NpgsqlDataSource dataSource, string commandText, CancellationToken cancellationToken = default) { await using var cmd = dataSource.CreateCommand(commandText); - return await cmd.ExecuteScalarAsync(CancellationToken.None); + return await cmd.ExecuteScalarAsync(cancellationToken); } - public static async Task ExecuteAsync(this NpgsqlDataSource dataSource, string commandText, params NpgsqlParameter[] parameters) + public static Task ExecuteAsync(this NpgsqlDataSource dataSource, string commandText, params NpgsqlParameter[] parameters) + => ExecuteAsync(dataSource, commandText, default, parameters); + + public static async Task ExecuteAsync(this NpgsqlDataSource dataSource, string commandText, CancellationToken cancellationToken, params NpgsqlParameter[] parameters) { await using var cmd = dataSource.CreateCommand(commandText, parameters); - return await cmd.ExecuteNonQueryAsync(); + return await cmd.ExecuteNonQueryAsync(cancellationToken); } - public static async Task ExecuteAsync(this NpgsqlDataSource dataSource, string commandText, Action configureParameters) + public static async Task ExecuteAsync(this NpgsqlDataSource dataSource, string commandText, Action configureParameters, CancellationToken cancellationToken = default) { await using var cmd = dataSource.CreateCommand(commandText, configureParameters); - return await cmd.ExecuteNonQueryAsync(); + return await cmd.ExecuteNonQueryAsync(cancellationToken); } - public static async Task QuerySingleAsync(this NpgsqlDataSource dataSource, string commandText, params NpgsqlParameter[] parameters) + public static Task QuerySingleAsync(this NpgsqlDataSource dataSource, string commandText, params NpgsqlParameter[] parameters) + where T : IDataReaderMapper + => QuerySingleAsync(dataSource, commandText, default, parameters); + + public static async Task QuerySingleAsync(this NpgsqlDataSource dataSource, string commandText, CancellationToken cancellationToken, params NpgsqlParameter[] parameters) where T : IDataReaderMapper { - await using var reader = await dataSource.QuerySingleAsync(commandText, parameters); + await using var reader = await dataSource.QuerySingleAsync(commandText, cancellationToken, parameters); return await reader.MapSingleAsync(); } - public static async Task QuerySingleAsync(this NpgsqlDataSource dataSource, string commandText, Action? configureParameters = null) + public static async Task QuerySingleAsync(this NpgsqlDataSource dataSource, string commandText, Action? configureParameters = null, CancellationToken cancellationToken = default) where T : IDataReaderMapper { await using var cmd = dataSource.CreateCommand(commandText, configureParameters); - await using var reader = await cmd.QuerySingleAsync(); + await using var reader = await cmd.QuerySingleAsync(cancellationToken); return await reader.MapSingleAsync(); } - public static async IAsyncEnumerable QueryAsync(this NpgsqlDataSource dataSource, string commandText, params NpgsqlParameter[] parameters) + public static IAsyncEnumerable QueryAsync(this NpgsqlDataSource dataSource, string commandText, params NpgsqlParameter[] parameters) + where T : IDataReaderMapper + => QueryAsync(dataSource, commandText, default, parameters); + + public static async IAsyncEnumerable QueryAsync(this NpgsqlDataSource dataSource, string commandText, [EnumeratorCancellation] CancellationToken cancellationToken, params NpgsqlParameter[] parameters) where T : IDataReaderMapper { - var query = dataSource.QueryAsync(commandText, parameterCollection => parameterCollection.AddRange(parameters)); + var query = dataSource.QueryAsync(commandText, parameterCollection => parameterCollection.AddRange(parameters), cancellationToken); await foreach (var item in query) { @@ -61,12 +73,12 @@ public static async IAsyncEnumerable QueryAsync(this NpgsqlDataSource data } } - public static async IAsyncEnumerable QueryAsync(this NpgsqlDataSource dataSource, string commandText, Action? configureParameters = null) + public static async IAsyncEnumerable QueryAsync(this NpgsqlDataSource dataSource, string commandText, Action? configureParameters = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) where T : IDataReaderMapper { await using var cmd = dataSource.CreateCommand(commandText, configureParameters); - await using var reader = await cmd.QueryAsync(); + await using var reader = await cmd.QueryAsync(cancellationToken); await foreach (var item in MapAsync(reader)) { @@ -109,31 +121,37 @@ public static async IAsyncEnumerable MapAsync(this NpgsqlDataReader reader } public static Task QuerySingleAsync(this NpgsqlDataSource dataSource, string commandText, params NpgsqlParameter[] parameters) - => QueryAsync(dataSource, commandText, CommandBehavior.SingleResult | CommandBehavior.SingleRow, parameters); + => QuerySingleAsync(dataSource, commandText, default, parameters); + + public static Task QuerySingleAsync(this NpgsqlDataSource dataSource, string commandText, CancellationToken cancellationToken, params NpgsqlParameter[] parameters) + => QueryAsync(dataSource, commandText, CommandBehavior.SingleResult | CommandBehavior.SingleRow, cancellationToken, parameters); + + public static Task QuerySingleAsync(this NpgsqlCommand command, CancellationToken cancellationToken = default) + => QueryAsync(command, CommandBehavior.SingleResult | CommandBehavior.SingleRow, cancellationToken); - public static Task QuerySingleAsync(this NpgsqlCommand command) - => QueryAsync(command, CommandBehavior.SingleResult | CommandBehavior.SingleRow); + public static Task QueryAsync(this NpgsqlDataSource dataSource, string commandText, CommandBehavior commandBehavior, params NpgsqlParameter[] parameters) + => QueryAsync(dataSource, commandText, commandBehavior, default, parameters); - public static async Task QueryAsync(this NpgsqlDataSource dataSource, string commandText, CommandBehavior commandBehavior, params NpgsqlParameter[] parameters) + public static async Task QueryAsync(this NpgsqlDataSource dataSource, string commandText, CommandBehavior commandBehavior, CancellationToken cancellationToken, params NpgsqlParameter[] parameters) { await using var cmd = dataSource.CreateCommand(commandText, parameters); - return await cmd.ExecuteReaderAsync(commandBehavior); + return await cmd.ExecuteReaderAsync(commandBehavior, cancellationToken); } - public static Task QueryAsync(this NpgsqlCommand command) - => QueryAsync(command, CommandBehavior.Default); + public static Task QueryAsync(this NpgsqlCommand command, CancellationToken cancellationToken = default) + => QueryAsync(command, CommandBehavior.Default, cancellationToken); - public static async Task QueryAsync(this NpgsqlCommand command, CommandBehavior commandBehavior) + public static async Task QueryAsync(this NpgsqlCommand command, CommandBehavior commandBehavior, CancellationToken cancellationToken = default) { - return await command.ExecuteReaderAsync(commandBehavior); + return await command.ExecuteReaderAsync(commandBehavior, cancellationToken); } - public static async Task> ToListAsync(this IAsyncEnumerable enumerable, int? initialCapacity = null) + public static async Task> ToListAsync(this IAsyncEnumerable enumerable, int? initialCapacity = null, CancellationToken cancellationToken = default) { var list = initialCapacity.HasValue ? new List(initialCapacity.Value) : new List(); - await foreach (var item in enumerable) + await foreach (var item in enumerable.WithCancellation(cancellationToken)) { list.Add(item); } diff --git a/src/BenchmarksApps/TodosApi/Database.cs b/src/BenchmarksApps/TodosApi/Database.cs index 291e616f0..21159e19c 100644 --- a/src/BenchmarksApps/TodosApi/Database.cs +++ b/src/BenchmarksApps/TodosApi/Database.cs @@ -4,7 +4,7 @@ namespace TodosApi; internal static class Database { - public static async Task Initialize(IServiceProvider services, ILogger logger) + public static async Task Initialize(IServiceProvider services, ILogger logger, CancellationToken cancellationToken = default) { var db = services.GetRequiredService(); @@ -33,7 +33,7 @@ INSERT INTO ('Walk the kangaroo.', CURRENT_DATE + INTERVAL '1 day', false), ('Call Grandma.', CURRENT_DATE + INTERVAL '1 day', false); """; - await db.ExecuteAsync(sql); + await db.ExecuteAsync(sql, cancellationToken); } else { diff --git a/src/BenchmarksApps/TodosApi/DatabaseHealthCheck.cs b/src/BenchmarksApps/TodosApi/DatabaseHealthCheck.cs index 7733a8475..0d2cbdcb0 100644 --- a/src/BenchmarksApps/TodosApi/DatabaseHealthCheck.cs +++ b/src/BenchmarksApps/TodosApi/DatabaseHealthCheck.cs @@ -18,7 +18,7 @@ public async Task CheckHealthAsync(HealthCheckContext context Exception? exception = null; try { - result = await _dataSource.ExecuteScalarAsync("SELECT id FROM public.todos LIMIT 1"); + result = await _dataSource.ExecuteScalarAsync("SELECT id FROM public.todos LIMIT 1", cancellationToken); } catch (Exception ex) { diff --git a/src/BenchmarksApps/TodosApi/Program.cs b/src/BenchmarksApps/TodosApi/Program.cs index eb974c67d..9f5fff490 100644 --- a/src/BenchmarksApps/TodosApi/Program.cs +++ b/src/BenchmarksApps/TodosApi/Program.cs @@ -1,3 +1,4 @@ +using System.Threading; using Microsoft.Extensions.Logging.Configuration; using Npgsql; using TodosApi; @@ -45,7 +46,7 @@ Connection string not found. // Configure health checks builder.Services.AddHealthChecks() - .AddCheck("Database") + .AddCheck("Database", timeout: TimeSpan.FromSeconds(2)) .AddCheck("JwtAuthentication"); // Problem details diff --git a/src/BenchmarksApps/TodosApi/TodoApi.cs b/src/BenchmarksApps/TodosApi/TodoApi.cs index 4d9cfb51f..5e0466ca4 100644 --- a/src/BenchmarksApps/TodosApi/TodoApi.cs +++ b/src/BenchmarksApps/TodosApi/TodoApi.cs @@ -11,26 +11,27 @@ public static RouteGroupBuilder MapTodoApi(this IEndpointRouteBuilder routes) { var group = routes.MapGroup("/api/todos"); - group.MapGet("/", (NpgsqlDataSource db) => db.QueryAsync("SELECT * FROM Todos")) + group.MapGet("/", (NpgsqlDataSource db, CancellationToken ct) => db.QueryAsync("SELECT * FROM Todos", ct)) .WithName("GetAllTodos"); - group.MapGet("/complete", (NpgsqlDataSource db) => db.QueryAsync("SELECT * FROM Todos WHERE IsComplete = true")) + group.MapGet("/complete", (NpgsqlDataSource db, CancellationToken ct) => db.QueryAsync("SELECT * FROM Todos WHERE IsComplete = true", ct)) .WithName("GetCompleteTodos"); - group.MapGet("/incomplete", (NpgsqlDataSource db) => db.QueryAsync("SELECT * FROM Todos WHERE IsComplete = false")) + group.MapGet("/incomplete", (NpgsqlDataSource db, CancellationToken ct) => db.QueryAsync("SELECT * FROM Todos WHERE IsComplete = false", ct)) .WithName("GetIncompleteTodos"); - group.MapGet("/{id:int}", async Task, NotFound>> (int id, NpgsqlDataSource db) => + group.MapGet("/{id:int}", async Task, NotFound>> (int id, NpgsqlDataSource db, CancellationToken ct) => await db.QuerySingleAsync( - "SELECT * FROM Todos WHERE Id = $1", id.AsTypedDbParameter()) + "SELECT * FROM Todos WHERE Id = $1", ct, id.AsTypedDbParameter()) is Todo todo ? TypedResults.Ok(todo) : TypedResults.NotFound()) .WithName("GetTodoById"); - group.MapGet("/find", async Task, NotFound>> (string title, bool? isComplete, NpgsqlDataSource db) => + group.MapGet("/find", async Task, NotFound>> (string title, bool? isComplete, NpgsqlDataSource db, CancellationToken ct) => await db.QuerySingleAsync( "SELECT * FROM Todos WHERE LOWER(Title) = LOWER($1) AND ($2 is NULL OR IsComplete = $2)", + ct, title.AsTypedDbParameter(), isComplete.AsTypedDbParameter()) is Todo todo @@ -38,10 +39,11 @@ is Todo todo : TypedResults.NotFound()) .WithName("FindTodo"); - group.MapPost("/", async Task, ValidationProblem>> (Todo todo, NpgsqlDataSource db) => + group.MapPost("/", async Task, ValidationProblem>> (Todo todo, NpgsqlDataSource db, CancellationToken ct) => { var createdTodo = await db.QuerySingleAsync( "INSERT INTO Todos(Title, IsComplete) Values($1, $2) RETURNING *", + ct, todo.Title.AsTypedDbParameter(), todo.IsComplete.AsTypedDbParameter()); @@ -49,12 +51,13 @@ is Todo todo }) .WithName("CreateTodo"); - group.MapPut("/{id}", async Task> (int id, Todo inputTodo, NpgsqlDataSource db) => + group.MapPut("/{id}", async Task> (int id, Todo inputTodo, NpgsqlDataSource db, CancellationToken ct) => { inputTodo.Id = id; return await db.ExecuteAsync( "UPDATE Todos SET Title = $1, IsComplete = $2 WHERE Id = $3", + ct, inputTodo.Title.AsTypedDbParameter(), inputTodo.IsComplete.AsTypedDbParameter(), id.AsTypedDbParameter()) == 1 @@ -63,28 +66,28 @@ is Todo todo }) .WithName("UpdateTodo"); - group.MapPut("/{id}/mark-complete", async Task> (int id, NpgsqlDataSource db) => + group.MapPut("/{id}/mark-complete", async Task> (int id, NpgsqlDataSource db, CancellationToken ct) => await db.ExecuteAsync( - "UPDATE Todos SET IsComplete = true WHERE Id = $1", id.AsTypedDbParameter()) == 1 + "UPDATE Todos SET IsComplete = true WHERE Id = $1", ct, id.AsTypedDbParameter()) == 1 ? TypedResults.NoContent() : TypedResults.NotFound()) .WithName("MarkComplete"); - group.MapPut("/{id}/mark-incomplete", async Task> (int id, NpgsqlDataSource db) => + group.MapPut("/{id}/mark-incomplete", async Task> (int id, NpgsqlDataSource db, CancellationToken ct) => await db.ExecuteAsync( - "UPDATE Todos SET IsComplete = false WHERE Id = $1", id.AsTypedDbParameter()) == 1 + "UPDATE Todos SET IsComplete = false WHERE Id = $1", ct, id.AsTypedDbParameter()) == 1 ? TypedResults.NoContent() : TypedResults.NotFound()) .WithName("MarkIncomplete"); - group.MapDelete("/{id}", async Task> (int id, NpgsqlDataSource db) => + group.MapDelete("/{id}", async Task> (int id, NpgsqlDataSource db, CancellationToken ct) => await db.ExecuteAsync( - "DELETE FROM Todos WHERE Id = $1", id.AsTypedDbParameter()) == 1 + "DELETE FROM Todos WHERE Id = $1", ct, id.AsTypedDbParameter()) == 1 ? TypedResults.NoContent() : TypedResults.NotFound()) .WithName("DeleteTodo"); - group.MapDelete("/delete-all", async (NpgsqlDataSource db) => TypedResults.Ok(await db.ExecuteAsync("DELETE FROM Todos"))) + group.MapDelete("/delete-all", async (NpgsqlDataSource db, CancellationToken ct) => TypedResults.Ok(await db.ExecuteAsync("DELETE FROM Todos", ct))) .WithName("DeleteAll") .RequireAuthorization(policy => policy.RequireAuthenticatedUser().RequireRole("admin")); From 9a6001668514fcfc783b05667336cedcd4311753 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Thu, 30 Mar 2023 16:15:47 -0700 Subject: [PATCH 17/26] Don't fail for bad JWT configuration & manually add authn/z middleware --- src/BenchmarksApps/TodosApi/JwtConfiguration.cs | 10 ++++------ src/BenchmarksApps/TodosApi/JwtHealthCheck.cs | 2 +- src/BenchmarksApps/TodosApi/Program.cs | 9 ++++++--- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/BenchmarksApps/TodosApi/JwtConfiguration.cs b/src/BenchmarksApps/TodosApi/JwtConfiguration.cs index ad8cdc160..033dfa050 100644 --- a/src/BenchmarksApps/TodosApi/JwtConfiguration.cs +++ b/src/BenchmarksApps/TodosApi/JwtConfiguration.cs @@ -22,14 +22,12 @@ public static Action ConfigureJwtBearer(WebApplicationBuilder // When not running in development configure the JWT signing key from environment variable var jwtKeyMaterialValue = builder.Configuration["JWT_SIGNING_KEY"]; - if (string.IsNullOrEmpty(jwtKeyMaterialValue)) + if (!string.IsNullOrEmpty(jwtKeyMaterialValue)) { - throw new InvalidOperationException("JWT signing key not found!"); + var jwtKeyMaterial = Convert.FromBase64String(jwtKeyMaterialValue); + var jwtSigningKey = new SymmetricSecurityKey(jwtKeyMaterial); + options.TokenValidationParameters.IssuerSigningKey = jwtSigningKey; } - - var jwtKeyMaterial = Convert.FromBase64String(jwtKeyMaterialValue); - var jwtSigningKey = new SymmetricSecurityKey(jwtKeyMaterial); - options.TokenValidationParameters.IssuerSigningKey = jwtSigningKey; } }; } diff --git a/src/BenchmarksApps/TodosApi/JwtHealthCheck.cs b/src/BenchmarksApps/TodosApi/JwtHealthCheck.cs index 8e79ec46d..d13e8e324 100644 --- a/src/BenchmarksApps/TodosApi/JwtHealthCheck.cs +++ b/src/BenchmarksApps/TodosApi/JwtHealthCheck.cs @@ -21,7 +21,7 @@ public Task CheckHealthAsync(HealthCheckContext context, Canc var valid = ValidateJwtOptions(_jwtOptions.CurrentValue); var status = valid ? HealthCheckResult.Healthy("JWT options configured correctly") - : HealthCheckResult.Degraded("JWT options are not configured. Run 'dotnet user-jwts create' in project directory to configure JWT."); + : HealthCheckResult.Degraded("JWT options are not configured. Verify the JWT signing key is correctly configured for the current environment."); return Task.FromResult(status); } diff --git a/src/BenchmarksApps/TodosApi/Program.cs b/src/BenchmarksApps/TodosApi/Program.cs index 9f5fff490..c6fc105cd 100644 --- a/src/BenchmarksApps/TodosApi/Program.cs +++ b/src/BenchmarksApps/TodosApi/Program.cs @@ -62,12 +62,15 @@ Connection string not found. } app.MapHealthChecks("/health"); - -app.MapTodoApi(); - // Enables testing request exception handling behavior app.MapGet("/throw", void () => throw new InvalidOperationException("You hit the throw endpoint")); +// These need to manually registered until https://github.com/dotnet/aspnetcore/issues/47507 is fixed +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapTodoApi(); + #if !ENABLE_LOGGING app.Lifetime.ApplicationStarted.Register(() => Console.WriteLine("Application started. Press Ctrl+C to shut down.")); app.Lifetime.ApplicationStopping.Register(() => Console.WriteLine("Application is shutting down...")); From adc3c5cdac135cb5c52f231f3781de1ef9e39435 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Thu, 30 Mar 2023 16:21:21 -0700 Subject: [PATCH 18/26] Remove unused using --- src/BenchmarksApps/TodosApi/Program.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/BenchmarksApps/TodosApi/Program.cs b/src/BenchmarksApps/TodosApi/Program.cs index c6fc105cd..32cb9e343 100644 --- a/src/BenchmarksApps/TodosApi/Program.cs +++ b/src/BenchmarksApps/TodosApi/Program.cs @@ -1,4 +1,3 @@ -using System.Threading; using Microsoft.Extensions.Logging.Configuration; using Npgsql; using TodosApi; From 4b5b37f83fd1f1a31246bc75aee5cdc555f01a90 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Thu, 30 Mar 2023 17:18:34 -0700 Subject: [PATCH 19/26] Update scenarios/goldilocks.benchmarks.yml Co-authored-by: Eric Erhardt --- scenarios/goldilocks.benchmarks.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/scenarios/goldilocks.benchmarks.yml b/scenarios/goldilocks.benchmarks.yml index 5f8481758..ec15031a3 100644 --- a/scenarios/goldilocks.benchmarks.yml +++ b/scenarios/goldilocks.benchmarks.yml @@ -98,7 +98,6 @@ scenarios: buildArguments: - "/p:PublishAot=true" - "/p:StripSymbols=true" - - "/p:EnableRequestDelegateGenerator=true" load: job: wrk variables: From 6182ed4a770f663c8ba6ec1192da4f2bfa7ffde0 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Fri, 31 Mar 2023 07:01:24 -0700 Subject: [PATCH 20/26] Update src/BenchmarksApps/TodosApi/DatabaseHealthCheck.cs Co-authored-by: Nino Floris --- src/BenchmarksApps/TodosApi/DatabaseHealthCheck.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/BenchmarksApps/TodosApi/DatabaseHealthCheck.cs b/src/BenchmarksApps/TodosApi/DatabaseHealthCheck.cs index 0d2cbdcb0..cc9beab13 100644 --- a/src/BenchmarksApps/TodosApi/DatabaseHealthCheck.cs +++ b/src/BenchmarksApps/TodosApi/DatabaseHealthCheck.cs @@ -25,9 +25,9 @@ public async Task CheckHealthAsync(HealthCheckContext context exception = ex; } - return result switch + return exception switch { - null or int => HealthCheckResult.Healthy("Database health verified successfully"), + null => HealthCheckResult.Healthy("Database health verified successfully"), _ => HealthCheckResult.Unhealthy("Error occurred when checking database health", exception: exception) }; } From fc3b1b20437e6b2d1daefb158c7e03c15bafff3a Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Fri, 31 Mar 2023 07:18:25 -0700 Subject: [PATCH 21/26] PR feedback & tweaks --- src/BenchmarksApps/TodosApi/DataExtensions.cs | 7 ++----- .../TodosApi/DatabaseHealthCheck.cs | 3 +-- src/BenchmarksApps/TodosApi/JwtConfiguration.cs | 16 ---------------- src/BenchmarksApps/TodosApi/JwtHealthCheck.cs | 17 ++++++++++++++++- src/BenchmarksApps/TodosApi/TodoApi.cs | 5 ++--- 5 files changed, 21 insertions(+), 27 deletions(-) diff --git a/src/BenchmarksApps/TodosApi/DataExtensions.cs b/src/BenchmarksApps/TodosApi/DataExtensions.cs index 4daade5f8..2cfaf6115 100644 --- a/src/BenchmarksApps/TodosApi/DataExtensions.cs +++ b/src/BenchmarksApps/TodosApi/DataExtensions.cs @@ -86,7 +86,6 @@ public static async IAsyncEnumerable QueryAsync(this NpgsqlDataSource data } } - public static Task MapSingleAsync(this NpgsqlDataReader reader) where T : IDataReaderMapper => MapSingleAsync(reader, T.Map); @@ -142,10 +141,8 @@ public static async Task QueryAsync(this NpgsqlDataSource data public static Task QueryAsync(this NpgsqlCommand command, CancellationToken cancellationToken = default) => QueryAsync(command, CommandBehavior.Default, cancellationToken); - public static async Task QueryAsync(this NpgsqlCommand command, CommandBehavior commandBehavior, CancellationToken cancellationToken = default) - { - return await command.ExecuteReaderAsync(commandBehavior, cancellationToken); - } + public static Task QueryAsync(this NpgsqlCommand command, CommandBehavior commandBehavior, CancellationToken cancellationToken = default) + => command.ExecuteReaderAsync(commandBehavior, cancellationToken); public static async Task> ToListAsync(this IAsyncEnumerable enumerable, int? initialCapacity = null, CancellationToken cancellationToken = default) { diff --git a/src/BenchmarksApps/TodosApi/DatabaseHealthCheck.cs b/src/BenchmarksApps/TodosApi/DatabaseHealthCheck.cs index cc9beab13..f80bc78c4 100644 --- a/src/BenchmarksApps/TodosApi/DatabaseHealthCheck.cs +++ b/src/BenchmarksApps/TodosApi/DatabaseHealthCheck.cs @@ -14,11 +14,10 @@ public DatabaseHealthCheck(NpgsqlDataSource dataSource) public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { - object? result = null; Exception? exception = null; try { - result = await _dataSource.ExecuteScalarAsync("SELECT id FROM public.todos LIMIT 1", cancellationToken); + await _dataSource.ExecuteScalarAsync("SELECT 1", cancellationToken); } catch (Exception ex) { diff --git a/src/BenchmarksApps/TodosApi/JwtConfiguration.cs b/src/BenchmarksApps/TodosApi/JwtConfiguration.cs index 033dfa050..b3bca5a4e 100644 --- a/src/BenchmarksApps/TodosApi/JwtConfiguration.cs +++ b/src/BenchmarksApps/TodosApi/JwtConfiguration.cs @@ -32,19 +32,3 @@ public static Action ConfigureJwtBearer(WebApplicationBuilder }; } } - - -internal class JwtOptionsSummary -{ - public string? Audience { get; set; } - public string? ClaimsIssuer { get; set; } - public IEnumerable? Audiences { get; set; } - public IEnumerable? Issuers { get; set; } - public SecurityKey? IssuerSigningKey { get; set; } - public IEnumerable? IssuerSigningKeys { get; set; } -} - -[JsonSerializable(typeof(JwtOptionsSummary))] -internal partial class JwtOptionsJsonSerializerContext : JsonSerializerContext -{ -} \ No newline at end of file diff --git a/src/BenchmarksApps/TodosApi/JwtHealthCheck.cs b/src/BenchmarksApps/TodosApi/JwtHealthCheck.cs index d13e8e324..cc84b49d9 100644 --- a/src/BenchmarksApps/TodosApi/JwtHealthCheck.cs +++ b/src/BenchmarksApps/TodosApi/JwtHealthCheck.cs @@ -5,7 +5,7 @@ namespace TodosApi; -public class JwtHealthCheck : IHealthCheck +internal class JwtHealthCheck : IHealthCheck { private readonly IOptionsMonitor _jwtOptions; private readonly ILogger _logger; @@ -53,3 +53,18 @@ private bool ValidateJwtOptions(JwtBearerOptions options) return true; } } + +internal class JwtOptionsSummary +{ + public string? Audience { get; set; } + public string? ClaimsIssuer { get; set; } + public IEnumerable? Audiences { get; set; } + public IEnumerable? Issuers { get; set; } + public SecurityKey? IssuerSigningKey { get; set; } + public IEnumerable? IssuerSigningKeys { get; set; } +} + +[JsonSerializable(typeof(JwtOptionsSummary))] +internal partial class JwtOptionsJsonSerializerContext : JsonSerializerContext +{ +} diff --git a/src/BenchmarksApps/TodosApi/TodoApi.cs b/src/BenchmarksApps/TodosApi/TodoApi.cs index 5e0466ca4..7d47a6141 100644 --- a/src/BenchmarksApps/TodosApi/TodoApi.cs +++ b/src/BenchmarksApps/TodosApi/TodoApi.cs @@ -87,7 +87,8 @@ await db.ExecuteAsync( : TypedResults.NotFound()) .WithName("DeleteTodo"); - group.MapDelete("/delete-all", async (NpgsqlDataSource db, CancellationToken ct) => TypedResults.Ok(await db.ExecuteAsync("DELETE FROM Todos", ct))) + group.MapDelete("/delete-all", async (NpgsqlDataSource db, CancellationToken ct) => + TypedResults.Ok(await db.ExecuteAsync("DELETE FROM Todos", ct))) .WithName("DeleteAll") .RequireAuthorization(policy => policy.RequireAuthenticatedUser().RequireRole("admin")); @@ -95,8 +96,6 @@ await db.ExecuteAsync( } } - - [JsonSerializable(typeof(Todo))] [JsonSerializable(typeof(IAsyncEnumerable))] [JsonSerializable(typeof(IEnumerable))] From fa0d1dd1f8519be85d428ae576fc01be5f410053 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Fri, 31 Mar 2023 09:34:55 -0700 Subject: [PATCH 22/26] Add database job definitions for stage 2 aot app --- scenarios/goldilocks.benchmarks.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/scenarios/goldilocks.benchmarks.yml b/scenarios/goldilocks.benchmarks.yml index ec15031a3..1d4f53dcc 100644 --- a/scenarios/goldilocks.benchmarks.yml +++ b/scenarios/goldilocks.benchmarks.yml @@ -52,6 +52,16 @@ jobs: waitForExit: false options: requiredOperatingSystem: linux + postgresql: + source: + repository: https://github.com/TechEmpower/FrameworkBenchmarks.git + branchOrCommit: master + dockerFile: toolset/databases/postgres/postgres.dockerfile + dockerImageName: postgres_te + dockerContextDirectory: toolset/databases/postgres + readyStateText: ready to accept connections + noClean: true + scenarios: @@ -93,11 +103,15 @@ scenarios: path: /todos todosapipublishaot: + db: + job: postgresql application: job: todosapiaspnetbenchmarks buildArguments: - "/p:PublishAot=true" - "/p:StripSymbols=true" + environmentVariables: + 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: @@ -105,6 +119,8 @@ scenarios: path: /api/todos todosapipublishtrimr2rsinglefile: + db: + job: postgresql application: job: todosapiaspnetbenchmarks buildArguments: @@ -114,6 +130,8 @@ scenarios: - "/p:PublishSingleFile=true" - "/p:TrimMode=full" - "/p:EnableRequestDelegateGenerator=true" + environmentVariables: + 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: @@ -121,11 +139,15 @@ scenarios: path: /api/todos todosapivanilla: + db: + job: postgresql application: job: todosapiaspnetbenchmarks buildArguments: - "/p:PublishAot=false" - "/p:EnableRequestDelegateGenerator=false" + environmentVariables: + 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: From 4ab5b49d560613faf9400b03ace664ca2b96fbb0 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Fri, 31 Mar 2023 09:40:49 -0700 Subject: [PATCH 23/26] Update goldilocks.benchmarks.yml --- scenarios/goldilocks.benchmarks.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scenarios/goldilocks.benchmarks.yml b/scenarios/goldilocks.benchmarks.yml index 1d4f53dcc..2574b4c81 100644 --- a/scenarios/goldilocks.benchmarks.yml +++ b/scenarios/goldilocks.benchmarks.yml @@ -111,7 +111,7 @@ scenarios: - "/p:PublishAot=true" - "/p:StripSymbols=true" environmentVariables: - connectionString: Server={{databaseServer}};Database=hello_world;User Id=benchmarkdbuser;Password=benchmarkdbpass;Maximum Pool Size=256;NoResetOnClose=true;Enlist=false;Max Auto Prepare=4 + CONNECTIONSTRINGS__TODOSDB: 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: - connectionString: Server={{databaseServer}};Database=hello_world;User Id=benchmarkdbuser;Password=benchmarkdbpass;Maximum Pool Size=256;NoResetOnClose=true;Enlist=false;Max Auto Prepare=4 + CONNECTIONSTRINGS__TODOSDB: 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: - connectionString: Server={{databaseServer}};Database=hello_world;User Id=benchmarkdbuser;Password=benchmarkdbpass;Maximum Pool Size=256;NoResetOnClose=true;Enlist=false;Max Auto Prepare=4 + CONNECTIONSTRINGS__TODOSDB: 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: From 5978271a8419a370f932f5f0ce9c7404f3742cbd Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Fri, 31 Mar 2023 09:42:26 -0700 Subject: [PATCH 24/26] Fix compile issue --- src/BenchmarksApps/TodosApi/JwtConfiguration.cs | 4 +--- src/BenchmarksApps/TodosApi/JwtHealthCheck.cs | 2 ++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/BenchmarksApps/TodosApi/JwtConfiguration.cs b/src/BenchmarksApps/TodosApi/JwtConfiguration.cs index b3bca5a4e..8b6df4cd7 100644 --- a/src/BenchmarksApps/TodosApi/JwtConfiguration.cs +++ b/src/BenchmarksApps/TodosApi/JwtConfiguration.cs @@ -1,6 +1,4 @@ -using System.Text.Json; -using System.Text.Json.Serialization; -using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; namespace Microsoft.AspNetCore.Builder; diff --git a/src/BenchmarksApps/TodosApi/JwtHealthCheck.cs b/src/BenchmarksApps/TodosApi/JwtHealthCheck.cs index cc84b49d9..8582d3045 100644 --- a/src/BenchmarksApps/TodosApi/JwtHealthCheck.cs +++ b/src/BenchmarksApps/TodosApi/JwtHealthCheck.cs @@ -1,7 +1,9 @@ using System.Text.Json; +using System.Text.Json.Serialization; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; namespace TodosApi; From 089025282d6972a3272a549a2c2a8eefab395f69 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Fri, 31 Mar 2023 09:43:43 -0700 Subject: [PATCH 25/26] Update goldilocks.benchmarks.yml --- scenarios/goldilocks.benchmarks.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scenarios/goldilocks.benchmarks.yml b/scenarios/goldilocks.benchmarks.yml index 2574b4c81..55f9a16c5 100644 --- a/scenarios/goldilocks.benchmarks.yml +++ b/scenarios/goldilocks.benchmarks.yml @@ -111,7 +111,7 @@ scenarios: - "/p:PublishAot=true" - "/p:StripSymbols=true" environmentVariables: - CONNECTIONSTRINGS__TODOSDB: Server={{databaseServer}};Database=hello_world;User Id=benchmarkdbuser;Password=benchmarkdbpass;Maximum Pool Size=256;NoResetOnClose=true;Enlist=false;Max Auto Prepare=4 + CONNECTIONSTRINGS__TODODB: 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__TODOSDB: Server={{databaseServer}};Database=hello_world;User Id=benchmarkdbuser;Password=benchmarkdbpass;Maximum Pool Size=256;NoResetOnClose=true;Enlist=false;Max Auto Prepare=4 + CONNECTIONSTRINGS__TODODB: 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__TODOSDB: Server={{databaseServer}};Database=hello_world;User Id=benchmarkdbuser;Password=benchmarkdbpass;Maximum Pool Size=256;NoResetOnClose=true;Enlist=false;Max Auto Prepare=4 + CONNECTIONSTRINGS__TODODB: 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: From 4be1491395e514d1c2f5dd8321ba5db3c5145acc Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Fri, 31 Mar 2023 09:46:31 -0700 Subject: [PATCH 26/26] Don't change todos app table owner on initialization --- src/BenchmarksApps/TodosApi/Database.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/BenchmarksApps/TodosApi/Database.cs b/src/BenchmarksApps/TodosApi/Database.cs index 21159e19c..48dc1d0c1 100644 --- a/src/BenchmarksApps/TodosApi/Database.cs +++ b/src/BenchmarksApps/TodosApi/Database.cs @@ -21,8 +21,6 @@ CREATE TABLE IF NOT EXISTS public.todos {nameof(Todo.DueBy)} date NULL, {nameof(Todo.IsComplete)} boolean NOT NULL DEFAULT false ); - ALTER TABLE IF EXISTS public.todos - OWNER to "TodosApp"; DELETE FROM public.todos; INSERT INTO public.todos ({nameof(Todo.Title)}, {nameof(Todo.DueBy)}, {nameof(Todo.IsComplete)})