Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
10 changes: 5 additions & 5 deletions .claude/architecture-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ PlaceOrder (HTTP POST → OrderService)
│ 2. Order aggregate saved + OrderPlacedEvent staged in outbox (same tx)
└─→ OrderPlacedEvent (Service Bus topic)
└─→ OrderPlacedEvent (RabbitMQ exchange)
├─→ PaymentService consumes (OrderPlacedHandler → ProcessPayment)
│ │
│ │ Payment aggregate saved + PaymentCompletedEvent staged in outbox
Expand Down Expand Up @@ -79,9 +79,9 @@ All events live in `NextAurora.Contracts/Events/`:
- `PaymentFailedEvent` — published by PaymentService
- `ShipmentDispatchedEvent` — published by ShippingService

Service Bus topology: one topic per event type, one subscription per consumer-source pair.
Subscription names are globally unique within the namespace (Aspire 13+ rule).
Convention: `{consumer}-{source-events}-sub` (e.g. `notify-orders-sub`).
RabbitMQ topology: one fanout exchange per event type, one queue per consumer bound to it.
Convention: `{consumer}-{source-events}` (e.g. `notify-orders` = NotificationService consuming
`order-events`). Wolverine declares + AutoProvisions the exchanges/queues/bindings.

---

Expand Down Expand Up @@ -150,7 +150,7 @@ the failure mode.

- **REST (HTTP)** — frontend ↔ services only. URL-segment versioned `/api/v1/...`.
- **gRPC (sync)** — OrderService → CatalogService for real-time product validation + stock reservation. Versioned via `.proto` `package` declarations.
- **Service Bus (async)** — all workflow events. Wolverine transports + transactional outbox.
- **RabbitMQ (async)** — all workflow events. Wolverine transport + transactional outbox.

---

Expand Down
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) |
8 changes: 8 additions & 0 deletions .coderabbit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,14 @@ reviews:
Same reasoning as the AppHost.cs rule above — reviewers reason against
the .svg picture of the request lifecycle. Flag missing pairs.

TRANSPORT-REMOVAL SWEEP. When a messaging/transport package is removed
(e.g. a Wolverine transport swap), removing the `PackageReference` is not
enough — flag two orphans: (1) a dead OpenTelemetry `AddSource("...")` for
that transport's ActivitySource in the `.WithTracing(...)` block here
(silently drops messaging spans from the dashboard), and (2) an orphaned
`PackageVersion` left in `Directory.Packages.props` with no remaining
`PackageReference`. Both bit the ASB→RabbitMQ swap (PR #159).

- path: "**"
instructions: |
FILE-MOVE DISCIPLINE (CLAUDE.md "File-move discipline"). When this PR's
Expand Down
13 changes: 6 additions & 7 deletions CLAUDE.md

Large diffs are not rendered by default.

5 changes: 2 additions & 3 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,15 +36,14 @@
<PackageVersion Include="Asp.Versioning.Mvc.ApiExplorer" Version="10.0.0" />

<!-- Messaging -->
<PackageVersion Include="Azure.Messaging.ServiceBus" Version="7.20.1" />
<PackageVersion Include="WolverineFx" Version="6.8.0" />
<!-- Wolverine 6 split the runtime Roslyn code generator out of core (GH-2876). Without it,
the host throws at startup in the default TypeLoadMode.Dynamic. This package auto-registers
the IAssemblyGenerator when referenced, restoring 5.x behavior. Referenced from
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
84 changes: 25 additions & 59 deletions NextAurora.AppHost/AppHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
// it's stated here. That makes it easy to reason about what runs in any environment.

const string keycloakHostname = "http://localhost:8080/";
const string disableAutoProvisionValue = "false";

var builder = DistributedApplication.CreateBuilder(args);

Expand All @@ -37,36 +36,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 @@ -118,48 +95,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, disableAutoProvisionValue);
.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, disableAutoProvisionValue);
.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, disableAutoProvisionValue);
.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, disableAutoProvisionValue);
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
11 changes: 7 additions & 4 deletions NextAurora.ServiceDefaults/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,13 @@ public static TBuilder ConfigureOpenTelemetry<TBuilder>(this TBuilder builder) w
.WithTracing(tracing =>
{
tracing.AddSource(builder.Environment.ApplicationName)
.AddSource("Azure.Messaging.ServiceBus")
// "NextAurora.Messaging" is the ActivitySource used by all Service Bus
// processors. Registering it here causes consumer spans to appear in the
// Aspire dashboard and any connected distributed tracing backend.
// Wolverine's own ActivitySource — emits the message send/receive/handle spans
// for the saga regardless of transport (RabbitMQ today). This is the span source
// you follow in the Aspire dashboard to watch an order walk Order→Payment→Shipping.
.AddSource("Wolverine")
// "NextAurora.Messaging" is the project's own ActivitySource (context-propagation
// middleware). Registering it makes those spans appear in the Aspire dashboard
// and any connected distributed tracing backend.
.AddSource("NextAurora.Messaging")
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
.AddAspNetCoreInstrumentation(tracing =>
// Exclude health check requests from tracing
Expand Down
2 changes: 1 addition & 1 deletion NotificationService/NotificationService.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<PackageReference Include="Microsoft.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 @@ -22,7 +22,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