Skip to content

fix(messaging): correct ASB subscription-listener API + emulator/AutoProvision docs (#148)#158

Closed
emeraldleaf wants to merge 2 commits into
mainfrom
feat/asb-emulator-local-saga
Closed

fix(messaging): correct ASB subscription-listener API + emulator/AutoProvision docs (#148)#158
emeraldleaf wants to merge 2 commits into
mainfrom
feat/asb-emulator-local-saga

Conversation

@emeraldleaf

@emeraldleaf emeraldleaf commented Jun 16, 2026

Copy link
Copy Markdown
Owner

What

Two things, both real and prod-relevant:

  1. Subscription-listener API fix (the actual long-standing local-saga blocker). Listeners were registered with a combined "{topic}/{sub}" string:

    opts.ListenToAzureServiceBusSubscription("order-events/payment-orders-sub"); // WRONG

    which silently mis-registers the listener so it never attaches. Corrected to separate args across Order/Payment/Shipping/Notification:

    opts.ListenToAzureServiceBusSubscription("payment-orders-sub", c => c.TopicName = "order-events");

    This would fail against real Azure Service Bus too — not just the emulator.

  2. Doc corrections. CLAUDE.md + architecture.md claimed "the emulator implements only the AMQP data plane, not the management API." That's false. The 2.0.0 emulator does expose the management API (on emulatorhealth, port 5300); only its subscription admin endpoints return HTTP 500 (topics/queues return 200). Corrected in CLAUDE.md, docs/architecture.md (×3), docs/how-it-works.md, docs/full-saga-deployment-plan.md.

Why AutoProvision=false stays (and is sufficient)

Decompiling Wolverine 6.8.0:

internal async ValueTask InitializeAsync(ServiceBusAdministrationClient client, ILogger logger)
{
    if (Parent.AutoProvision)               // ← only path that calls SubscriptionExistsAsync (the 500)
        await SetupAsync(client, logger);
    ...
}

With AutoProvision=false, no management-API call happens — the listener binds over AMQP to the subscription Aspire's Config.json already created, so the subscription-admin 500 is never reached. An earlier "Approach B" commit on this branch re-enabled AutoProvision + injected a management connection string; that reintroduced the 500 and is fully reverted here. Net diff vs main = the API fix + accurate comments.

Test plan

  • dotnet build — clean (0 warnings, TreatWarningsAsErrors on).
  • Stubbed-transport integration tests (Order/Payment/Shipping) continue to cover the saga handlers.
  • ⚠️ Live end-to-end verification deferred — 4 full-stack boots could not complete: Aspire's DCP times out in CreateRenderedResourcesAsync (container creation) on the external-drive-backed Docker, before any service starts. Unrelated to this change. The fix rests on the decompiled control-flow proof above. Re-run once Docker is on faster storage to observe the subscription listeners attach.

Notes

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Corrected Azure Service Bus subscription configuration to properly separate topic and subscription parameters, preventing misconfiguration from combined path strings.
  • Documentation

    • Enhanced guidance for Azure Service Bus subscription setup and naming conventions.
    • Improved documentation of local development behavior with the Service Bus emulator, including AutoProvision configuration and requirements.
    • Updated configuration examples to reflect correct subscription registration patterns.

emeraldleaf and others added 2 commits June 15, 2026 19:37
Official Wolverine emulator recipe confirms feat-branch Approach B and contradicts the 'emulator has no management API' rule in CLAUDE.md:230 + architecture.md:399. Tracked on #148.
…Provision docs (#148)

The local saga never advanced past Placed because subscription listeners were
registered with a combined "{topic}/{sub}" string instead of
ListenToAzureServiceBusSubscription("{sub}", c => c.TopicName = "{topic}"), which
silently mis-registers the listener so it never attaches — and fails against real
Azure too, not just the emulator. Fixed across Order/Payment/Shipping/Notification.

Keep Wolverine AutoProvision=false locally. Decompiling Wolverine 6.8.0 shows
AzureServiceBusSubscription.InitializeAsync calls the management-API path
(SetupAsync -> SubscriptionExistsAsync) ONLY when AutoProvision is true; with it
false the listener binds over AMQP with no management call, so the emulator's
subscription-admin HTTP 500 is never reached. An earlier "Approach B" attempt on this
branch re-enabled AutoProvision + injected a management connection string — reverted
here, because it reintroduced the 500.

Correct the docs that claimed the emulator has no management API: it does (on the
emulatorhealth endpoint, port 5300), but its subscription admin endpoints return 500
while topics/queues return 200. Updated CLAUDE.md, docs/architecture.md,
docs/how-it-works.md, docs/full-saga-deployment-plan.md.

Live end-to-end verification is currently blocked by an Aspire DCP container-creation
timeout on the external-drive-backed Docker (unrelated to this change); the fix rests
on the decompiled control-flow proof plus the stubbed-transport integration tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@codecov

codecov Bot commented Jun 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 72.22222% with 5 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
NotificationService/Program.cs 0.00% 5 Missing ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Corrects ListenToAzureServiceBusSubscription calls across all four services, replacing the previously used combined "topic/subscription" string with separate subscription name and explicit TopicName callback arguments. Updates inline comments in each service and in AppHost.cs to explain that the Service Bus emulator's management API returns HTTP 500 for per-subscription admin endpoints, requiring Wolverine__AutoProvision=false in local dev. Matching documentation in CLAUDE.md, architecture.md, how-it-works.md, and full-saga-deployment-plan.md is updated accordingly.

Changes

Azure Service Bus Subscription Wiring Fix

Layer / File(s) Summary
Subscription listener API fix across services
NotificationService/Program.cs, OrderService/Program.cs, PaymentService/Program.cs, ShippingService/Program.cs
Each service replaces the combined "topic/subscription" string in ListenToAzureServiceBusSubscription with a separate subscription name argument and explicit TopicName assignment in a configuration callback. AutoProvision inline comments are expanded in each file with emulator HTTP 500 management endpoint behavior.
AppHost and CLAUDE.md rule updates
NextAurora.AppHost/AppHost.cs, CLAUDE.md
AppHost.cs comments are rewritten to specify subscription and topic names as separate args and to describe the emulator's partial management API causing startup crashes. CLAUDE.md rules are updated with the same subscription naming convention, emulator fallback guidance, and Wolverine__AutoProvision=false remediation.
Documentation updates
docs/architecture.md, docs/full-saga-deployment-plan.md, docs/how-it-works.md, .claude/audits/INDEX.md
architecture.md corrects the subscription listener contract, expands the emulator AutoProvision rationale, and fixes the AWS migration Phase 0 example. full-saga-deployment-plan.md and how-it-works.md update their code snippets to the corrected API form. The audit index records the article-audit outcome.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • emeraldleaf/NextAurora#146: Directly related — both PRs address Wolverine AutoProvision incompatibility with the Service Bus emulator, with the retrieved PR introducing the Wolverine__AutoProvision=false AppHost-based disabling that this PR documents and corrects in service-level wiring.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: fixing the Azure Service Bus subscription-listener API and correcting AutoProvision/emulator documentation. It is specific, concise, and directly reflects the core issues addressed in the changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/asb-emulator-local-saga

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with 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.

Inline comments:
In `@CLAUDE.md`:
- Around line 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.

In `@NextAurora.AppHost/AppHost.cs`:
- Around line 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.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 5831fe78-bb5f-45d9-8964-a778d11fa9eb

📥 Commits

Reviewing files that changed from the base of the PR and between 4f87c68 and 9777204.

📒 Files selected for processing (10)
  • .claude/audits/INDEX.md
  • CLAUDE.md
  • NextAurora.AppHost/AppHost.cs
  • NotificationService/Program.cs
  • OrderService/Program.cs
  • PaymentService/Program.cs
  • ShippingService/Program.cs
  • docs/architecture.md
  • docs/full-saga-deployment-plan.md
  • docs/how-it-works.md

Comment thread CLAUDE.md
Comment on lines +227 to 231
- **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.

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

Comment on lines +120 to 133
// 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";

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.

@emeraldleaf

Copy link
Copy Markdown
Owner Author

Correction — live testing revealed the fix is necessary but NOT sufficient (and I'd over-claimed)

A live end-to-end run (stack actually booted once the Docker VM had enough RAM) showed the order stays at Placed — the saga does not advance with just the API fix + AutoProvision=false. Service logs showed:

  • PaymentService's order-events/payment-orders-sub listener never starts.
  • A flood of MessagingEntityNotFound for Wolverine's system queues (wolverine.retries.*, wolverine.response.*) — they aren't in Aspire's Config.json, and AutoProvision=false means Wolverine never creates them, so the transport degrades.

So the emulator has a dual-bind: AutoProvision=true → subscription-admin 500; AutoProvision=false → system queues missing. This is why the saga had never actually run locally (integration tests stub the transport).

Per the official Wolverine guidance for permission-limited brokers, the fix is SystemQueuesAreEnabled(false) (pub/sub works without the system queues). I've added it (AutoProvision off ⇒ system queues off) and am verifying live now. I will only update this PR to claim "saga runs locally" once an order actually advances past Placed.

The subscription-listener API fix and the "emulator has a management API" doc correction remain valid regardless.

@emeraldleaf

Copy link
Copy Markdown
Owner Author

Superseded by #159. The ASB subscription-listener API fix here is carried into #159, but the headline goal shifted: rather than fix the ASB emulator (which has upstream limitations — subscription admin 500 + non-provisionable system queues), #159 makes the transport config-selectable with RabbitMQ as the default, which runs the full saga locally (verified: order → Shipped) and matches the deployed broker. ASB stays opt-in for Azure. Closing this in favor of #159.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant