From ea274978a0845ed2ed48f1812e3c845baa780a0d Mon Sep 17 00:00:00 2001 From: Rhys Bevilaqua Date: Fri, 19 Jun 2026 14:58:27 +0800 Subject: [PATCH 1/4] First pass convert aspire --- .github/workflows/nuget-audit.yml | 16 +-- .github/workflows/update-dependencies.yml | 46 +++---- README.md | 30 ++++ apphost_creation_plan.md | 92 +++++++++++++ aspire.config.json | 5 + src/AppHost/AppHost.csproj | 24 ++++ src/AppHost/Program.cs | 151 +++++++++++++++++++++ src/AppHost/Properties/launchSettings.json | 29 ++++ src/AppHost/appsettings.Development.json | 8 ++ src/AppHost/appsettings.json | 9 ++ src/AzureLoanBrokerShowcase.slnx | 1 + 11 files changed, 380 insertions(+), 31 deletions(-) create mode 100644 apphost_creation_plan.md create mode 100644 aspire.config.json create mode 100644 src/AppHost/AppHost.csproj create mode 100644 src/AppHost/Program.cs create mode 100644 src/AppHost/Properties/launchSettings.json create mode 100644 src/AppHost/appsettings.Development.json create mode 100644 src/AppHost/appsettings.json diff --git a/.github/workflows/nuget-audit.yml b/.github/workflows/nuget-audit.yml index 3c42532..e5a2359 100644 --- a/.github/workflows/nuget-audit.yml +++ b/.github/workflows/nuget-audit.yml @@ -1,9 +1,9 @@ -name: NuGet Audit -on: - workflow_dispatch: -env: - DOTNET_NOLOGO: true -jobs: - call-shared-nuget-audit: - uses: particular/shared-workflows/.github/workflows/nuget-audit.yml@main +name: NuGet Audit +on: + workflow_dispatch: +env: + DOTNET_NOLOGO: true +jobs: + call-shared-nuget-audit: + uses: particular/shared-workflows/.github/workflows/nuget-audit.yml@main secrets: inherit \ No newline at end of file diff --git a/.github/workflows/update-dependencies.yml b/.github/workflows/update-dependencies.yml index 0702b6c..8cf2585 100644 --- a/.github/workflows/update-dependencies.yml +++ b/.github/workflows/update-dependencies.yml @@ -1,24 +1,24 @@ -name: Update Dependencies -on: - schedule: - # At 14 minutes past the hour, every 6 hours, starting at 3AM UTC - - cron: '14 3/6 * * *' - workflow_dispatch: - inputs: - dryRun: - type: boolean - required: false - default: false - description: Dry run (don't create PRs) - debugMode: - type: boolean - required: false - default: false - description: Run Renovate with debug logging -jobs: - renovate: - uses: particular/shared-workflows/.github/workflows/dependency-updates.yml@main - with: - dryRun: ${{ inputs.dryRun || false }} - debugMode: ${{ inputs.debugMode || false }} +name: Update Dependencies +on: + schedule: + # At 14 minutes past the hour, every 6 hours, starting at 3AM UTC + - cron: '14 3/6 * * *' + workflow_dispatch: + inputs: + dryRun: + type: boolean + required: false + default: false + description: Dry run (don't create PRs) + debugMode: + type: boolean + required: false + default: false + description: Run Renovate with debug logging +jobs: + renovate: + uses: particular/shared-workflows/.github/workflows/dependency-updates.yml@main + with: + dryRun: ${{ inputs.dryRun || false }} + debugMode: ${{ inputs.debugMode || false }} secrets: inherit \ No newline at end of file diff --git a/README.md b/README.md index 571a0be..585b253 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ The example also ships the following monitoring services: - .NET 10 SDK - Docker - Docker Compose +- Aspire CLI (for AppHost publish/deploy workflow) ## How to run the example @@ -61,6 +62,34 @@ docker compose up -d The `dotnet publish` command builds projects and publishes Linux container images to the local Docker registry using the .NET SDK built-in container support. The Docker Compose command starts those endpoint images and configures all the additional infrastructural containers. +### Running with .NET Aspire AppHost (default setup) + +An Aspire AppHost is available at `src/AppHost/AppHost.csproj` and models the default setup from `docker-compose.yml` (no Azure Service Bus emulator path). + +To run the default setup through AppHost orchestration: + +```shell +Parameters__azureServiceBusConnectionString="" \ +Parameters__particularSoftwareLicense="" \ +dotnet run --project src/AppHost/AppHost.csproj +``` + +To generate Docker Compose deployment artifacts from the AppHost: + +```shell +Parameters__azureServiceBusConnectionString="" \ +Parameters__particularSoftwareLicense="" \ +aspire publish --apphost src/AppHost/AppHost.csproj --output-path ./aspire-output +``` + +Then run the generated artifacts: + +```shell +docker compose --env-file ./aspire-output/.env -f ./aspire-output/docker-compose.yaml up -d +``` + +The generated AppHost Compose output keeps the existing OTEL collector/Prometheus/Grafana/Jaeger path and also adds the Aspire dashboard container. + To stop the running solution and remove all deployed containers. Using a command prompt, execute the following command: ```shell @@ -108,6 +137,7 @@ The endpoint containers (Client, LoanBroker, Bank1/2/3, EmailSender) do not expo | Port | Service | |-------|-----------------------------------------------| | 1433 | SQL Server (NServiceBus persistence) | +| 18888 | Aspire dashboard UI (AppHost-generated Compose) | | 3000 | Grafana | | 4317 | OpenTelemetry Collector — OTLP gRPC | | 5318 | OpenTelemetry Collector — OTLP HTTP | diff --git a/apphost_creation_plan.md b/apphost_creation_plan.md new file mode 100644 index 0000000..90b5e7c --- /dev/null +++ b/apphost_creation_plan.md @@ -0,0 +1,92 @@ +# Aspire AppHost creation plan (Docker Compose parity) + +## Goal + +- [x] Add a new .NET Aspire AppHost that models the existing **default** `docker-compose.yml` topology (no ASB emulator flow). +- [x] Keep current behavior for local orchestration and publishing so container artifacts can be generated from AppHost for Docker Compose deployments. +- [x] Preserve the existing OpenTelemetry collector pipeline (Prometheus + Jaeger) while also enabling Aspire’s own telemetry/dashboard support. + +## 1) Create AppHost and baseline wiring + +- [x] Add a new project, e.g. `src/AppHost/AppHost.csproj`, and include it in `src/AzureLoanBrokerShowcase.slnx`. +- [x] Add required AppHost packages: + - `Aspire.Hosting.AppHost` + - `Aspire.Hosting.Docker` (for Docker Compose environment publishing) +- [x] In `AppHost/Program.cs`, create the builder with `DistributedApplication.CreateBuilder(args)`. +- [x] Add a Docker Compose compute environment in the app model (`AddDockerComposeEnvironment(...)`) so `aspire publish` can emit compose artifacts. + +## 2) Model infrastructure from `docker-compose.yml` + +- [x] Model the following as container resources in AppHost (same image intent as Compose): + - `sqlserver` + - `creditbureau` + - `otel-collector` + - `prometheus` + - `grafana` + - `jaeger` + - `servicecontrol` + - `servicecontrol-db` + - `servicecontrol-audit` + - `servicecontrol-monitoring` + - `servicepulse` +- [x] Mirror key container settings from Compose: + - ports + - volumes/bind mounts + - env vars and required secrets/placeholders + - startup ordering/dependencies (where relevant) +- [x] Explicitly **exclude** ASB emulator services and related health-check sidecar from AppHost. + +## 3) Model application services and references + +- [x] Add project resources for: + - `loan-broker`, `bank1`, `bank2`, `bank3`, `email-sender`, `client` +- [x] Keep image identity aligned with current publish flow (`loanbroker-azure/*`) so generated artifacts remain compatible with existing conventions. +- [x] Model `creditbureau` either: + - as Dockerfile-based resource matching current `src/CreditBureau/Dockerfile`, or + - as a project resource only if container publish parity is confirmed for the Azure Functions project. +- [x] Wire environment variables currently provided by `env/azure.env` + `env/metrics.env`: + - `AZURE_SERVICE_BUS_CONNECTION_STRING` + - `SQL_CONNECTION_STRING` + - `CREDIT_BUREAU_URL` + - `OTLP_METRICS_URL` + - `OTLP_TRACING_URL` +- [x] Use AppHost parameters/secrets for externalized values (especially Service Bus connection string), so publish output contains resolvable placeholders instead of hard-coded secrets. + +## 4) Telemetry coexistence plan (existing collector + Aspire telemetry) + +- [x] Keep existing `otel-collector` container and its mounted config (`src/otel/otel-collector-config.yaml`). +- [x] Keep endpoint OTLP env vars pointing to collector (`http://otel-collector:5318/...`) so current NServiceBus/OpenTelemetry emission path is unchanged. +- [x] Enable Aspire dashboard support in the Docker Compose environment (do not replace existing collector/Prometheus/Grafana/Jaeger stack). +- [x] Confirm generated Compose output includes both: + - existing observability containers, and + - Aspire dashboard/telemetry resources (if enabled for that environment). + +## 5) Publishing and deployment flow + +- [x] Standardize on Aspire CLI publish flow for AppHost artifacts: + - `aspire publish --apphost src/AppHost/AppHost.csproj` +- [x] Ensure published output includes Docker Compose artifacts (`docker-compose.yml`, `.env` placeholders, related generated files). +- [x] Keep ability to build/push images for project/container resources via Aspire Docker integration (including remote image naming/tag strategy when needed). +- [x] Ensure published artifact structure can replace today’s manual `dotnet publish ... /t:PublishContainer` + `docker compose up` workflow for containerized runs. + +## 6) Documentation and operational updates + +- [x] Update `README.md` with AppHost run/publish instructions (default scenario only; emulator path remains separate or explicitly out of scope). +- [x] Document required parameters/secrets and how to pass them during `aspire publish`. +- [x] Document expected host ports and any AppHost-specific differences from current Compose UX. + +## 7) Suggested implementation order + +- [x] Scaffold AppHost + Docker integration package. +- [x] Add infrastructure containers first (SQL/collector/monitoring/Particular platform). +- [x] Add application project resources and env wiring. +- [x] Add publish customization to preserve container naming and compose parity. +- [x] Update docs and perform parity validation against existing `docker-compose.yml`. + +## References used + +- [x] Aspire AppHost overview: +- [x] Aspire deployment pipeline model: +- [x] Aspire CLI publish: +- [x] Aspire Docker integration (Compose generation/customization): +- [x] Aspire Community Toolkit overview: diff --git a/aspire.config.json b/aspire.config.json new file mode 100644 index 0000000..983fec8 --- /dev/null +++ b/aspire.config.json @@ -0,0 +1,5 @@ +{ + "appHost": { + "path": "src/AppHost/AppHost.csproj" + } +} \ No newline at end of file diff --git a/src/AppHost/AppHost.csproj b/src/AppHost/AppHost.csproj new file mode 100644 index 0000000..e516175 --- /dev/null +++ b/src/AppHost/AppHost.csproj @@ -0,0 +1,24 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + + + + + + + + + + diff --git a/src/AppHost/Program.cs b/src/AppHost/Program.cs new file mode 100644 index 0000000..f7b7596 --- /dev/null +++ b/src/AppHost/Program.cs @@ -0,0 +1,151 @@ +var builder = DistributedApplication.CreateBuilder(args); + +builder.AddDockerComposeEnvironment("compose") + .WithDashboard(enabled: true); + +var serviceBusConnectionString = builder.AddParameter("azureServiceBusConnectionString", secret: true); +var particularSoftwareLicense = builder.AddParameter("particularSoftwareLicense", secret: true); + +const string sqlConnectionString = + "Server=sqlserver;Database=NServiceBus;User Id=sa;Password=YourStrong@Passw0rd;TrustServerCertificate=True;"; +const string creditBureauUrl = "http://creditbureau:80/api/score"; +const string otlpMetricsUrl = "http://otel-collector:5318/v1/metrics"; +const string otlpTracingUrl = "http://otel-collector:5318/v1/traces"; + +var sqlServer = builder.AddContainer("sqlserver", "mcr.microsoft.com/mssql/server", "2025-latest") + .WithEnvironment("ACCEPT_EULA", "Y") + .WithEnvironment("MSSQL_SA_PASSWORD", "YourStrong@Passw0rd") + .WithEnvironment("MSSQL_PID", "Developer") + .WithEnvironment("MSSQL_COLLATION", "SQL_Latin1_General_CP1_CI_AS") + .WithBindMount("../src/sqlserver/init-db.sh", "/var/opt/mssql/init-db.sh") + .WithVolume("sqlserver-data", "/var/opt/mssql") + .WithArgs("bash", "-c", "/var/opt/mssql/init-db.sh") + .WithEndpoint(port: 1433, targetPort: 1433, name: "sql", scheme: "tcp", isExternal: true); + +var creditBureau = builder.AddDockerfile( + "creditbureau", + "..", + dockerfilePath: "./src/CreditBureau/Dockerfile") + .WithEndpoint(targetPort: 80, port: 7071, scheme: "http", name: "http", isExternal: true) + .WaitFor(sqlServer); + +var otelCollector = builder.AddContainer("otel-collector", "otel/opentelemetry-collector-contrib", "0.102.1") + .WithBindMount("../src/otel/otel-collector-config.yaml", "/etc/otelcol-contrib/config.yaml", isReadOnly: true) + .WithArgs("--config=/etc/otelcol-contrib/config.yaml") + .WithEndpoint(port: 4317, targetPort: 4317, name: "grpc", scheme: "tcp", isExternal: true) + .WithEndpoint(targetPort: 5318, port: 5318, name: "http", scheme: "http", isExternal: true) + .WithEndpoint(targetPort: 1234, port: 1234, name: "prometheus", scheme: "http", isExternal: true); + +var prometheus = builder.AddContainer("prometheus", "docker.io/prom/prometheus", "v2.53.2") + .WithBindMount("../src/prometheus", "/etc/prometheus") + .WithVolume("prometheus-data", "/prometheus") + .WithArgs("--web.enable-lifecycle", "--config.file=/etc/prometheus/prometheus.yml") + .WithEndpoint(targetPort: 9090, port: 9090, scheme: "http", name: "http", isExternal: true) + .WaitFor(otelCollector); + +var grafana = builder.AddContainer("grafana", "docker.io/grafana/grafana-oss", "latest") + .WithBindMount("../src/grafana/provisioning", "/etc/grafana/provisioning") + .WithBindMount("../src/grafana/dashboards", "/var/lib/grafana/dashboards") + .WithVolume("grafana-data", "/var/lib/grafana") + .WithEndpoint(targetPort: 3000, port: 3000, scheme: "http", name: "http", isExternal: true) + .WaitFor(prometheus); + +var jaeger = builder.AddContainer("jaeger", "docker.io/jaegertracing/all-in-one", "latest") + .WithEnvironment("COLLECTOR_OTLP_ENABLED", "true") + .WithVolume("jaeger-data", "/tmp") + .WithEndpoint(targetPort: 16686, port: 16686, scheme: "http", name: "http", isExternal: true) + .WaitFor(otelCollector); + +var serviceControlDb = builder.AddContainer("servicecontrol-db", "docker.io/particular/servicecontrol-ravendb", "latest") + .WithEnvironment("RAVEN_ARGS", "--Setup.Mode=None") + .WithEnvironment("RAVEN_Security_UnsecuredAccessAllowed", "PublicNetwork") + .WithVolume("servicecontrol-db", "/var/lib/ravendb/data") + .WithVolume("servicecontrol-db-config", "/var/lib/ravendb/config") + .WithEndpoint(targetPort: 8080, port: 8080, scheme: "http", name: "http", isExternal: true); + +var serviceControl = builder.AddContainer("servicecontrol", "docker.io/particular/servicecontrol", "latest") + .WithEnvironment("TRANSPORTTYPE", "NetStandardAzureServiceBus") + .WithEnvironment("PARTICULARSOFTWARE_LICENSE", particularSoftwareLicense) + .WithEnvironment("RAVENDB_CONNECTIONSTRING", "http://servicecontrol-db:8080") + .WithEnvironment("REMOTEINSTANCES", "[{\"api_uri\":\"http://servicecontrol-audit:44444/api\"}]") + .WithEnvironment("SERVICECONTROL_CONNECTIONSTRING", serviceBusConnectionString) + .WithEnvironment("SERVICECONTROL_ALLOWMESSAGEEDITING", "true") + .WithArgs("--setup-and-run") + .WithEndpoint(targetPort: 33333, port: 33333, scheme: "http", name: "http", isExternal: true) + .WaitFor(serviceControlDb); + +var serviceControlAudit = builder.AddContainer("servicecontrol-audit", "docker.io/particular/servicecontrol-audit", "latest") + .WithEnvironment("TRANSPORTTYPE", "NetStandardAzureServiceBus") + .WithEnvironment("PARTICULARSOFTWARE_LICENSE", particularSoftwareLicense) + .WithEnvironment("RAVENDB_CONNECTIONSTRING", "http://servicecontrol-db:8080") + .WithEnvironment("SERVICECONTROL_AUDIT_CONNECTIONSTRING", serviceBusConnectionString) + .WithEnvironment("SERVICECONTROL_AUDIT_SERVICECONTROLQUEUEADDRESS", "Particular.ServiceControl") + .WithArgs("--setup-and-run") + .WithEndpoint(targetPort: 44444, port: 44444, scheme: "http", name: "http", isExternal: true) + .WaitFor(serviceControlDb); + +var serviceControlMonitoring = builder + .AddContainer("servicecontrol-monitoring", "docker.io/particular/servicecontrol-monitoring", "latest") + .WithEnvironment("TRANSPORTTYPE", "NetStandardAzureServiceBus") + .WithEnvironment("PARTICULARSOFTWARE_LICENSE", particularSoftwareLicense) + .WithEnvironment("MONITORING_CONNECTIONSTRING", serviceBusConnectionString) + .WithArgs("--setup-and-run") + .WithEndpoint(targetPort: 33633, port: 33633, scheme: "http", name: "http", isExternal: true) + .WaitFor(serviceControlDb); + +var servicePulse = builder.AddContainer("servicepulse", "docker.io/particular/servicepulse", "latest") + .WithEnvironment("SERVICECONTROL_URL", "http://servicecontrol:33333") + .WithEnvironment("MONITORING_URL", "http://servicecontrol-monitoring:33633") + .WithEndpoint(targetPort: 9090, port: 9999, scheme: "http", name: "http", isExternal: true) + .WaitFor(serviceControl) + .WaitFor(serviceControlMonitoring); + +var sharedServiceEnvironment = new (string Name, string Value)[] +{ + ("SQL_CONNECTION_STRING", sqlConnectionString), + ("CREDIT_BUREAU_URL", creditBureauUrl), + ("OTLP_METRICS_URL", otlpMetricsUrl), + ("OTLP_TRACING_URL", otlpTracingUrl) +}; + +var loanBroker = builder.AddProject("loan-broker") + .WithEnvironment("AZURE_SERVICE_BUS_CONNECTION_STRING", serviceBusConnectionString); + +foreach (var (name, value) in sharedServiceEnvironment) +{ + loanBroker.WithEnvironment(name, value); +} + +loanBroker.WaitFor(creditBureau).WaitFor(servicePulse); + +var bank1 = builder.AddProject("bank1") + .WithEnvironment("AZURE_SERVICE_BUS_CONNECTION_STRING", serviceBusConnectionString) + .WaitFor(servicePulse); + +var bank2 = builder.AddProject("bank2") + .WithEnvironment("AZURE_SERVICE_BUS_CONNECTION_STRING", serviceBusConnectionString) + .WaitFor(servicePulse); + +var bank3 = builder.AddProject("bank3") + .WithEnvironment("AZURE_SERVICE_BUS_CONNECTION_STRING", serviceBusConnectionString) + .WaitFor(servicePulse); + +var emailSender = builder.AddProject("email-sender") + .WithEnvironment("AZURE_SERVICE_BUS_CONNECTION_STRING", serviceBusConnectionString) + .WaitFor(servicePulse); + +var client = builder.AddProject("client") + .WithEnvironment("AZURE_SERVICE_BUS_CONNECTION_STRING", serviceBusConnectionString) + .WithArgs("--demo") + .WaitFor(servicePulse) + .WaitFor(loanBroker); + +foreach (var endpoint in new[] { bank1, bank2, bank3, emailSender, client }) +{ + foreach (var (name, value) in sharedServiceEnvironment) + { + endpoint.WithEnvironment(name, value); + } +} + +builder.Build().Run(); diff --git a/src/AppHost/Properties/launchSettings.json b/src/AppHost/Properties/launchSettings.json new file mode 100644 index 0000000..a8224fc --- /dev/null +++ b/src/AppHost/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://loanbrokershowcase.dev.localhost:17290;http://loanbrokershowcase.dev.localhost:15170", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21118", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22281" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://loanbrokershowcase.dev.localhost:15170", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19233", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20173" + } + } + } +} diff --git a/src/AppHost/appsettings.Development.json b/src/AppHost/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/src/AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/AppHost/appsettings.json b/src/AppHost/appsettings.json new file mode 100644 index 0000000..31c092a --- /dev/null +++ b/src/AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/src/AzureLoanBrokerShowcase.slnx b/src/AzureLoanBrokerShowcase.slnx index 4274c9b..fee6aa3 100644 --- a/src/AzureLoanBrokerShowcase.slnx +++ b/src/AzureLoanBrokerShowcase.slnx @@ -34,6 +34,7 @@ + From 985ec2e0e089e0c9db352278ee584acfe369d305 Mon Sep 17 00:00:00 2001 From: Rhys Bevilaqua Date: Mon, 22 Jun 2026 08:38:44 +0800 Subject: [PATCH 2/4] Update some packages --- README.md | 4 +- src/AppHost/AppHost.csproj | 9 ++-- src/AppHost/Program.cs | 93 ++++++++++++++------------------------ 3 files changed, 40 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index 585b253..d66382a 100644 --- a/README.md +++ b/README.md @@ -69,16 +69,16 @@ An Aspire AppHost is available at `src/AppHost/AppHost.csproj` and models the de To run the default setup through AppHost orchestration: ```shell +env 'Parameters__particular-license=' \ Parameters__azureServiceBusConnectionString="" \ -Parameters__particularSoftwareLicense="" \ dotnet run --project src/AppHost/AppHost.csproj ``` To generate Docker Compose deployment artifacts from the AppHost: ```shell +env 'Parameters__particular-license=' \ Parameters__azureServiceBusConnectionString="" \ -Parameters__particularSoftwareLicense="" \ aspire publish --apphost src/AppHost/AppHost.csproj --output-path ./aspire-output ``` diff --git a/src/AppHost/AppHost.csproj b/src/AppHost/AppHost.csproj index e516175..f5ace65 100644 --- a/src/AppHost/AppHost.csproj +++ b/src/AppHost/AppHost.csproj @@ -1,16 +1,17 @@ - + Exe net10.0 enable enable + 4e39df4e-d8de-43bc-84c0-0b596fb97262 - - - + + + diff --git a/src/AppHost/Program.cs b/src/AppHost/Program.cs index f7b7596..1f93389 100644 --- a/src/AppHost/Program.cs +++ b/src/AppHost/Program.cs @@ -1,10 +1,12 @@ +using Aspire.Hosting.ApplicationModel; + var builder = DistributedApplication.CreateBuilder(args); builder.AddDockerComposeEnvironment("compose") .WithDashboard(enabled: true); var serviceBusConnectionString = builder.AddParameter("azureServiceBusConnectionString", secret: true); -var particularSoftwareLicense = builder.AddParameter("particularSoftwareLicense", secret: true); +var transport = builder.AddConnectionString("transport", ReferenceExpression.Create($"{serviceBusConnectionString}")); const string sqlConnectionString = "Server=sqlserver;Database=NServiceBus;User Id=sa;Password=YourStrong@Passw0rd;TrustServerCertificate=True;"; @@ -12,40 +14,43 @@ const string otlpMetricsUrl = "http://otel-collector:5318/v1/metrics"; const string otlpTracingUrl = "http://otel-collector:5318/v1/traces"; -var sqlServer = builder.AddContainer("sqlserver", "mcr.microsoft.com/mssql/server", "2025-latest") +var sqlServer = builder.AddSqlServer("sqlserver") .WithEnvironment("ACCEPT_EULA", "Y") .WithEnvironment("MSSQL_SA_PASSWORD", "YourStrong@Passw0rd") .WithEnvironment("MSSQL_PID", "Developer") .WithEnvironment("MSSQL_COLLATION", "SQL_Latin1_General_CP1_CI_AS") - .WithBindMount("../src/sqlserver/init-db.sh", "/var/opt/mssql/init-db.sh") + .WithContainerRuntimeArgs("--platform", "linux/amd64") + .WithContainerRuntimeArgs("--ulimit", "stack=8192:8192") .WithVolume("sqlserver-data", "/var/opt/mssql") - .WithArgs("bash", "-c", "/var/opt/mssql/init-db.sh") - .WithEndpoint(port: 1433, targetPort: 1433, name: "sql", scheme: "tcp", isExternal: true); + .WithBindMount("../src/sqlserver/", "/tmp/scripts/") + .AddDatabase("sqlserver-db","NServiceBus") +// .WithArgs("bash", "-c", "/tmp/scripts/init-db.sh") + ; var creditBureau = builder.AddDockerfile( "creditbureau", - "..", + "../..", dockerfilePath: "./src/CreditBureau/Dockerfile") .WithEndpoint(targetPort: 80, port: 7071, scheme: "http", name: "http", isExternal: true) .WaitFor(sqlServer); var otelCollector = builder.AddContainer("otel-collector", "otel/opentelemetry-collector-contrib", "0.102.1") - .WithBindMount("../src/otel/otel-collector-config.yaml", "/etc/otelcol-contrib/config.yaml", isReadOnly: true) + .WithBindMount("../otel/otel-collector-config.yaml", "/etc/otelcol-contrib/config.yaml", isReadOnly: true) .WithArgs("--config=/etc/otelcol-contrib/config.yaml") .WithEndpoint(port: 4317, targetPort: 4317, name: "grpc", scheme: "tcp", isExternal: true) .WithEndpoint(targetPort: 5318, port: 5318, name: "http", scheme: "http", isExternal: true) .WithEndpoint(targetPort: 1234, port: 1234, name: "prometheus", scheme: "http", isExternal: true); var prometheus = builder.AddContainer("prometheus", "docker.io/prom/prometheus", "v2.53.2") - .WithBindMount("../src/prometheus", "/etc/prometheus") + .WithBindMount("../prometheus", "/etc/prometheus") .WithVolume("prometheus-data", "/prometheus") .WithArgs("--web.enable-lifecycle", "--config.file=/etc/prometheus/prometheus.yml") .WithEndpoint(targetPort: 9090, port: 9090, scheme: "http", name: "http", isExternal: true) .WaitFor(otelCollector); var grafana = builder.AddContainer("grafana", "docker.io/grafana/grafana-oss", "latest") - .WithBindMount("../src/grafana/provisioning", "/etc/grafana/provisioning") - .WithBindMount("../src/grafana/dashboards", "/var/lib/grafana/dashboards") + .WithBindMount("../grafana/provisioning", "/etc/grafana/provisioning") + .WithBindMount("../grafana/dashboards", "/var/lib/grafana/dashboards") .WithVolume("grafana-data", "/var/lib/grafana") .WithEndpoint(targetPort: 3000, port: 3000, scheme: "http", name: "http", isExternal: true) .WaitFor(prometheus); @@ -56,49 +61,10 @@ .WithEndpoint(targetPort: 16686, port: 16686, scheme: "http", name: "http", isExternal: true) .WaitFor(otelCollector); -var serviceControlDb = builder.AddContainer("servicecontrol-db", "docker.io/particular/servicecontrol-ravendb", "latest") - .WithEnvironment("RAVEN_ARGS", "--Setup.Mode=None") - .WithEnvironment("RAVEN_Security_UnsecuredAccessAllowed", "PublicNetwork") - .WithVolume("servicecontrol-db", "/var/lib/ravendb/data") - .WithVolume("servicecontrol-db-config", "/var/lib/ravendb/config") - .WithEndpoint(targetPort: 8080, port: 8080, scheme: "http", name: "http", isExternal: true); - -var serviceControl = builder.AddContainer("servicecontrol", "docker.io/particular/servicecontrol", "latest") - .WithEnvironment("TRANSPORTTYPE", "NetStandardAzureServiceBus") - .WithEnvironment("PARTICULARSOFTWARE_LICENSE", particularSoftwareLicense) - .WithEnvironment("RAVENDB_CONNECTIONSTRING", "http://servicecontrol-db:8080") - .WithEnvironment("REMOTEINSTANCES", "[{\"api_uri\":\"http://servicecontrol-audit:44444/api\"}]") - .WithEnvironment("SERVICECONTROL_CONNECTIONSTRING", serviceBusConnectionString) - .WithEnvironment("SERVICECONTROL_ALLOWMESSAGEEDITING", "true") - .WithArgs("--setup-and-run") - .WithEndpoint(targetPort: 33333, port: 33333, scheme: "http", name: "http", isExternal: true) - .WaitFor(serviceControlDb); - -var serviceControlAudit = builder.AddContainer("servicecontrol-audit", "docker.io/particular/servicecontrol-audit", "latest") - .WithEnvironment("TRANSPORTTYPE", "NetStandardAzureServiceBus") - .WithEnvironment("PARTICULARSOFTWARE_LICENSE", particularSoftwareLicense) - .WithEnvironment("RAVENDB_CONNECTIONSTRING", "http://servicecontrol-db:8080") - .WithEnvironment("SERVICECONTROL_AUDIT_CONNECTIONSTRING", serviceBusConnectionString) - .WithEnvironment("SERVICECONTROL_AUDIT_SERVICECONTROLQUEUEADDRESS", "Particular.ServiceControl") - .WithArgs("--setup-and-run") - .WithEndpoint(targetPort: 44444, port: 44444, scheme: "http", name: "http", isExternal: true) - .WaitFor(serviceControlDb); - -var serviceControlMonitoring = builder - .AddContainer("servicecontrol-monitoring", "docker.io/particular/servicecontrol-monitoring", "latest") - .WithEnvironment("TRANSPORTTYPE", "NetStandardAzureServiceBus") - .WithEnvironment("PARTICULARSOFTWARE_LICENSE", particularSoftwareLicense) - .WithEnvironment("MONITORING_CONNECTIONSTRING", serviceBusConnectionString) - .WithArgs("--setup-and-run") - .WithEndpoint(targetPort: 33633, port: 33633, scheme: "http", name: "http", isExternal: true) - .WaitFor(serviceControlDb); - -var servicePulse = builder.AddContainer("servicepulse", "docker.io/particular/servicepulse", "latest") - .WithEnvironment("SERVICECONTROL_URL", "http://servicecontrol:33333") - .WithEnvironment("MONITORING_URL", "http://servicecontrol-monitoring:33633") - .WithEndpoint(targetPort: 9090, port: 9999, scheme: "http", name: "http", isExternal: true) - .WaitFor(serviceControl) - .WaitFor(serviceControlMonitoring); +var platform = builder + .AddParticularPlatform("particular") + .WithTransportAzureServiceBus(transport) + .AddDefaultComponents(); var sharedServiceEnvironment = new (string Name, string Value)[] { @@ -109,35 +75,42 @@ }; var loanBroker = builder.AddProject("loan-broker") - .WithEnvironment("AZURE_SERVICE_BUS_CONNECTION_STRING", serviceBusConnectionString); + .WithParticularPlatform(platform) + .WithEnvironment("AZURE_SERVICE_BUS_CONNECTION_STRING", serviceBusConnectionString) + .WaitFor(platform); foreach (var (name, value) in sharedServiceEnvironment) { loanBroker.WithEnvironment(name, value); } -loanBroker.WaitFor(creditBureau).WaitFor(servicePulse); +loanBroker.WaitFor(creditBureau); var bank1 = builder.AddProject("bank1") + .WithParticularPlatform(platform) .WithEnvironment("AZURE_SERVICE_BUS_CONNECTION_STRING", serviceBusConnectionString) - .WaitFor(servicePulse); + .WaitFor(platform); var bank2 = builder.AddProject("bank2") + .WithParticularPlatform(platform) .WithEnvironment("AZURE_SERVICE_BUS_CONNECTION_STRING", serviceBusConnectionString) - .WaitFor(servicePulse); + .WaitFor(platform); var bank3 = builder.AddProject("bank3") + .WithParticularPlatform(platform) .WithEnvironment("AZURE_SERVICE_BUS_CONNECTION_STRING", serviceBusConnectionString) - .WaitFor(servicePulse); + .WaitFor(platform); var emailSender = builder.AddProject("email-sender") + .WithParticularPlatform(platform) .WithEnvironment("AZURE_SERVICE_BUS_CONNECTION_STRING", serviceBusConnectionString) - .WaitFor(servicePulse); + .WaitFor(platform); var client = builder.AddProject("client") + .WithParticularPlatform(platform) .WithEnvironment("AZURE_SERVICE_BUS_CONNECTION_STRING", serviceBusConnectionString) .WithArgs("--demo") - .WaitFor(servicePulse) + .WaitFor(platform) .WaitFor(loanBroker); foreach (var endpoint in new[] { bank1, bank2, bank3, emailSender, client }) From d01fda66979613d4b97be4f33c0161c4b7be1a01 Mon Sep 17 00:00:00 2001 From: Warwick Schroeder Date: Mon, 22 Jun 2026 13:02:04 +0800 Subject: [PATCH 3/4] Refactor Azure transport configuration and improve OpenTelemetry integration --- .gitignore | 1 + src/AppHost/Program.cs | 99 ++++++------------- src/AppHost/appsettings.Development.json | 8 -- src/Client/UILoop.cs | 12 ++- .../OpenTelemetryExtensions.cs | 33 +++++-- src/CommonConfigurations/SharedConventions.cs | 8 +- 6 files changed, 76 insertions(+), 85 deletions(-) delete mode 100644 src/AppHost/appsettings.Development.json diff --git a/.gitignore b/.gitignore index fd0d72e..8f3b5a0 100644 --- a/.gitignore +++ b/.gitignore @@ -106,3 +106,4 @@ src/scaffolding.config src/volumes .env +src/AppHost/appsettings.Development.json diff --git a/src/AppHost/Program.cs b/src/AppHost/Program.cs index 1f93389..39c0602 100644 --- a/src/AppHost/Program.cs +++ b/src/AppHost/Program.cs @@ -8,24 +8,13 @@ var serviceBusConnectionString = builder.AddParameter("azureServiceBusConnectionString", secret: true); var transport = builder.AddConnectionString("transport", ReferenceExpression.Create($"{serviceBusConnectionString}")); -const string sqlConnectionString = - "Server=sqlserver;Database=NServiceBus;User Id=sa;Password=YourStrong@Passw0rd;TrustServerCertificate=True;"; -const string creditBureauUrl = "http://creditbureau:80/api/score"; -const string otlpMetricsUrl = "http://otel-collector:5318/v1/metrics"; -const string otlpTracingUrl = "http://otel-collector:5318/v1/traces"; - -var sqlServer = builder.AddSqlServer("sqlserver") - .WithEnvironment("ACCEPT_EULA", "Y") - .WithEnvironment("MSSQL_SA_PASSWORD", "YourStrong@Passw0rd") +var sqlPassword = builder.AddParameter("sql-password", "YourStrong@Passw0rd"); +var sqlServer = builder.AddSqlServer("sqlserver", password: sqlPassword) .WithEnvironment("MSSQL_PID", "Developer") .WithEnvironment("MSSQL_COLLATION", "SQL_Latin1_General_CP1_CI_AS") - .WithContainerRuntimeArgs("--platform", "linux/amd64") - .WithContainerRuntimeArgs("--ulimit", "stack=8192:8192") - .WithVolume("sqlserver-data", "/var/opt/mssql") - .WithBindMount("../src/sqlserver/", "/tmp/scripts/") - .AddDatabase("sqlserver-db","NServiceBus") -// .WithArgs("bash", "-c", "/tmp/scripts/init-db.sh") - ; + .WithVolume("sqlserver-data", "/var/opt/mssql"); + +var nsbDatabase = sqlServer.AddDatabase("nsb-database", "NServiceBus"); var creditBureau = builder.AddDockerfile( "creditbureau", @@ -66,59 +55,33 @@ .WithTransportAzureServiceBus(transport) .AddDefaultComponents(); -var sharedServiceEnvironment = new (string Name, string Value)[] -{ - ("SQL_CONNECTION_STRING", sqlConnectionString), - ("CREDIT_BUREAU_URL", creditBureauUrl), - ("OTLP_METRICS_URL", otlpMetricsUrl), - ("OTLP_TRACING_URL", otlpTracingUrl) -}; - -var loanBroker = builder.AddProject("loan-broker") - .WithParticularPlatform(platform) - .WithEnvironment("AZURE_SERVICE_BUS_CONNECTION_STRING", serviceBusConnectionString) - .WaitFor(platform); - -foreach (var (name, value) in sharedServiceEnvironment) -{ - loanBroker.WithEnvironment(name, value); -} - -loanBroker.WaitFor(creditBureau); - -var bank1 = builder.AddProject("bank1") - .WithParticularPlatform(platform) - .WithEnvironment("AZURE_SERVICE_BUS_CONNECTION_STRING", serviceBusConnectionString) - .WaitFor(platform); - -var bank2 = builder.AddProject("bank2") - .WithParticularPlatform(platform) - .WithEnvironment("AZURE_SERVICE_BUS_CONNECTION_STRING", serviceBusConnectionString) - .WaitFor(platform); - -var bank3 = builder.AddProject("bank3") - .WithParticularPlatform(platform) - .WithEnvironment("AZURE_SERVICE_BUS_CONNECTION_STRING", serviceBusConnectionString) - .WaitFor(platform); - -var emailSender = builder.AddProject("email-sender") - .WithParticularPlatform(platform) - .WithEnvironment("AZURE_SERVICE_BUS_CONNECTION_STRING", serviceBusConnectionString) - .WaitFor(platform); - -var client = builder.AddProject("client") - .WithParticularPlatform(platform) - .WithEnvironment("AZURE_SERVICE_BUS_CONNECTION_STRING", serviceBusConnectionString) +var otelHttp = otelCollector.GetEndpoint("http"); +var creditBureauHttp = creditBureau.GetEndpoint("http"); + +IResourceBuilder ConfigureEndpoint(IResourceBuilder endpoint) => + endpoint + .WithParticularPlatform(platform) + .WithEnvironment(context => + { + context.EnvironmentVariables["SQL_CONNECTION_STRING"] = nsbDatabase.Resource.ConnectionStringExpression; + context.EnvironmentVariables["CREDIT_BUREAU_URL"] = ReferenceExpression.Create($"{creditBureauHttp}/api/score"); + context.EnvironmentVariables["OTLP_METRICS_URL"] = ReferenceExpression.Create($"{otelHttp}/v1/metrics"); + context.EnvironmentVariables["OTLP_TRACING_URL"] = ReferenceExpression.Create($"{otelHttp}/v1/traces"); + }) + .WaitFor(sqlServer) + .WaitFor(otelCollector) + .WaitFor(platform); + +var loanBroker = ConfigureEndpoint(builder.AddProject("loan-broker")) + .WaitFor(creditBureau); + +ConfigureEndpoint(builder.AddProject("bank1")); +ConfigureEndpoint(builder.AddProject("bank2")); +ConfigureEndpoint(builder.AddProject("bank3")); +ConfigureEndpoint(builder.AddProject("email-sender")); + +ConfigureEndpoint(builder.AddProject("client")) .WithArgs("--demo") - .WaitFor(platform) .WaitFor(loanBroker); -foreach (var endpoint in new[] { bank1, bank2, bank3, emailSender, client }) -{ - foreach (var (name, value) in sharedServiceEnvironment) - { - endpoint.WithEnvironment(name, value); - } -} - builder.Build().Run(); diff --git a/src/AppHost/appsettings.Development.json b/src/AppHost/appsettings.Development.json deleted file mode 100644 index 0c208ae..0000000 --- a/src/AppHost/appsettings.Development.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - } -} diff --git a/src/Client/UILoop.cs b/src/Client/UILoop.cs index d88daa1..d99e1df 100644 --- a/src/Client/UILoop.cs +++ b/src/Client/UILoop.cs @@ -20,6 +20,16 @@ public static async Task RunLoop(IHost app, string[] args) var messageSession = app.Services.GetRequiredService(); var running = true; var continuousSend = false; + + // Console.KeyAvailable/ReadKey throw when stdin is redirected (e.g. running as an Aspire + // project resource or any other non-TTY host), so only poll the keyboard when we actually + // have an interactive console. In --demo mode the loop drives itself and needs no input. + var interactive = !Console.IsInputRedirected; + if (!interactive) + { + Console.WriteLine("No interactive console detected; keyboard controls are disabled."); + } + Console.CancelKeyPress += (_, e) => { e.Cancel = true; @@ -37,7 +47,7 @@ public static async Task RunLoop(IHost app, string[] args) while (running) { - if (Console.KeyAvailable) + if (interactive && Console.KeyAvailable) { var k = Console.ReadKey(true); switch (k.Key) diff --git a/src/CommonConfigurations/OpenTelemetryExtensions.cs b/src/CommonConfigurations/OpenTelemetryExtensions.cs index 002a56e..d92f4e7 100644 --- a/src/CommonConfigurations/OpenTelemetryExtensions.cs +++ b/src/CommonConfigurations/OpenTelemetryExtensions.cs @@ -21,7 +21,7 @@ public static void EnableOpenTelemetryMetrics(this EndpointConfiguration endpoin var resourceBuilder = ResourceBuilder.CreateDefault().AddAttributes(attributes); - Sdk.CreateMeterProviderBuilder() + var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() .SetResourceBuilder(resourceBuilder) .AddMeter("NServiceBus.Core.Pipeline.Incoming") .AddMeter("LoanBroker") @@ -30,8 +30,16 @@ public static void EnableOpenTelemetryMetrics(this EndpointConfiguration endpoin var url = Environment.GetEnvironmentVariable(OtlpMetricsUrlEnvVar) ?? OtlpMetricsDefaultUrl; cfg.Endpoint = new Uri(url); cfg.Protocol = OtlpExportProtocol.HttpProtobuf; - }) - .Build(); + }); + + // When orchestrated by Aspire, also export to the dashboard's OTLP endpoint so traces and + // metrics show up in the Aspire dashboard. + if (HasAspireOtlpEndpoint()) + { + meterProviderBuilder.AddOtlpExporter(); + } + + meterProviderBuilder.Build(); } public static void EnableOpenTelemetryTracing(this EndpointConfiguration endpointConfiguration) @@ -46,7 +54,7 @@ public static void EnableOpenTelemetryTracing(this EndpointConfiguration endpoin var resourceBuilder = ResourceBuilder.CreateDefault().AddAttributes(attributes); - Sdk.CreateTracerProviderBuilder() + var tracerProviderBuilder = Sdk.CreateTracerProviderBuilder() .SetResourceBuilder(resourceBuilder) .AddSource("NServiceBus.Core") .AddOtlpExporter(cfg => @@ -54,10 +62,23 @@ public static void EnableOpenTelemetryTracing(this EndpointConfiguration endpoin var url = Environment.GetEnvironmentVariable(OtlpTracesUrlEnvVar) ?? OtlpTracesDefaultUrl; cfg.Endpoint = new Uri(url); cfg.Protocol = OtlpExportProtocol.HttpProtobuf; - }) - .Build(); + }); + + // Also feed the Aspire dashboard when running under the AppHost (see metrics for details). + if (HasAspireOtlpEndpoint()) + { + tracerProviderBuilder.AddOtlpExporter(); + } + + tracerProviderBuilder.Build(); } + // Aspire injects OTEL_EXPORTER_OTLP_ENDPOINT (plus protocol/headers) into project resources it + // orchestrates. Its absence means we are not running under Aspire (e.g. plain Docker Compose), + // so the dashboard exporter is skipped and only the collector pipeline is used. + static bool HasAspireOtlpEndpoint() => + !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_ENDPOINT")); + const string OtlpMetricsDefaultUrl = "http://localhost:5318/v1/metrics"; const string OtlpTracesDefaultUrl = "http://localhost:5318/v1/traces"; const string OtlpMetricsUrlEnvVar = "OTLP_METRICS_URL"; diff --git a/src/CommonConfigurations/SharedConventions.cs b/src/CommonConfigurations/SharedConventions.cs index 667730d..d1fc9d1 100644 --- a/src/CommonConfigurations/SharedConventions.cs +++ b/src/CommonConfigurations/SharedConventions.cs @@ -20,8 +20,12 @@ public static HostApplicationBuilder ConfigureAzureNServiceBusEndpoint(this Host var endpointConfiguration = new EndpointConfiguration(endpointName); - // Configure Azure Transport - var serviceBusConnectionString = Environment.GetEnvironmentVariable("AZURE_SERVICE_BUS_CONNECTION_STRING"); + // Configure Azure Transport. Prefer the transport connection string provided by the + // Particular Service Platform (when orchestrated by Aspire it is injected as + // ConnectionStrings__transport). Fall back to the legacy env var for the standalone + // docker-compose flow. + var serviceBusConnectionString = builder.Configuration.GetConnectionString("transport") + ?? Environment.GetEnvironmentVariable("AZURE_SERVICE_BUS_CONNECTION_STRING"); ArgumentException.ThrowIfNullOrWhiteSpace(serviceBusConnectionString); From 7248c5ece7510b03b2a96f3629e4b4221ff0b641 Mon Sep 17 00:00:00 2001 From: Rhys Bevilaqua Date: Mon, 22 Jun 2026 14:17:44 +0800 Subject: [PATCH 4/4] Remove completed planning markdown --- apphost_creation_plan.md | 92 ---------------------------------------- 1 file changed, 92 deletions(-) delete mode 100644 apphost_creation_plan.md diff --git a/apphost_creation_plan.md b/apphost_creation_plan.md deleted file mode 100644 index 90b5e7c..0000000 --- a/apphost_creation_plan.md +++ /dev/null @@ -1,92 +0,0 @@ -# Aspire AppHost creation plan (Docker Compose parity) - -## Goal - -- [x] Add a new .NET Aspire AppHost that models the existing **default** `docker-compose.yml` topology (no ASB emulator flow). -- [x] Keep current behavior for local orchestration and publishing so container artifacts can be generated from AppHost for Docker Compose deployments. -- [x] Preserve the existing OpenTelemetry collector pipeline (Prometheus + Jaeger) while also enabling Aspire’s own telemetry/dashboard support. - -## 1) Create AppHost and baseline wiring - -- [x] Add a new project, e.g. `src/AppHost/AppHost.csproj`, and include it in `src/AzureLoanBrokerShowcase.slnx`. -- [x] Add required AppHost packages: - - `Aspire.Hosting.AppHost` - - `Aspire.Hosting.Docker` (for Docker Compose environment publishing) -- [x] In `AppHost/Program.cs`, create the builder with `DistributedApplication.CreateBuilder(args)`. -- [x] Add a Docker Compose compute environment in the app model (`AddDockerComposeEnvironment(...)`) so `aspire publish` can emit compose artifacts. - -## 2) Model infrastructure from `docker-compose.yml` - -- [x] Model the following as container resources in AppHost (same image intent as Compose): - - `sqlserver` - - `creditbureau` - - `otel-collector` - - `prometheus` - - `grafana` - - `jaeger` - - `servicecontrol` - - `servicecontrol-db` - - `servicecontrol-audit` - - `servicecontrol-monitoring` - - `servicepulse` -- [x] Mirror key container settings from Compose: - - ports - - volumes/bind mounts - - env vars and required secrets/placeholders - - startup ordering/dependencies (where relevant) -- [x] Explicitly **exclude** ASB emulator services and related health-check sidecar from AppHost. - -## 3) Model application services and references - -- [x] Add project resources for: - - `loan-broker`, `bank1`, `bank2`, `bank3`, `email-sender`, `client` -- [x] Keep image identity aligned with current publish flow (`loanbroker-azure/*`) so generated artifacts remain compatible with existing conventions. -- [x] Model `creditbureau` either: - - as Dockerfile-based resource matching current `src/CreditBureau/Dockerfile`, or - - as a project resource only if container publish parity is confirmed for the Azure Functions project. -- [x] Wire environment variables currently provided by `env/azure.env` + `env/metrics.env`: - - `AZURE_SERVICE_BUS_CONNECTION_STRING` - - `SQL_CONNECTION_STRING` - - `CREDIT_BUREAU_URL` - - `OTLP_METRICS_URL` - - `OTLP_TRACING_URL` -- [x] Use AppHost parameters/secrets for externalized values (especially Service Bus connection string), so publish output contains resolvable placeholders instead of hard-coded secrets. - -## 4) Telemetry coexistence plan (existing collector + Aspire telemetry) - -- [x] Keep existing `otel-collector` container and its mounted config (`src/otel/otel-collector-config.yaml`). -- [x] Keep endpoint OTLP env vars pointing to collector (`http://otel-collector:5318/...`) so current NServiceBus/OpenTelemetry emission path is unchanged. -- [x] Enable Aspire dashboard support in the Docker Compose environment (do not replace existing collector/Prometheus/Grafana/Jaeger stack). -- [x] Confirm generated Compose output includes both: - - existing observability containers, and - - Aspire dashboard/telemetry resources (if enabled for that environment). - -## 5) Publishing and deployment flow - -- [x] Standardize on Aspire CLI publish flow for AppHost artifacts: - - `aspire publish --apphost src/AppHost/AppHost.csproj` -- [x] Ensure published output includes Docker Compose artifacts (`docker-compose.yml`, `.env` placeholders, related generated files). -- [x] Keep ability to build/push images for project/container resources via Aspire Docker integration (including remote image naming/tag strategy when needed). -- [x] Ensure published artifact structure can replace today’s manual `dotnet publish ... /t:PublishContainer` + `docker compose up` workflow for containerized runs. - -## 6) Documentation and operational updates - -- [x] Update `README.md` with AppHost run/publish instructions (default scenario only; emulator path remains separate or explicitly out of scope). -- [x] Document required parameters/secrets and how to pass them during `aspire publish`. -- [x] Document expected host ports and any AppHost-specific differences from current Compose UX. - -## 7) Suggested implementation order - -- [x] Scaffold AppHost + Docker integration package. -- [x] Add infrastructure containers first (SQL/collector/monitoring/Particular platform). -- [x] Add application project resources and env wiring. -- [x] Add publish customization to preserve container naming and compose parity. -- [x] Update docs and perform parity validation against existing `docker-compose.yml`. - -## References used - -- [x] Aspire AppHost overview: -- [x] Aspire deployment pipeline model: -- [x] Aspire CLI publish: -- [x] Aspire Docker integration (Compose generation/customization): -- [x] Aspire Community Toolkit overview: