Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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) |
5 changes: 2 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,10 +224,9 @@ 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`.
- **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.
- **Aspire 13+ Azure resources need explicit local-dev fallbacks.** `AddAzureApplicationInsights` has no local emulator — gate it on `builder.ExecutionContext.IsPublishMode` and skip in dev. Without that, AppHost's resource pane shows "Missing subscription configuration" and every service that `WithReference`s it 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.
- **Messaging transport is RabbitMQ** (every environment — local dev, CI, and the Hetzner deployment, so dev matches prod). Wolverine maps the saga's pub/sub onto **fanout exchanges** (one per event family: `order-events`, `payment-events`, `shipping-events`) with a **queue per consumer** bound to each; `AutoProvision()` declares them against the live broker, gated by `Wolverine:AutoProvision` (default on; off in integration tests, which stub the transport via `DisableAllExternalWolverineTransports()` + a fake `amqp://` connection string). The full saga flows locally — verified end-to-end (place an order → reaches `Shipped` in seconds). The transport is swappable (Wolverine abstracts it — a ~5-line block per service); **Azure Service Bus was evaluated and removed** — its local emulator can't run the saga (subscription admin returns HTTP 500 *and* Wolverine's system queues can't auto-provision against it) and there's no Azure deployment today, so carrying a second wiring earned nothing. Re-add ASB only if Azure becomes a real target. Full story: [docs/full-saga-deployment-plan.md](docs/full-saga-deployment-plan.md) (D3) + #148.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

## Communication Patterns

Expand Down
4 changes: 2 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
StreamJsonRpc ships with MessagePack >= 2.5.301. -->
<PackageVersion Include="MessagePack" Version="2.5.301" />
<PackageVersion Include="Aspire.Hosting.Azure.ApplicationInsights" Version="13.3.0" />
<PackageVersion Include="Aspire.Hosting.Azure.ServiceBus" Version="13.3.0" />
<PackageVersion Include="Aspire.Hosting.PostgreSQL" Version="13.3.0" />
<PackageVersion Include="Aspire.Hosting.RabbitMQ" Version="13.3.0" />
Comment thread
coderabbitai[bot] marked this conversation as resolved.
<PackageVersion Include="Aspire.Hosting.Redis" Version="13.3.0" />
<PackageVersion Include="Aspire.Hosting.SqlServer" Version="13.3.0" />

Expand All @@ -36,7 +36,7 @@
ServiceDefaults so it flows to every Wolverine service. Production alternative: pre-generated
static codegen (TypeLoadMode.Static) — deferred. See https://wolverinefx.net/guide/codegen.html. -->
<PackageVersion Include="WolverineFx.RuntimeCompilation" Version="6.8.0" />
<PackageVersion Include="WolverineFx.AzureServiceBus" Version="6.8.0" />
<PackageVersion Include="WolverineFx.RabbitMQ" Version="6.8.0" />
<PackageVersion Include="WolverineFx.EntityFrameworkCore" Version="6.8.0" />
<PackageVersion Include="WolverineFx.FluentValidation" Version="6.8.0" />
<PackageVersion Include="WolverineFx.SqlServer" Version="6.8.0" />
Expand Down
83 changes: 25 additions & 58 deletions NextAurora.AppHost/AppHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,36 +34,14 @@
// once we wire it (architecture.md "Future Considerations").
var redis = builder.AddRedis("cache");

// Azure Service Bus — Aspire 13 requires explicit `.RunAsEmulator()` for local dev (in 9.x
// the emulator was implicit). Without this, AppHost reports "Missing subscription configuration"
// at startup because Aspire treats the resource as needing a real Azure subscription.
// See CLAUDE.md.
var serviceBus = builder.AddAzureServiceBus("messaging")
.RunAsEmulator();

// Topic / subscription topology. Each service that publishes events owns a topic; subscribers
// get their own subscription per topic so they can be scaled and dead-lettered independently.
//
// 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.
var orderEventsTopic = serviceBus.AddServiceBusTopic("order-events");
orderEventsTopic.AddServiceBusSubscription("payment-orders-sub"); // PaymentService consumes
orderEventsTopic.AddServiceBusSubscription("notify-orders-sub"); // NotificationService consumes

var paymentEventsTopic = serviceBus.AddServiceBusTopic("payment-events");
paymentEventsTopic.AddServiceBusSubscription("order-payments-sub"); // OrderService consumes
paymentEventsTopic.AddServiceBusSubscription("shipping-payments-sub"); // ShippingService consumes
paymentEventsTopic.AddServiceBusSubscription("notify-payments-sub"); // NotificationService consumes (failure notifications)

var shippingEventsTopic = serviceBus.AddServiceBusTopic("shipping-events");
shippingEventsTopic.AddServiceBusSubscription("order-shipping-sub"); // OrderService consumes
shippingEventsTopic.AddServiceBusSubscription("notify-shipping-sub"); // NotificationService consumes

// Direct queue (not topic) for "send a notification right now" requests — single consumer,
// fan-in only. NotificationService listens here in addition to the topic subscriptions.
serviceBus.AddServiceBusQueue("send-notification");
// --- Messaging broker: RabbitMQ ---
// One container + the management UI (a demo artifact at :15672). Wolverine declares the
// exchanges/queues/bindings itself via AutoProvision against the live broker, so no topology is
// hand-declared here. RabbitMQ is also the deployed broker (Hetzner) — dev/prod parity. The
// connection string is exposed under "messaging". The transport could be swapped to Azure Service
// Bus later (~5-line Wolverine block per service); it was removed because the ASB emulator can't run
// the saga locally and there's no Azure deployment today. See CLAUDE.md + docs/full-saga-deployment-plan.md (D3) + #148.
var messaging = builder.AddRabbitMQ("messaging").WithManagementPlugin();
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Application Insights only when running in Publish mode (i.e. real deploys to Azure).
// Aspire 13 has no local emulator for App Insights — keeping this in for local dev causes
Expand Down Expand Up @@ -114,48 +92,37 @@ static IResourceBuilder<ProjectResource> WithOptionalAppInsights(
.WithReference(realm, configurationPrefix: keycloakConfigPrefix).WaitFor(realm)
.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
// (Wolverine:AutoProvision defaults true) to create the entities. Mirrors the test harnesses'
// `Wolverine:AutoProvision=false`. See CLAUDE.md.
const string disableAutoProvision = "Wolverine__AutoProvision";
// Wires a Wolverine service to RabbitMQ: connection-string reference + WaitFor (Aspire 13 needs an
// explicit WaitFor for health). Wolverine AutoProvisions its exchanges/queues against the live
// broker at startup. See CLAUDE.md.
IResourceBuilder<ProjectResource> WithMessaging(IResourceBuilder<ProjectResource> project)
{
return project.WithReference(messaging).WaitFor(messaging);
}

// OrderService also references catalogService — that gives it the gRPC client config to call
// into Catalog for product validation during order placement.
var orderService = WithOptionalAppInsights(
var orderService = WithMessaging(WithOptionalAppInsights(
builder.AddProject<Projects.OrderService>("order-service")
.WithReference(ordersDb).WaitFor(ordersDb)
.WithReference(serviceBus).WaitFor(serviceBus)
.WithReference(catalogService).WaitFor(catalogService), appInsights)
.WithReference(realm, configurationPrefix: keycloakConfigPrefix).WaitFor(realm)
.WithEnvironment("Frontend__AllowedOrigins", SpaDevOrigin)
.WithEnvironment(disableAutoProvision, "false");
.WithEnvironment("Frontend__AllowedOrigins", SpaDevOrigin));

WithOptionalAppInsights(
WithMessaging(WithOptionalAppInsights(
builder.AddProject<Projects.PaymentService>("payment-service")
.WithReference(paymentsDb).WaitFor(paymentsDb)
.WithReference(serviceBus).WaitFor(serviceBus), appInsights)
.WithReference(realm, configurationPrefix: keycloakConfigPrefix).WaitFor(realm)
.WithEnvironment(disableAutoProvision, "false");
.WithReference(paymentsDb).WaitFor(paymentsDb), appInsights)
.WithReference(realm, configurationPrefix: keycloakConfigPrefix).WaitFor(realm));

WithOptionalAppInsights(
WithMessaging(WithOptionalAppInsights(
builder.AddProject<Projects.ShippingService>("shipping-service")
.WithReference(shippingDb).WaitFor(shippingDb)
.WithReference(serviceBus).WaitFor(serviceBus), appInsights)
.WithReference(shippingDb).WaitFor(shippingDb), appInsights)
.WithReference(realm, configurationPrefix: keycloakConfigPrefix).WaitFor(realm)
.WithEnvironment("Frontend__AllowedOrigins", SpaDevOrigin)
.WithEnvironment(disableAutoProvision, "false");
.WithEnvironment("Frontend__AllowedOrigins", SpaDevOrigin));

// NotificationService is stateless — no DB reference, just messaging + telemetry.
WithOptionalAppInsights(
builder.AddProject<Projects.NotificationService>("notification-service")
.WithReference(serviceBus).WaitFor(serviceBus), appInsights)
.WithEnvironment(disableAutoProvision, "false");
WithMessaging(WithOptionalAppInsights(
builder.AddProject<Projects.NotificationService>("notification-service"), appInsights));

// --- Frontend ---
// Storefront and SellerPortal reference the API services so service-discovery resolves
Expand Down
2 changes: 1 addition & 1 deletion NextAurora.AppHost/NextAurora.AppHost.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
<ItemGroup>
<PackageReference Include="Aspire.Hosting.PostgreSQL" />
<PackageReference Include="Aspire.Hosting.SqlServer" />
<PackageReference Include="Aspire.Hosting.RabbitMQ" />
<PackageReference Include="Aspire.Hosting.Redis" />
<PackageReference Include="Aspire.Hosting.Azure.ServiceBus" />
<PackageReference Include="Aspire.Hosting.Azure.ApplicationInsights" />
<PackageReference Include="Keycloak.AuthServices.Aspire.Hosting" />
<!-- Transitive-vulnerability override (NU1903) — see Directory.Packages.props MessagePack pin. -->
Expand Down
2 changes: 1 addition & 1 deletion NotificationService/NotificationService.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
<PackageReference Include="Scalar.AspNetCore" />
<PackageReference Include="WolverineFx" />
<PackageReference Include="WolverineFx.AzureServiceBus" />
<PackageReference Include="WolverineFx.RabbitMQ" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../NextAurora.ServiceDefaults/NextAurora.ServiceDefaults.csproj" />
Expand Down
30 changes: 13 additions & 17 deletions NotificationService/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
using NotificationService.Infrastructure;
using Scalar.AspNetCore;
using Wolverine;
using Wolverine.AzureServiceBus;
using Wolverine.RabbitMQ;

var builder = WebApplication.CreateBuilder(args);

Expand All @@ -14,25 +14,21 @@
builder.Host.UseWolverine(opts =>
{
var connectionString = builder.Configuration.GetConnectionString("messaging")!;
var azureServiceBus = opts.UseAzureServiceBus(connectionString);

// 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
// 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.
// RabbitMQ transport. NotificationService is listen-only (the saga sink): bind a per-source
// queue to each event exchange, plus the direct send-notification queue. AutoProvision is gated
// (default on) for consistency with the other services. See OrderService/Program.cs + CLAUDE.md.
var rabbit = opts.UseRabbitMq(factory => factory.Uri = new Uri(connectionString));
if (builder.Configuration.GetValue("Wolverine:AutoProvision", defaultValue: true))
{
azureServiceBus.AutoProvision();
rabbit.AutoProvision();
}

// 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");

// Listen to direct command queue
opts.ListenToAzureServiceBusQueue("send-notification");
rabbit.BindExchange("order-events", ExchangeType.Fanout).ToQueue("notify-orders");
rabbit.BindExchange("payment-events", ExchangeType.Fanout).ToQueue("notify-payments");
rabbit.BindExchange("shipping-events", ExchangeType.Fanout).ToQueue("notify-shipping");
opts.ListenToRabbitQueue("notify-orders");
opts.ListenToRabbitQueue("notify-payments");
opts.ListenToRabbitQueue("notify-shipping");
opts.ListenToRabbitQueue("send-notification");
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Single-project assembly — Wolverine auto-discovers handlers from the entry assembly,
// so no explicit IncludeAssembly call is needed.
Expand Down
2 changes: 1 addition & 1 deletion OrderService/OrderService.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" />
<PackageReference Include="Scalar.AspNetCore" />
<PackageReference Include="WolverineFx" />
<PackageReference Include="WolverineFx.AzureServiceBus" />
<PackageReference Include="WolverineFx.RabbitMQ" />
<PackageReference Include="WolverineFx.EntityFrameworkCore" />
<PackageReference Include="WolverineFx.FluentValidation" />
<PackageReference Include="WolverineFx.SqlServer" />
Expand Down
Loading
Loading