From ab2932dcde76f8da14659aadd2c646f529ca3386 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Mon, 2 Feb 2026 20:35:14 +0100 Subject: [PATCH 01/10] First attempt --- src/Turnierplan.AppHost/AppHost.cs | 21 +++++++++++++++++++ .../Properties/launchSettings.json | 18 ++++++++++++++++ .../Turnierplan.AppHost.csproj | 20 ++++++++++++++++++ .../appsettings.Development.json | 8 +++++++ src/Turnierplan.AppHost/appsettings.json | 9 ++++++++ src/turnierplan.NET.slnx | 1 + 6 files changed, 77 insertions(+) create mode 100644 src/Turnierplan.AppHost/AppHost.cs create mode 100644 src/Turnierplan.AppHost/Properties/launchSettings.json create mode 100644 src/Turnierplan.AppHost/Turnierplan.AppHost.csproj create mode 100644 src/Turnierplan.AppHost/appsettings.Development.json create mode 100644 src/Turnierplan.AppHost/appsettings.json diff --git a/src/Turnierplan.AppHost/AppHost.cs b/src/Turnierplan.AppHost/AppHost.cs new file mode 100644 index 00000000..61d7664a --- /dev/null +++ b/src/Turnierplan.AppHost/AppHost.cs @@ -0,0 +1,21 @@ +using Projects; + +var builder = DistributedApplication.CreateBuilder(args); + +var database = builder.AddPostgres("Postgres-Server") + .WithDataVolume() + .WithLifetime(ContainerLifetime.Persistent) + .AddDatabase("Postgres-Database"); + +builder.AddProject("Turnierplan-Backend") + .WaitFor(database) + .WithHttpHealthCheck("/health") + .WithEnvironment("Database__ConnectionString", database.Resource.ConnectionStringExpression); + +builder.AddJavaScriptApp("Turnierplan-Client", "../Turnierplan.App/Client") + .WithRunScript("start") + .WithHttpEndpoint(45001, isProxied: false) + .WithHttpHealthCheck("/index.html") + .WithNpm(installCommand: "ci"); + +builder.Build().Run(); diff --git a/src/Turnierplan.AppHost/Properties/launchSettings.json b/src/Turnierplan.AppHost/Properties/launchSettings.json new file mode 100644 index 00000000..387a5a24 --- /dev/null +++ b/src/Turnierplan.AppHost/Properties/launchSettings.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "Turnierplan.AppHost": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15285", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19065", + "ASPIRE_DASHBOARD_MCP_ENDPOINT_URL": "http://localhost:18247", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20277" + } + } + } +} diff --git a/src/Turnierplan.AppHost/Turnierplan.AppHost.csproj b/src/Turnierplan.AppHost/Turnierplan.AppHost.csproj new file mode 100644 index 00000000..98d509bd --- /dev/null +++ b/src/Turnierplan.AppHost/Turnierplan.AppHost.csproj @@ -0,0 +1,20 @@ + + + + + net10.0 + enable + enable + Exe + 523f324d-1695-4d45-a000-a79765a1468a + + + + + + + + + + + diff --git a/src/Turnierplan.AppHost/appsettings.Development.json b/src/Turnierplan.AppHost/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/src/Turnierplan.AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/Turnierplan.AppHost/appsettings.json b/src/Turnierplan.AppHost/appsettings.json new file mode 100644 index 00000000..31c092aa --- /dev/null +++ b/src/Turnierplan.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/src/turnierplan.NET.slnx b/src/turnierplan.NET.slnx index ba0e0a96..fec1bec6 100644 --- a/src/turnierplan.NET.slnx +++ b/src/turnierplan.NET.slnx @@ -15,6 +15,7 @@ + From 405aeb93332c683d4fe6b19b8b59073e15fe5102 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Mon, 2 Feb 2026 20:39:25 +0100 Subject: [PATCH 02/10] Remove now unnecessary docker compose --- src/docker-compose.yml | 15 --------------- src/turnierplan.NET.slnx | 1 - 2 files changed, 16 deletions(-) delete mode 100644 src/docker-compose.yml diff --git a/src/docker-compose.yml b/src/docker-compose.yml deleted file mode 100644 index 31727155..00000000 --- a/src/docker-compose.yml +++ /dev/null @@ -1,15 +0,0 @@ -services: - turnierplan.database: - image: postgres:17.0 - environment: - - POSTGRES_PASSWORD=P@ssw0rd - - POSTGRES_DB=turnierplan - volumes: - - turnierplan-data:/var/lib/postgresql/data - ports: - - "5432:5432" - restart: unless-stopped - -volumes: - turnierplan-data: - diff --git a/src/turnierplan.NET.slnx b/src/turnierplan.NET.slnx index fec1bec6..03a88477 100644 --- a/src/turnierplan.NET.slnx +++ b/src/turnierplan.NET.slnx @@ -1,6 +1,5 @@ - From a63aa746a705ced2708a52382905514696f8190e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Mon, 2 Feb 2026 20:41:07 +0100 Subject: [PATCH 03/10] Change some resource names + add Todo for OTEL --- src/Turnierplan.AppHost/AppHost.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Turnierplan.AppHost/AppHost.cs b/src/Turnierplan.AppHost/AppHost.cs index 61d7664a..4a5d23ad 100644 --- a/src/Turnierplan.AppHost/AppHost.cs +++ b/src/Turnierplan.AppHost/AppHost.cs @@ -2,10 +2,10 @@ var builder = DistributedApplication.CreateBuilder(args); -var database = builder.AddPostgres("Postgres-Server") +var database = builder.AddPostgres("Turnierplan-Postgres") .WithDataVolume() .WithLifetime(ContainerLifetime.Persistent) - .AddDatabase("Postgres-Database"); + .AddDatabase("Turnierplan-Database"); builder.AddProject("Turnierplan-Backend") .WaitFor(database) @@ -19,3 +19,5 @@ .WithNpm(installCommand: "ci"); builder.Build().Run(); + +// TODO: Add OpenTelemetry collection From 36a11bba62298c18a0c0e8379f9f3ca9557202ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Tue, 3 Feb 2026 07:43:06 +0100 Subject: [PATCH 04/10] Revise development environment setup in README Updated development setup instructions and clarified tool requirements. --- README.md | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 5160c5c1..524e0bf8 100644 --- a/README.md +++ b/README.md @@ -23,20 +23,21 @@ The documentation sources are located in the `docs` directory. See the [docs rea ## Development -This section describes how to set up the development environment. First, you need to install the following tools: +This section describes how to set up the development environment. First, you need to install the following tools installed on your machine: +- Docker - .NET 10.0 SDK -- node.js v24.x and npm -- your favourite IDE +- Node.js v24.x and npm +- [JetBrains Rider](https://www.jetbrains.com/rider/) (free for non-commercial use) or [Visual Studio](https://visualstudio.microsoft.com/vs/) To run the application from source, follow these steps: -1. Open the `src/Turnierplan.slnx` solution and navigate to the docker compose file located under `Solution Items`. Run the `turnierplan.database` docker compose service. This will start up the PostgreSQL database for local development. -2. Navigate to the `Turnierplan.App` project and run the `Turnierplan.App` launch configuration. This will start the backend using port `45000`. -3. Open a terminal and navigate to the `src/Turnierplan.App/Client` directory. Run `npm install` to install the node dependencies. Next, you can start the client application by typing `npm run start`. -4. Access the client application using [http://localhost:45001](http://localhost:45001) and log in using default credentials. The user name is `admin` and the password is `P@ssw0rd`. +1. Open the `src/Turnierplan.slnx` solution. +2. Run the `Turnierplan.AppHost` project. This will start the Aspire AppHost which will do the following steps: + - Download the postgres container imgage and run a local database + - Install npm dependencies and run the client app + - Run the backend `Turnierplan.App` +3. The Aspire dashboard will open up from which you can navigate to the client application ([http://localhost:45001](http://localhost:45001)). +4. Now you can log in using default credentials: The user name is `admin` and the password is `P@ssw0rd`. When running locally, the API documentation can be viewed by opening [http://localhost:45000/scalar](http://localhost:45000/scalar). - -> [!NOTE] -> The solution must be built first before the client application can be started. This is because the client application startup depends on OpenAPI files generated during the solution build. From 5b188b71aa9b44747a3f89148e130b429eb80b85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Sat, 14 Feb 2026 18:44:22 +0100 Subject: [PATCH 05/10] Update launchsettings --- .../Properties/launchSettings.json | 5 ++-- src/Turnierplan.AppHost/AppHost.cs | 27 +++++++++++++------ .../Properties/launchSettings.json | 16 +++++++++-- 3 files changed, 36 insertions(+), 12 deletions(-) diff --git a/src/Turnierplan.App/Properties/launchSettings.json b/src/Turnierplan.App/Properties/launchSettings.json index 3610cf4f..9b5e92c4 100644 --- a/src/Turnierplan.App/Properties/launchSettings.json +++ b/src/Turnierplan.App/Properties/launchSettings.json @@ -1,6 +1,7 @@ { + "$schema": "https://json.schemastore.org/launchsettings.json", "profiles": { - "Turnierplan.App": { + "postgres": { "commandName": "Project", "launchBrowser": false, "applicationUrl": "http://localhost:45000", @@ -8,7 +9,7 @@ "ASPNETCORE_ENVIRONMENT": "Development" } }, - "Turnierplan.App (in-memory)": { + "in-memory": { "commandName": "Project", "launchBrowser": false, "applicationUrl": "http://localhost:45000", diff --git a/src/Turnierplan.AppHost/AppHost.cs b/src/Turnierplan.AppHost/AppHost.cs index 4a5d23ad..72aa1117 100644 --- a/src/Turnierplan.AppHost/AppHost.cs +++ b/src/Turnierplan.AppHost/AppHost.cs @@ -1,22 +1,33 @@ +using Microsoft.Extensions.Configuration; using Projects; var builder = DistributedApplication.CreateBuilder(args); -var database = builder.AddPostgres("Turnierplan-Postgres") +var database = builder.AddPostgres("turnierplan-postgres") .WithDataVolume() .WithLifetime(ContainerLifetime.Persistent) - .AddDatabase("Turnierplan-Database"); + .AddDatabase("turnierplan-database"); -builder.AddProject("Turnierplan-Backend") +builder.AddProject("turnierplan-backend") .WaitFor(database) .WithHttpHealthCheck("/health") .WithEnvironment("Database__ConnectionString", database.Resource.ConnectionStringExpression); -builder.AddJavaScriptApp("Turnierplan-Client", "../Turnierplan.App/Client") - .WithRunScript("start") - .WithHttpEndpoint(45001, isProxied: false) - .WithHttpHealthCheck("/index.html") - .WithNpm(installCommand: "ci"); +if (builder.Configuration.GetValue("TURNIERPLAN_ASPIRE_RUN_CLIENT", defaultValue: false)) +{ + builder.AddJavaScriptApp("turnierplan-client", "../Turnierplan.App/Client") + .WithRunScript("start") + .WithHttpEndpoint(45001, isProxied: false) + .WithHttpHealthCheck("/index.html") + .WithNpm(install: true, installCommand: "ci"); +} +else +{ + // When starting locally without client app, add an external resource with the client app URL + // and health check so that the client app is still visible and accessible in the dashboard. + builder.AddExternalService("turnierplan-client", "http://localhost:45001") + .WithHttpHealthCheck("/index.html"); +} builder.Build().Run(); diff --git a/src/Turnierplan.AppHost/Properties/launchSettings.json b/src/Turnierplan.AppHost/Properties/launchSettings.json index 387a5a24..f9bc36b0 100644 --- a/src/Turnierplan.AppHost/Properties/launchSettings.json +++ b/src/Turnierplan.AppHost/Properties/launchSettings.json @@ -1,9 +1,21 @@ { "$schema": "https://json.schemastore.org/launchsettings.json", "profiles": { - "Turnierplan.AppHost": { + "with client": { + "commandName": "Project", + "launchBrowser": true, + "applicationUrl": "http://localhost:15285", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19065", + "ASPIRE_DASHBOARD_MCP_ENDPOINT_URL": "http://localhost:18247", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20277", + "TURNIERPLAN_ASPIRE_RUN_CLIENT": "true" + } + }, + "without client": { "commandName": "Project", - "dotnetRunMessages": true, "launchBrowser": true, "applicationUrl": "http://localhost:15285", "environmentVariables": { From decbea4a93d93b553948e6108212f16d688b9560 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Sat, 14 Feb 2026 19:51:14 +0100 Subject: [PATCH 06/10] Add oltp --- .../Extensions/ServiceCollectionExtensions.cs | 50 +++++++++++++++---- src/Turnierplan.App/Turnierplan.App.csproj | 2 + src/Turnierplan.AppHost/AppHost.cs | 2 - 3 files changed, 43 insertions(+), 11 deletions(-) diff --git a/src/Turnierplan.App/Extensions/ServiceCollectionExtensions.cs b/src/Turnierplan.App/Extensions/ServiceCollectionExtensions.cs index 94a6ddb3..3c375452 100644 --- a/src/Turnierplan.App/Extensions/ServiceCollectionExtensions.cs +++ b/src/Turnierplan.App/Extensions/ServiceCollectionExtensions.cs @@ -1,6 +1,10 @@ using Azure.Monitor.OpenTelemetry.AspNetCore; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Identity; +using Npgsql; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; using Turnierplan.App.Security; using Turnierplan.Core.ApiKey; using Turnierplan.Core.User; @@ -14,17 +18,45 @@ internal static class ServiceCollectionExtensions { public static void AddTurnierplanMonitoring(this IServiceCollection services, IConfiguration configuration) { - var connectionString = configuration.GetSection("ApplicationInsights").GetValue("ConnectionString"); + var applicationInsightsConnectionString = configuration.GetSection("ApplicationInsights").GetValue("ConnectionString"); + var hasApplicationInsights = !string.IsNullOrEmpty(applicationInsightsConnectionString); + var hasOtlpExport = !string.IsNullOrWhiteSpace(configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); - if (!string.IsNullOrWhiteSpace(connectionString)) + if (!hasApplicationInsights && !hasOtlpExport) { - services.AddOpenTelemetry() - .WithTracing(tracing => - { - tracing.AddTurnierplanDataAccessLayer(); - tracing.AddTurnierplanDocumentRendering(); - }) - .UseAzureMonitor(opt => opt.ConnectionString = connectionString); + return; + } + + var openTelemetryBuilder = services.AddOpenTelemetry(); + + openTelemetryBuilder.WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation(); + metrics.AddHttpClientInstrumentation(); + metrics.AddRuntimeInstrumentation(); + metrics.AddNpgsqlInstrumentation(); + }); + + openTelemetryBuilder.WithTracing(tracing => + { + tracing.AddAspNetCoreInstrumentation(options => + { + // Exclude health check requests from tracing + options.Filter = context => !context.Request.Path.StartsWithSegments("/health"); + }); + tracing.AddHttpClientInstrumentation(); + tracing.AddTurnierplanDataAccessLayer(); + tracing.AddTurnierplanDocumentRendering(); + }); + + if (hasApplicationInsights) + { + openTelemetryBuilder.UseAzureMonitor(opt => opt.ConnectionString = applicationInsightsConnectionString); + } + + if (hasOtlpExport) + { + openTelemetryBuilder.UseOtlpExporter(); } } diff --git a/src/Turnierplan.App/Turnierplan.App.csproj b/src/Turnierplan.App/Turnierplan.App.csproj index e2fd2686..e145d3d5 100644 --- a/src/Turnierplan.App/Turnierplan.App.csproj +++ b/src/Turnierplan.App/Turnierplan.App.csproj @@ -24,6 +24,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/src/Turnierplan.AppHost/AppHost.cs b/src/Turnierplan.AppHost/AppHost.cs index 72aa1117..700e6337 100644 --- a/src/Turnierplan.AppHost/AppHost.cs +++ b/src/Turnierplan.AppHost/AppHost.cs @@ -30,5 +30,3 @@ } builder.Build().Run(); - -// TODO: Add OpenTelemetry collection From 95a75b6ca163b6e1b8a46b25a5d8a02f493bc6e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Sat, 14 Feb 2026 20:48:18 +0100 Subject: [PATCH 07/10] Readme --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 524e0bf8..deea441f 100644 --- a/README.md +++ b/README.md @@ -28,16 +28,16 @@ This section describes how to set up the development environment. First, you nee - Docker - .NET 10.0 SDK - Node.js v24.x and npm -- [JetBrains Rider](https://www.jetbrains.com/rider/) (free for non-commercial use) or [Visual Studio](https://visualstudio.microsoft.com/vs/) +- Your preferred IDE such as [JetBrains Rider](https://www.jetbrains.com/rider/) or [Visual Studio](https://visualstudio.microsoft.com/vs/) To run the application from source, follow these steps: 1. Open the `src/Turnierplan.slnx` solution. -2. Run the `Turnierplan.AppHost` project. This will start the Aspire AppHost which will do the following steps: - - Download the postgres container imgage and run a local database +2. Run the `Turnierplan.AppHost` project. Make sure to use the configuration *with client*. This will start the Aspire AppHost which will do the following steps: + - Download the postgres container image and run a local database - Install npm dependencies and run the client app - Run the backend `Turnierplan.App` 3. The Aspire dashboard will open up from which you can navigate to the client application ([http://localhost:45001](http://localhost:45001)). -4. Now you can log in using default credentials: The user name is `admin` and the password is `P@ssw0rd`. +4. Now you can log in using default credentials: The username is `admin` and the password is `P@ssw0rd`. When running locally, the API documentation can be viewed by opening [http://localhost:45000/scalar](http://localhost:45000/scalar). From 971edbe7447c8b7838613f31b120fb52dc97482a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Sat, 14 Feb 2026 20:50:13 +0100 Subject: [PATCH 08/10] appsettings --- src/Turnierplan.App/appsettings.Development.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Turnierplan.App/appsettings.Development.json b/src/Turnierplan.App/appsettings.Development.json index 68dff27e..702029d6 100644 --- a/src/Turnierplan.App/appsettings.Development.json +++ b/src/Turnierplan.App/appsettings.Development.json @@ -4,9 +4,6 @@ "Default": "Information" } }, - "Database": { - "ConnectionString": "Host=localhost;Database=turnierplan;Username=postgres;Password=P@ssw0rd" - }, "Turnierplan": { "ApplicationUrl": "http://localhost:45000", "InitialUserPassword": "P@ssw0rd" From 8b1f480b6909eb98ece970dbd59726dec4df5511 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Sat, 14 Feb 2026 20:52:40 +0100 Subject: [PATCH 09/10] Order --- src/Turnierplan.AppHost/Turnierplan.AppHost.csproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Turnierplan.AppHost/Turnierplan.AppHost.csproj b/src/Turnierplan.AppHost/Turnierplan.AppHost.csproj index 98d509bd..ee8ce96b 100644 --- a/src/Turnierplan.AppHost/Turnierplan.AppHost.csproj +++ b/src/Turnierplan.AppHost/Turnierplan.AppHost.csproj @@ -9,12 +9,12 @@ 523f324d-1695-4d45-a000-a79765a1468a - - - - + + + + From d78fc0260a2f57ce8c4b6cdeb2d2cdfe1666b83d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Sat, 14 Feb 2026 20:54:35 +0100 Subject: [PATCH 10/10] hasOltpExporter --- .../Extensions/ServiceCollectionExtensions.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Turnierplan.App/Extensions/ServiceCollectionExtensions.cs b/src/Turnierplan.App/Extensions/ServiceCollectionExtensions.cs index 3c375452..6ec999a4 100644 --- a/src/Turnierplan.App/Extensions/ServiceCollectionExtensions.cs +++ b/src/Turnierplan.App/Extensions/ServiceCollectionExtensions.cs @@ -20,9 +20,9 @@ public static void AddTurnierplanMonitoring(this IServiceCollection services, IC { var applicationInsightsConnectionString = configuration.GetSection("ApplicationInsights").GetValue("ConnectionString"); var hasApplicationInsights = !string.IsNullOrEmpty(applicationInsightsConnectionString); - var hasOtlpExport = !string.IsNullOrWhiteSpace(configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + var hasOltpExporter = !string.IsNullOrWhiteSpace(configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); - if (!hasApplicationInsights && !hasOtlpExport) + if (!hasApplicationInsights && !hasOltpExporter) { return; } @@ -54,7 +54,7 @@ public static void AddTurnierplanMonitoring(this IServiceCollection services, IC openTelemetryBuilder.UseAzureMonitor(opt => opt.ConnectionString = applicationInsightsConnectionString); } - if (hasOtlpExport) + if (hasOltpExporter) { openTelemetryBuilder.UseOtlpExporter(); }