Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/audits/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,4 @@ Persistent log of every `/article-audit` run. One row per audit; full reasoning
| 2026-06-12 | [The Async/Await Habit That Quietly Slows ASP.NET Core Under Load](2026-06-12-async-await-habits-under-load.md) (Kerim Kara, Activated Thinker) | ✅ All five teaser categories encoded, mostly more rigorously (build-time bans + hooks vs advice); paywall-limited — intro only, re-audit on paste | No action |
| 2026-06-12 | [.NET Microservices Guide — What To Do and What To Avoid](2026-06-12-dotnet-microservices-do-avoid.md) (Kerim Kara, Real World .NET) | ⚙️ Re-audited after user pasted full body (initial pass paywall-limited). All 10 do/avoid items covered; article's own examples violate the canon 3× (publish-after-save dual-write vs outbox; ValidateAudience=false vs JWT rule; IOrderService layer vs VSA). Recurring implicit stance: internal-gRPC trust + no-gateway undocumented (2nd hit today) | Offered small encoding issue (internal-trust + no-gateway triggers); pending user decision |
| 2026-06-12 | [Building Dapr Workflows in .NET With Aspire](2026-06-12-dapr-workflows-aspire.md) (Milan Jovanović, The .NET Weekly) | ✅ Considered + rejected with depth — project-decisions.md §22 ("Dapr — considered, not adopted") + architecture.md:283 (choreography saga, no central orchestrator) + perf doc already names Temporal/Durable Functions/Step Functions as the durable-execution alternatives. 202-Accepted + idempotency + outbox all encoded | No new issue — §22's table maps the classic 5 building blocks but not **Dapr Workflow**; fold a Workflow row + orchestration-vs-choreography trigger into open #86 |
| 2026-06-15 | [Wolverine + Azure Service Bus Emulator](2026-06-15-wolverine-asb-emulator.md) (WolverineFx official docs) | ⚙️ Official recipe (`UseAzureServiceBus` + `transport.ManagementConnectionString` on :5300 + AutoProvision) matches in-flight feat-branch fix and **contradicts two `main` rules** — CLAUDE.md:230 + architecture.md:399 claim "emulator has no management API" (false; it does, only subscription admin returns 500). Also confirms the `ListenToAzureServiceBusSubscription(sub, c => c.TopicName=topic)` API vs our wrong `"{topic}/{sub}"` form | No new issue — tracked on #148; doc corrections land with that branch. Confirms feat-branch "Approach B" is officially correct; subscription-admin-500 limitation is ours to document (upstream omits it) |
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,10 +224,10 @@ This rule is for humans, AI assistants, and future-you. Don't wait to be asked.
- Individual `.csproj` files reference packages WITHOUT version attributes
- Shared build settings in `Directory.Build.props`
- **Aspire SDK and runtime packages must match — including minor versions.** The `Aspire.AppHost.Sdk/X.Y.Z` declared in `NextAurora.AppHost.csproj` and the `Aspire.Hosting.*` package versions in `Directory.Packages.props` need to match exactly (or the SDK ≥ packages). Major mismatches surface at *build/startup* as `TypeLoadException` (internal types like `PublishingContext`). Minor mismatches surface at *runtime* as DCP rejecting startup with `Newer version of the Aspire.Hosting.AppHost package is required`. Bump SDK and packages together as one change.
- **Service Bus subscription names are globally unique within the namespace** (Aspire 13+). Don't reuse the same subscription name on different topics — `DistributedApplicationException` at AppHost startup. Convention: `{consumer}-{source-events}-sub` (e.g. `notify-orders-sub`, `notify-payments-sub`). When adding a new subscription in `AppHost.cs`, also update the matching `ListenToAzureServiceBusSubscription("{topic}/{sub}")` string in the consuming service's `Program.cs`.
- **Service Bus subscription names are globally unique within the namespace** (Aspire 13+). Don't reuse the same subscription name on different topics — `DistributedApplicationException` at AppHost startup. Convention: `{consumer}-{source-events}-sub` (e.g. `notify-orders-sub`, `notify-payments-sub`). When adding a new subscription in `AppHost.cs`, also update the matching `ListenToAzureServiceBusSubscription("{sub}", c => c.TopicName = "{topic}")` call in the consuming service's `Program.cs` — subscription name and topic are SEPARATE arguments, NOT a combined `"{topic}/{sub}"` string (that form silently mis-registers the listener so it never attaches, and fails against real Azure too, not just the emulator).
- **Aspire 13+ Azure resources need explicit local-dev fallbacks.** `AddAzureServiceBus` requires a chained `.RunAsEmulator()` for local runs (the implicit emulator behavior from Aspire 9 is gone). `AddAzureApplicationInsights` has no local emulator at all — gate it on `builder.ExecutionContext.IsPublishMode` and skip in dev. Without these, AppHost's resource pane shows "Missing subscription configuration" and every service that `WithReference`s the resource fails to start.
- **`WithReference(x)` ≠ wait-for-healthy in Aspire 13.** `WithReference` only injects connection strings / endpoints; the service starts as soon as its env vars are resolvable, even if the target is still warming up. Containers like the Service Bus emulator take 30-60s to be healthy on first run; services that race past that crash with "connection refused" and exit. **Hard rule: every `WithReference` on a non-trivial dependency (DB, messaging, identity, peer service) gets a matching `.WaitFor(x)`.** Without it, the Aspire dashboard shows services as "Finished" instead of "Running" because they exited before infra was ready.
- **Wolverine `AutoProvision()` is incompatible with the Service Bus emulator — disable it for local dev.** `AutoProvision()` (default on; `Wolverine:AutoProvision` true) provisions topics/subscriptions at startup via the Service Bus **management API** (`ServiceBusAdministrationClient`). The **emulator implements only the AMQP data plane, not the management API**, so every check fails, retries 4×, and the host dies with `BrokerInitializationException: Unable to initialize the Broker asb in time` — a sub-second, deterministic startup crash hitting *every* Wolverine service (catalog has no ASB, so it survives, which makes it look selective). Locally the topology is already declared in `AppHost.cs` (`AddServiceBusTopic`/`AddServiceBusSubscription` write the emulator's config), so provisioning is impossible *and* redundant. **Fix: AppHost injects `Wolverine__AutoProvision=false` into each Wolverine service** (mirrors the integration-test harnesses); in Publish mode against real Azure it stays on to create entities. See [docs/architecture.md](docs/architecture.md) Infrastructure section.
- **Wolverine `AutoProvision()` is incompatible with the Service Bus emulator — disable it for local dev.** `AutoProvision()` (default on; `Wolverine:AutoProvision` true) provisions topics/subscriptions at startup via the Service Bus **management API** (`ServiceBusAdministrationClient`). The 2.0.0 emulator *does* implement that API (on its `emulatorhealth` HTTP endpoint, port 5300), but its **subscription admin endpoints return HTTP 500** (topics and queues return 200), so AutoProvision's per-subscription check fails, retries 4×, and the host dies with `BrokerInitializationException: Unable to initialize the Broker asb in time` — a sub-second, deterministic startup crash hitting *every* Wolverine service (catalog has no ASB, so it survives, which makes it look selective). Locally the topology is already declared in `AppHost.cs` (`AddServiceBusTopic`/`AddServiceBusSubscription` write the emulator's `Config.json`, which it provisions at container start), so provisioning is redundant — and with `AutoProvision=false` the subscription **listener binds directly over AMQP with no management call** (decompiled: `AzureServiceBusSubscription.InitializeAsync` only hits the 500-ing path when `AutoProvision` is true). **Fix: AppHost injects `Wolverine__AutoProvision=false` into each Wolverine service** (mirrors the integration-test harnesses); in Publish mode against real Azure it stays on to create entities. See [docs/architecture.md](docs/architecture.md) Infrastructure section.

Comment on lines +227 to 231

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

CLAUDE.md size now blocks CI merge.

BUILD_AND_TEST / build is failing because this file crossed the 500-line hard cap. Move detailed rationale to a paired doc/skill and keep only concise rule headlines + links here.

As per coding guidelines, CLAUDE.md must stay lean and CI enforces a hard fail at 500 lines.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CLAUDE.md` around lines 227 - 231, CLAUDE.md exceeds the 500-line hard cap
which is blocking the CI build. Refactor by moving the four detailed bullet
points about Azure Aspire configuration (Service Bus subscription naming
conventions, Aspire 13+ resource fallbacks, WithReference behavior with WaitFor
requirements, and Wolverine AutoProvision incompatibility with the emulator) out
of CLAUDE.md and into a paired documentation file in the docs/ folder (e.g.,
docs/aspire-setup.md or similar). Replace the detailed explanations in CLAUDE.md
with concise rule headlines followed by links to the new paired doc, similar to
how the existing content references docs/architecture.md. This keeps CLAUDE.md
lean and scannable while preserving the detailed rationale in dedicated skill
documentation.

Sources: Coding guidelines, Pipeline failures

## Communication Patterns

Expand Down
25 changes: 16 additions & 9 deletions NextAurora.AppHost/AppHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,10 @@
//
// Subscription naming: `{consumer}-{source-events}-sub`. Aspire 13 requires subscription names
// to be globally unique within the bus namespace (not scoped per topic), hence the source
// suffix. The strings here must match the `ListenToAzureServiceBusSubscription("{topic}/{sub}")`
// calls in each service's Program.cs.
// suffix. The names here must match the
// `ListenToAzureServiceBusSubscription("{sub}", c => c.TopicName = "{topic}")` calls in each
// service's Program.cs — the subscription name and topic are SEPARATE args, NOT a combined
// "{topic}/{sub}" string (that form silently mis-registers the listener). See CLAUDE.md + #148.
var orderEventsTopic = serviceBus.AddServiceBusTopic("order-events");
orderEventsTopic.AddServiceBusSubscription("payment-orders-sub"); // PaymentService consumes
orderEventsTopic.AddServiceBusSubscription("notify-orders-sub"); // NotificationService consumes
Expand Down Expand Up @@ -115,14 +117,19 @@ static IResourceBuilder<ProjectResource> WithOptionalAppInsights(
.WithEnvironment("Frontend__AllowedOrigins", SpaDevOrigin);

// Wolverine's AutoProvision() uses the Service Bus *management* API (ServiceBusAdministrationClient)
// to create/verify topics + subscriptions at host startup. The Service Bus emulator does NOT
// implement the management API — only the AMQP data plane — so AutoProvision retries, times out,
// and the host dies with `BrokerInitializationException: Unable to initialize the Broker asb in
// time`. Locally the topology is already declared above (AddServiceBusTopic/AddServiceBusSubscription
// write the emulator's config), so provisioning is both impossible AND redundant. Disable it for
// the four Wolverine services in dev; in Publish mode against real Azure, AutoProvision stays on
// to create/verify topics + subscriptions at host startup. The 2.0.0 emulator DOES implement the
// management API (on its 'emulatorhealth' HTTP endpoint, port 5300) — but its SUBSCRIPTION admin
// endpoints return HTTP 500 (topics + queues return 200). So AutoProvision's per-subscription
// SubscriptionExistsAsync/CreateSubscriptionAsync calls fail and the host dies with
// `BrokerInitializationException: Unable to initialize the Broker asb in time`. Locally the topology
// is already declared above (AddServiceBusTopic/AddServiceBusSubscription write the emulator's
// Config.json, which the emulator provisions at container start), and Wolverine's listener attaches
// over AMQP — which works — so provisioning is both impossible AND redundant. (Decompiled proof:
// AzureServiceBusSubscription.InitializeAsync only calls the 500-ing SetupAsync when AutoProvision
// is true; with it false, the listener binds over AMQP with no management call.) Disable it for the
// four Wolverine services in dev; in Publish mode against real Azure, AutoProvision stays on
// (Wolverine:AutoProvision defaults true) to create the entities. Mirrors the test harnesses'
// `Wolverine:AutoProvision=false`. See CLAUDE.md.
// `Wolverine:AutoProvision=false`. See CLAUDE.md + #148.
const string disableAutoProvision = "Wolverine__AutoProvision";
Comment on lines +120 to 133

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Publish-mode AutoProvision behavior is contradicted by current env injection.

Line 130 says AutoProvision stays enabled in publish mode, but Lines 144, 151, 159, and 165 set Wolverine__AutoProvision=false unconditionally. That keeps AutoProvision disabled even for publish runs via AppHost.

Suggested fix
 const string disableAutoProvision = "Wolverine__AutoProvision";
+var autoProvisionValue = builder.ExecutionContext.IsPublishMode ? "true" : "false";
@@
     .WithReference(realm, configurationPrefix: keycloakConfigPrefix).WaitFor(realm)
     .WithEnvironment("Frontend__AllowedOrigins", SpaDevOrigin)
-    .WithEnvironment(disableAutoProvision, "false");
+    .WithEnvironment(disableAutoProvision, autoProvisionValue);
@@
     .WithReference(realm, configurationPrefix: keycloakConfigPrefix).WaitFor(realm)
-    .WithEnvironment(disableAutoProvision, "false");
+    .WithEnvironment(disableAutoProvision, autoProvisionValue);
@@
     .WithReference(realm, configurationPrefix: keycloakConfigPrefix).WaitFor(realm)
     .WithEnvironment("Frontend__AllowedOrigins", SpaDevOrigin)
-    .WithEnvironment(disableAutoProvision, "false");
+    .WithEnvironment(disableAutoProvision, autoProvisionValue);
@@
 WithOptionalAppInsights(
     builder.AddProject<Projects.NotificationService>("notification-service")
         .WithReference(serviceBus).WaitFor(serviceBus), appInsights)
-    .WithEnvironment(disableAutoProvision, "false");
+    .WithEnvironment(disableAutoProvision, autoProvisionValue);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@NextAurora.AppHost/AppHost.cs` around lines 120 - 133, The code
unconditionally disables AutoProvision across all environments by setting
`Wolverine__AutoProvision=false` at four locations, contradicting the documented
intent that AutoProvision should remain enabled in publish mode. Modify the four
environment variable assignments where `disableAutoProvision` is used (in the
Wolverine service configurations) to only apply the false value when building
for dev/emulator mode. For publish mode builds, either omit the setting to allow
the Wolverine default of true, or explicitly set `Wolverine__AutoProvision=true`
to ensure AutoProvision is enabled in production against real Azure Service Bus.


// OrderService also references catalogService — that gives it the gRPC client config to call
Expand Down
9 changes: 5 additions & 4 deletions NotificationService/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@

// AutoProvision creates topics/subscriptions via the Service Bus management API at host
// startup. Disabled in two environments: integration tests (fake ASB string hangs) and
// local dev (the emulator has no management API → BrokerInitializationException). The
// local dev (the emulator's SUBSCRIPTION admin endpoints return HTTP 500 → AutoProvision
// dies with BrokerInitializationException; the listener binds over AMQP without it). The
// AppHost injects Wolverine__AutoProvision=false for the emulator. Gate on a config flag
// (defaults true) so real Azure still provisions. See OrderService/Program.cs + CLAUDE.md.
if (builder.Configuration.GetValue("Wolverine:AutoProvision", defaultValue: true))
Expand All @@ -27,9 +28,9 @@
}

// Listen to events from other services
opts.ListenToAzureServiceBusSubscription("order-events/notify-orders-sub");
opts.ListenToAzureServiceBusSubscription("payment-events/notify-payments-sub");
opts.ListenToAzureServiceBusSubscription("shipping-events/notify-shipping-sub");
opts.ListenToAzureServiceBusSubscription("notify-orders-sub", c => c.TopicName = "order-events");
opts.ListenToAzureServiceBusSubscription("notify-payments-sub", c => c.TopicName = "payment-events");
opts.ListenToAzureServiceBusSubscription("notify-shipping-sub", c => c.TopicName = "shipping-events");

// Listen to direct command queue
opts.ListenToAzureServiceBusQueue("send-notification");
Expand Down
13 changes: 7 additions & 6 deletions OrderService/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,11 @@
// configured endpoint. Two environments must disable it:
// 1. Integration tests use a fake ASB connection string, so provisioning hangs/times out
// (it fires before DisableAllExternalWolverineTransports() takes effect).
// 2. Local dev (Aspire) uses the Service Bus emulator, which implements only the AMQP data
// plane — NOT the management API — so AutoProvision retries 4× and the host dies with
// BrokerInitializationException. The AppHost declares the topology and injects
// Wolverine__AutoProvision=false for each Wolverine service.
// 2. Local dev (Aspire) uses the Service Bus emulator, whose SUBSCRIPTION admin endpoints
// return HTTP 500 (topics + queues return 200) — so AutoProvision retries 4× and the host
// dies with BrokerInitializationException. The AppHost declares the topology (the emulator
// provisions it from Config.json at container start) and injects Wolverine__AutoProvision=
// false; the listener then binds over AMQP with no management call. See #148.
// Gate on a config flag (defaults true) so real Azure / Publish mode still provisions. See CLAUDE.md.
if (builder.Configuration.GetValue("Wolverine:AutoProvision", defaultValue: true))
{
Expand All @@ -40,8 +41,8 @@
opts.PublishMessage<OrderPlacedEvent>().ToAzureServiceBusTopic("order-events");

// Listen to incoming events from other services
opts.ListenToAzureServiceBusSubscription("payment-events/order-payments-sub");
opts.ListenToAzureServiceBusSubscription("shipping-events/order-shipping-sub");
opts.ListenToAzureServiceBusSubscription("order-payments-sub", c => c.TopicName = "payment-events");
opts.ListenToAzureServiceBusSubscription("order-shipping-sub", c => c.TopicName = "shipping-events");

// Transactional outbox: persist outgoing messages to SQL Server in the same
// transaction as the entity write, then dispatch via background flush.
Expand Down
5 changes: 3 additions & 2 deletions PaymentService/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@

// AutoProvision creates topics/subscriptions via the Service Bus management API at host
// startup. Disabled in two environments: integration tests (fake ASB string hangs) and
// local dev (the emulator has no management API → BrokerInitializationException). The
// local dev (the emulator's SUBSCRIPTION admin endpoints return HTTP 500 → AutoProvision
// dies with BrokerInitializationException; the listener binds over AMQP without it). The
// AppHost injects Wolverine__AutoProvision=false for the emulator. Gate on a config flag
// (defaults true) so real Azure still provisions. See OrderService/Program.cs + CLAUDE.md.
if (builder.Configuration.GetValue("Wolverine:AutoProvision", defaultValue: true))
Expand All @@ -59,7 +60,7 @@
opts.PublishMessage<PaymentFailedEvent>().ToAzureServiceBusTopic("payment-events");

// Listen to incoming events from other services
opts.ListenToAzureServiceBusSubscription("order-events/payment-orders-sub");
opts.ListenToAzureServiceBusSubscription("payment-orders-sub", c => c.TopicName = "order-events");

// Transactional outbox: persist outgoing messages to SQL Server in the same
// transaction as the entity write, then dispatch via background flush.
Expand Down
5 changes: 3 additions & 2 deletions ShippingService/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@

// AutoProvision creates topics/subscriptions via the Service Bus management API at host
// startup. Disabled in two environments: integration tests (fake ASB string hangs) and
// local dev (the emulator has no management API → BrokerInitializationException). The
// local dev (the emulator's SUBSCRIPTION admin endpoints return HTTP 500 → AutoProvision
// dies with BrokerInitializationException; the listener binds over AMQP without it). The
// AppHost injects Wolverine__AutoProvision=false for the emulator. Gate on a config flag
// (defaults true) so real Azure still provisions. See OrderService/Program.cs + CLAUDE.md.
if (builder.Configuration.GetValue("Wolverine:AutoProvision", defaultValue: true))
Expand All @@ -33,7 +34,7 @@
opts.PublishMessage<ShipmentDispatchedEvent>().ToAzureServiceBusTopic("shipping-events");

// Listen to incoming events from other services
opts.ListenToAzureServiceBusSubscription("payment-events/shipping-payments-sub");
opts.ListenToAzureServiceBusSubscription("shipping-payments-sub", c => c.TopicName = "payment-events");

// Transactional outbox: persist outgoing messages to Postgres in the same
// transaction as the entity write, then dispatch via background flush.
Expand Down
Loading
Loading