Repository navigation
⭐ Aspire Orchestration
.NET Aspire is the orchestration layer that boots and wires together every resource in GridPulse's six-service pipeline from a single dotnet run — this page walks through what src/AppHost/AppHost.cs actually declares and why.
AppHost.cs declares 12 resources total, all visible (and individually restartable/stoppable) in the Aspire dashboard's Resources view:
-
postgres— one Postgres container (WithDataVolume(), so data survives restarts) hosting three logical databases:gridpulsedb,accountsdb, andgridoperationsdb -
kafka— one Kafka container (WithDataVolume()) plus a Kafka UI sidecar (WithKafkaUI()) for inspecting topics/consumer groups directly -
schema-registry— a rawconfluentinc/cp-schema-registrycontainer (no first-party Aspire integration exists for it), wired to Kafka's own internal broker endpoint and given an HTTP health check against/subjects -
account-customer,usage-aggregation,billing— three of the .NET projects, eachAddProject<...>referencing whichever of Postgres/Kafka/schema-registry it actually needs -
grid-operations— a fourth .NET project, with its owngridoperationsdb; depends on Kafka, schema-registry, andaccount-customer(it resolves each detected anomaly's real street address via a live HTTP call, the same "needsaccount-customerlive at runtime" shapemeter-simulatoralready has) -
meter-simulator— the fifth .NET project; depends on Kafka, schema-registry, andaccount-customer(it resolves real account IDs for each simulated meter at startup) -
notification-service— the one non-.NET resource, added viaAddNodeApp(...).WithYarn().WithRunScript("start")rather thanAddProject<...>, since it's the TypeScript/Node service from Phase 3
Every .WithReference(x) is paired with a .WaitFor(x) — WithReference alone only injects connection info (a connection string, a discovered URL); it does not make one resource wait for another to actually be ready. Without WaitFor, usage-aggregation could start and immediately fail its first dbContext.Database.Migrate() call because gridpulsedb isn't accepting connections yet. The graph this produces: postgres/kafka at the root, schema-registry waits for kafka, account-customer waits for accountsdb, grid-operations waits for gridoperationsdb, and usage-aggregation/billing/grid-operations/meter-simulator/notification-service all wait for kafka + schema-registry (grid-operations, meter-simulator, and notification-service additionally wait for account-customer, since all three need it live at runtime — one to resolve a quiet meter's real street address, one to resolve account IDs, the other to look up webhook URLs).
.WithHttpHealthCheck(path: "/health", endpointName: "http") on account-customer, usage-aggregation, billing, and grid-operations (added as each service gained real HTTP endpoints worth verifying — ESPI for Usage Aggregation, the Stripe webhook for Billing, the work-order/outage API for Grid Operations) means Aspire polls each service's actual /health endpoint, not just "is the process running" — a service that starts but can't reach its own dependencies shows unhealthy rather than a false-positive green. schema-registry's /subjects health check works the same way for the one container-based resource with real request/response semantics to check.
streetName/startingAddress/buildingsPerSide/city/zipCode/notificationWebhookUrl are all builder.AddParameter(...) calls with no default values — Aspire resolves each one from this project's dotnet user-secrets store at development time (never committed; see the "Meter Simulator configuration" decision in docs/architecture.md for the full fail-fast reasoning). stripe-secret-key/stripe-webhook-secret are two more, marked secret: true so Aspire masks them in the dashboard, sourced the same way. AppHost itself refuses to start if any are missing, one layer up from MeterSimulator's own equivalent fail-fast validation.
dotnet run (or scripts/run-app.ps1) prints a https://localhost:17038/login?t=... URL — the Aspire dashboard, a web UI (not a GridPulse-authored page; it ships with Aspire itself) showing every resource's live state, structured logs, distributed traces, and metrics across all 12 resources in one place, correlated by the OpenTelemetry instrumentation ServiceDefaults wires into every .NET project. The Resources tab is the fastest way to confirm the whole stack is actually healthy (not just "the terminal didn't print an error") during live verification.
AppHost.cs itself (not just scripts/stop-app.ps1) registers an IHostApplicationLifetime.ApplicationStopping callback that shells out to stop-app.ps1 on any graceful stop, regardless of how the AppHost was started (Visual Studio, dotnet run, aspire run, or the script) — because AppHost is the one process supervising every other resource's lifecycle, it's the natural place for this to live rather than depending on whichever tool happened to launch it. A forceful kill bypasses this by OS design; stop-app.ps1 stays available to run manually as a safety net for that case. Full reasoning in todo.md.