From bae19be00e66e5ae5cad73f5666afecac2f21e5a Mon Sep 17 00:00:00 2001 From: emeraldleaf Date: Mon, 15 Jun 2026 19:37:23 -0600 Subject: [PATCH 1/2] docs(audits): log wolverine-asb-emulator audit 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. --- .claude/audits/INDEX.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.claude/audits/INDEX.md b/.claude/audits/INDEX.md index 34136cd8..4c0895fd 100644 --- a/.claude/audits/INDEX.md +++ b/.claude/audits/INDEX.md @@ -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) | From 9777204a650415d066b9f94a66dba0186106de21 Mon Sep 17 00:00:00 2001 From: emeraldleaf Date: Mon, 15 Jun 2026 21:02:24 -0600 Subject: [PATCH 2/2] fix(messaging): correct ASB subscription-listener API + emulator/AutoProvision docs (#148) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CLAUDE.md | 4 ++-- NextAurora.AppHost/AppHost.cs | 25 ++++++++++++++++--------- NotificationService/Program.cs | 9 +++++---- OrderService/Program.cs | 13 +++++++------ PaymentService/Program.cs | 5 +++-- ShippingService/Program.cs | 5 +++-- docs/architecture.md | 8 +++++--- docs/full-saga-deployment-plan.md | 4 ++-- docs/how-it-works.md | 3 +-- 9 files changed, 44 insertions(+), 32 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 0017c083..d35488ea 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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. ## Communication Patterns diff --git a/NextAurora.AppHost/AppHost.cs b/NextAurora.AppHost/AppHost.cs index 5e68a2b0..a049c204 100644 --- a/NextAurora.AppHost/AppHost.cs +++ b/NextAurora.AppHost/AppHost.cs @@ -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 @@ -115,14 +117,19 @@ static IResourceBuilder 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"; // OrderService also references catalogService — that gives it the gRPC client config to call diff --git a/NotificationService/Program.cs b/NotificationService/Program.cs index 148da476..3fd36ac2 100644 --- a/NotificationService/Program.cs +++ b/NotificationService/Program.cs @@ -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)) @@ -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"); diff --git a/OrderService/Program.cs b/OrderService/Program.cs index 55f78897..beed4b89 100644 --- a/OrderService/Program.cs +++ b/OrderService/Program.cs @@ -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)) { @@ -40,8 +41,8 @@ opts.PublishMessage().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. diff --git a/PaymentService/Program.cs b/PaymentService/Program.cs index 4694e39c..07db5ecb 100644 --- a/PaymentService/Program.cs +++ b/PaymentService/Program.cs @@ -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)) @@ -59,7 +60,7 @@ opts.PublishMessage().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. diff --git a/ShippingService/Program.cs b/ShippingService/Program.cs index be6d9f81..859a9990 100644 --- a/ShippingService/Program.cs +++ b/ShippingService/Program.cs @@ -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)) @@ -33,7 +34,7 @@ opts.PublishMessage().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. diff --git a/docs/architecture.md b/docs/architecture.md index cff8f223..057b57f3 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -251,7 +251,7 @@ Azure Service Bus +-- Queue: send-notification -> NotificationService ``` -**Subscription naming convention: `{consumer}-{source-events}-sub`.** Aspire 13 enforces globally unique subscription names within a bus namespace (the per-topic scoping behavior of Aspire 9 was dropped). Including the source-events suffix in the name keeps it readable and unique. The strings here must match the `ListenToAzureServiceBusSubscription("{topic}/{sub}")` calls in each service's `Program.cs`. +**Subscription naming convention: `{consumer}-{source-events}-sub`.** Aspire 13 enforces globally unique subscription names within a bus namespace (the per-topic scoping behavior of Aspire 9 was dropped). Including the source-events suffix in the name keeps it readable and unique. The names here must match the `ListenToAzureServiceBusSubscription("{sub}", c => c.TopicName = "{topic}")` calls in each 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). ### Event Contracts (NextAurora.Contracts) @@ -398,7 +398,9 @@ SellerPortal -> catalog-service, order-service #### Service Bus emulator — Wolverine AutoProvision is disabled locally -Locally the `messaging` resource runs as the Azure Service Bus **emulator** (`.RunAsEmulator()`), which implements only the AMQP data plane — **not** the management/administration HTTP API. Wolverine's `AutoProvision()` (on by default) provisions topics/subscriptions via that management API at host startup, so against the emulator it fails, retries, and the host dies with `BrokerInitializationException: Unable to initialize the Broker asb in time` — a deterministic startup crash for every Wolverine service (CatalogService has no Service Bus, so it's unaffected). The AppHost already declares the full topology (`AddServiceBusTopic` / `AddServiceBusSubscription`), so provisioning is both impossible and redundant locally. The AppHost therefore injects `Wolverine__AutoProvision=false` into the four Wolverine services (Order/Payment/Shipping/Notification); in Publish mode against real Azure the flag stays `true` so provisioning creates the entities. This mirrors the integration-test harnesses' `Wolverine:AutoProvision=false`. See CLAUDE.md "Package Management". +Locally the `messaging` resource runs as the Azure Service Bus **emulator** (`.RunAsEmulator()`). The 2.0.0 emulator *does* expose the management/administration HTTP API (on its `emulatorhealth` endpoint, port 5300) — but its **subscription** admin endpoints return **HTTP 500** (topic and queue endpoints return 200; verified by `curl` and reproduced across runs). Wolverine's `AutoProvision()` (on by default) provisions topics/subscriptions via that management API at host startup, so its per-subscription `SubscriptionExistsAsync`/`CreateSubscriptionAsync` calls hit the 500, retry, and the host dies with `BrokerInitializationException: Unable to initialize the Broker asb in time` — a deterministic startup crash for every Wolverine service (CatalogService has no Service Bus, so it's unaffected). The AppHost already declares the full topology (`AddServiceBusTopic` / `AddServiceBusSubscription` → the emulator's `Config.json`, provisioned at container start), so provisioning is redundant locally. The AppHost therefore injects `Wolverine__AutoProvision=false` into the four Wolverine services (Order/Payment/Shipping/Notification); in Publish mode against real Azure the flag stays `true` so provisioning creates the entities. This mirrors the integration-test harnesses' `Wolverine:AutoProvision=false`. See CLAUDE.md "Package Management". + +> **Why `AutoProvision=false` is sufficient, not just a crash-avoidance hack.** 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 directly over AMQP — which the emulator fully implements — to the subscription `Config.json` already created. So the 500 is never reached and the saga runs locally. The historical "saga won't run locally" symptom was a *separate* bug: the listeners were registered with a combined `"{topic}/{sub}"` string instead of `ListenToAzureServiceBusSubscription("{sub}", c => c.TopicName = "{topic}")`, which silently mis-registers them (and would fail against real Azure too). Both are fixed; tracked in [#148](https://github.com/emeraldleaf/NextAurora/issues/148). End-to-end live verification on this workstation is currently blocked by an unrelated Aspire DCP container-creation timeout on the external-drive-backed Docker, so the fix rests on the decompiled control-flow proof above plus the stubbed-transport integration tests. ### Service Defaults (NextAurora.ServiceDefaults) @@ -604,7 +606,7 @@ Phased plan, smallest blast radius first. Each phase is independently shippable. **Phase 0 — Code swap (~half day, app-level changes only).** No infra yet; just prove the codebase compiles and tests pass against the Wolverine SQS transport. - Bump packages: `WolverineFx.AzureServiceBus` → `WolverineFx.AmazonSqs` in [Directory.Packages.props](../Directory.Packages.props) and the four service Api csprojs. -- In each service's `Program.cs`: `opts.UseAzureServiceBus(connectionString)` → `opts.UseAmazonSqs(...)`. Topic publish: `PublishMessage().ToAzureServiceBusTopic("foo")` → `.ToSnsTopic("foo")`. Subscription listen: `ListenToAzureServiceBusSubscription("foo/bar")` → `ListenToSqsQueue("bar")` (SQS doesn't have the topic-prefix path). +- In each service's `Program.cs`: `opts.UseAzureServiceBus(connectionString)` → `opts.UseAmazonSqs(...)`. Topic publish: `PublishMessage().ToAzureServiceBusTopic("foo")` → `.ToSnsTopic("foo")`. Subscription listen: `ListenToAzureServiceBusSubscription("bar", c => c.TopicName = "foo")` → `ListenToSqsQueue("bar")`. - Handlers, domain entities, DTOs, middleware, the `WolverineEventPublisher` adapter — all unchanged. - Tests stay unit-level (no transport in unit tests). Add at least one integration test using LocalStack to exercise the new transport before going further. diff --git a/docs/full-saga-deployment-plan.md b/docs/full-saga-deployment-plan.md index 81e705f0..189b2ac6 100644 --- a/docs/full-saga-deployment-plan.md +++ b/docs/full-saga-deployment-plan.md @@ -187,8 +187,8 @@ all-Azure) is a lock-in — it's a localized config swap, not a rewrite: opts.UseAzureServiceBus(conn); // ← UseRabbitMq(conn) opts.PublishMessage() .ToAzureServiceBusTopic("payment-events"); // ← .ToRabbitExchange("payment-events") -opts.ListenToAzureServiceBusSubscription("order-events/payment-orders-sub"); - // ← .ListenToRabbitQueue("payment-orders") +opts.ListenToAzureServiceBusSubscription("payment-orders-sub", + c => c.TopicName = "order-events"); // ← .ListenToRabbitQueue("payment-orders") // What does NOT change — transport-agnostic: opts.PersistMessagesWithSqlServer(db, "wolverine"); // outbox = DB concern diff --git a/docs/how-it-works.md b/docs/how-it-works.md index 45047f6b..3b8c790d 100644 --- a/docs/how-it-works.md +++ b/docs/how-it-works.md @@ -395,8 +395,7 @@ Used for the order fulfillment pipeline where immediate response isn't required. **Consuming.** Wolverine subscribes to topics declared in `Program.cs`: ```csharp -opts.ListenToAzureServiceBusSubscription("order-events/payment-orders-sub") - .FromTopic("order-events"); +opts.ListenToAzureServiceBusSubscription("payment-orders-sub", c => c.TopicName = "order-events"); ``` Wolverine then discovers handler classes for the message types and dispatches each incoming envelope to the right one. The pipeline around each consumer is the same as the HTTP-side one: FluentValidation (rare for events) → `ContextPropagationMiddleware` (restores the correlation/user/session scope from envelope headers) → `AutoApplyTransactions` → handler. Idempotency guards inside handlers (status checks, "already processed" lookups) handle Service Bus's at-least-once delivery.