Skip to content

Host the KurrentDB Bookings sample in Aspire with blob storage projections - #574

Open
alexeyzimarev wants to merge 1 commit into
devfrom
aspire-apphost-sample
Open

Host the KurrentDB Bookings sample in Aspire with blob storage projections#574
alexeyzimarev wants to merge 1 commit into
devfrom
aspire-apphost-sample

Conversation

@alexeyzimarev

Copy link
Copy Markdown
Contributor

Aspire hosting for the Bookings sample

Supersedes #556. Instead of adding a third Bookings clone under samples/azure, the existing KurrentDB sample gains a .NET Aspire AppHost and the Azure pieces worth demonstrating — one sample to maintain instead of two, exercising the new Eventuous.Azure.Storage.Blobs package (#550) against a KurrentDB event store.

What's included

  • Bookings.AppHost orchestrates KurrentDB (same image as the test fixtures, arm64-aware), MongoDB 7.0, the Azurite blob emulator, both services, and a Scalar API reference. Service telemetry flows to the Aspire dashboard via OTLP; the debugger works across both services. Run with aspire run or dotnet run --project Bookings.AppHost.
  • Blob storage projection: BookingStateBlobProjection projects booking state to Azure Blob Storage from the same all-stream subscription as the Mongo projections — one subscription, multiple projection targets. It uses ByGlobalPosition idempotency (valid here: the all-stream subscription provides real global positions) and race retries. GET /bookings/{id}/view serves the blob read model next to the event-store fold (GET /bookings/{id}) and the Mongo view (GET /bookings/my/{userId}).
  • Standalone mode preserved: docker compose up -d now also starts KurrentDB and Azurite; a new README covers both run modes.

Latent sample bugs fixed (found by actually running it)

  • Payments crashed at startup — nothing set the default event serializer, required since Make IEventSerializer AOT-compatible #524.
  • Payments' MapDiscoveredCommands mapped zero routes: the sample never referenced the Eventuous.Extensions.AspNetCore.Generators analyzer (analyzers don't flow across ProjectReference, unlike the packaged analyzers/dotnet/cs path), and the [HttpCommand] command wasn't bound to a state — commands without a state land in the registry's never-read WithoutState list. Now bound via [HttpCommand<PaymentState>].
  • Both services gained /health endpoints and serve their OpenAPI document at openapi/{documentName}.json so the Scalar reference finds it.

Trade-off to be aware of

Aspire cannot run multi-targeted projects, so the sample is pinned to net10.0 — and Eventuous.Tests.Spyglass uses the sample apps as fixtures, so it moves from the multi-framework CI matrix to a net10-only step. The clean long-term fix is dedicated multi-TFM fixture apps for the Spyglass tests.

Verification

  • Full solution builds with zero errors; Spyglass tests 5/5 on net10.
  • Verified live under Aspire: booked a room, recorded a payment through the Payments service (POST /recordPayment), integration events flowed through the gateway → KurrentDB → persistent subscription, and all three read paths agree — the blob view ends at outstanding: 0, paid: true.

The AppHost structure, Scalar wiring, and blob projection approach are salvaged from @quezlatch's work in #556 (credited via Co-authored-by).

🤖 Generated with Claude Code

… projections

Supersedes #556: instead of adding a third Bookings clone under
samples/azure, the existing KurrentDB sample gains an Aspire AppHost and
the Azure pieces worth keeping.

- Add Bookings.AppHost orchestrating KurrentDB, MongoDB 7.0, the Azurite
  blob emulator, both services, and a Scalar API reference; service
  telemetry flows to the Aspire dashboard via OTLP
- Add BookingStateBlobProjection: booking state projected to Azure Blob
  Storage from the same all-stream subscription as the Mongo projections,
  with ByGlobalPosition idempotency and race retries, exposed via
  GET /bookings/{id}/view and readable next to the event-store fold
- Fix latent Payments sample breakage: set the default event serializer
  (required since #524), reference the AspNetCore command mapping
  generator, and bind RecordPayment with HttpCommand<PaymentState> so
  MapDiscoveredCommands actually maps the route
- Add health endpoints and Scalar-compatible OpenAPI document routes to
  both services; keep fixed ports for standalone runs while letting
  Aspire assign URLs
- Add kurrentdb and azurite services to docker-compose for standalone
  runs; add a sample README covering both run modes
- Pin the sample (and the Spyglass tests that use its apps as fixtures)
  to net10.0, since Aspire cannot run multi-targeted projects; Spyglass
  tests move to a net10-only CI step
- Verified end to end under Aspire: book, pay via the Payments service,
  integration events through the gateway, and all three read paths agree

Co-authored-by: Quezlatch <quezlatch@gmail.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@qodo-free-for-open-source-projects

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Host KurrentDB Bookings in Aspire with blob projections

✨ Enhancement 🐞 Bug fix 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Orchestrates KurrentDB, Bookings, Payments, MongoDB, Azurite, and Scalar through .NET Aspire.
• Projects booking state to blobs alongside MongoDB and exposes a blob-backed query endpoint.
• Fixes Payments startup, command discovery, telemetry, health checks, and net10 CI coverage.
Diagram

graph TD
    A["Aspire AppHost"] --> S["Scalar Reference"] --> B["Bookings API"] --> K[("KurrentDB")] --> Q["Shared Subscription"] --> M[("MongoDB")]
    S --> P["Payments API"] --> K
    Q --> Z[("Blob Storage")]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Separate Azure Bookings sample
  • ➕ Avoids changing the existing KurrentDB sample's target framework and CI behavior.
  • ➕ Keeps Azure-specific dependencies isolated from the established sample.
  • ➖ Duplicates the Bookings application and increases long-term maintenance.
  • ➖ Splits related KurrentDB and blob-projection concepts across separate samples.
2. Dedicated Spyglass fixture applications
  • ➕ Restores multi-target-framework coverage for Spyglass tests.
  • ➕ Decouples experimental tests from sample hosting constraints.
  • ➖ Introduces additional fixture projects to maintain.
  • ➖ Duplicates enough application setup to make fixtures representative.

Recommendation: Consolidating Aspire and blob projection behavior into the existing KurrentDB sample is preferable to maintaining another Bookings clone. The net10-only Spyglass coverage is the main compromise; dedicated multi-targeted fixture applications are a worthwhile follow-up if preserving the full test matrix is important.

Files changed (23) +329 / -41

Enhancement (7) +183 / -8
AppHost.csOrchestrate the complete Bookings environment with Aspire +63/-0

Orchestrate the complete Bookings environment with Aspire

• Defines architecture-aware KurrentDB, MongoDB 7.0, Azurite blobs, Bookings, Payments, and Scalar resources. It wires connection settings, health checks, dependencies, and startup ordering.

samples/kurrentdb/Bookings.AppHost/AppHost.cs

Program.csExpose Payments health and Scalar-compatible OpenAPI routes +5/-2

Expose Payments health and Scalar-compatible OpenAPI routes

• Registers health checks, serves OpenAPI at the route expected by Scalar, updates Swagger UI accordingly, and maps the health endpoint.

samples/kurrentdb/Bookings.Payments/Program.cs

BookingsQueryService.csRead projected booking views from blob storage +22/-1

Read projected booking views from blob storage

• Adds blob-backed booking retrieval using the projector's naming and JSON settings. Missing blobs are translated from storage 404 responses to null results.

samples/kurrentdb/Bookings/Application/BookingsQueryService.cs

BookingStateBlobProjection.csProject booking state into Azure Blob Storage +33/-0

Project booking state into Azure Blob Storage

• Introduces a blob projector that builds one BookingView per stream from booking and payment events. It runs alongside existing MongoDB handlers on the shared all-stream subscription.

samples/kurrentdb/Bookings/Application/Queries/BookingStateBlobProjection.cs

BookingView.csDefine the blob-backed booking read model +19/-0

Define the blob-backed booking read model

• Adds the serializable booking state projected into each stream's JSON blob, including guest, room, dates, price, outstanding balance, and payment status.

samples/kurrentdb/Bookings/Application/Queries/BookingView.cs

Program.csExpose blob views and Aspire service endpoints +27/-2

Expose blob views and Aspire service endpoints

• Adds health checks, Scalar-compatible OpenAPI routing, and GET /bookings/{bookingId}/view. It ensures the blob container exists and preserves the fixed standalone port while allowing Aspire-assigned URLs.

samples/kurrentdb/Bookings/Program.cs

Registrations.csRegister blob storage projection infrastructure +14/-3

Register blob storage projection infrastructure

• Registers the blob client and projector options with global-position idempotency and race retries. The blob projector joins the existing MongoDB handlers on the same partitioned all-stream subscription.

samples/kurrentdb/Bookings/Registrations.cs

Bug fix (3) +31 / -14
CommandService.csBind payment commands to PaymentState +1/-2

Bind payment commands to PaymentState

• Changes the RecordPayment annotation to HttpCommand<PaymentState>, allowing generated command discovery to register the endpoint in the stateful command registry.

samples/kurrentdb/Bookings.Payments/Application/CommandService.cs

Bookings.Payments.csprojAdd serializer and command generator dependencies +2/-0

Add serializer and command generator dependencies

• References the dynamic JSON serialization project and directly includes the ASP.NET Core command mapping generator as an analyzer. This restores Payments startup and generated route discovery when using project references.

samples/kurrentdb/Bookings.Payments/Bookings.Payments.csproj

Registrations.csRestore serialization and Aspire telemetry for Payments +28/-12

Restore serialization and Aspire telemetry for Payments

• Sets the required default event serializer. Metrics and traces now export through OTLP when Aspire provides an endpoint, while standalone tracing retains Zipkin as its fallback.

samples/kurrentdb/Bookings.Payments/Registrations.cs

Refactor (1) +1 / -1
CommandApi.csNormalize the Payments command API namespace +1/-1

Normalize the Payments command API namespace

• Removes trailing whitespace from the namespace declaration without changing runtime behavior.

samples/kurrentdb/Bookings.Payments/Application/CommandApi.cs

Documentation (2) +50 / -1
CLAUDE.mdRefresh the documented sample layout +1/-1

Refresh the documented sample layout

• Updates the repository overview to describe the KurrentDB Aspire sample and current sample set.

CLAUDE.md

README.mdDocument Aspire and standalone sample workflows +49/-0

Document Aspire and standalone sample workflows

• Explains the two-service architecture, projection targets, Aspire orchestration, standalone startup, telemetry dependencies, and endpoints for exercising MongoDB and blob read models.

samples/kurrentdb/README.md

Other (10) +64 / -17
pull-request.ymlRun Spyglass tests only on the net10 CI leg +5/-1

Run Spyglass tests only on the net10 CI leg

• Removes Spyglass from the multi-framework project list and adds a dedicated net10 test step because its sample fixtures now target net10 exclusively.

.github/workflows/pull-request.yml

Directory.Packages.propsCentrally manage Aspire and Scalar package versions +6/-0

Centrally manage Aspire and Scalar package versions

• Adds the shared Aspire version and package versions for Azure Storage hosting, MongoDB hosting, and Scalar Aspire integration.

Directory.Packages.props

Eventuous.slnxAdd the Bookings AppHost to the solution +1/-0

Add the Bookings AppHost to the solution

• Registers the new Aspire AppHost project under the KurrentDB samples solution folder.

Eventuous.slnx

Bookings.AppHost.csprojDefine the Aspire AppHost project +19/-0

Define the Aspire AppHost project

• Creates the executable Aspire project with references to both services and the Azure Storage, MongoDB, and Scalar hosting integrations.

samples/kurrentdb/Bookings.AppHost/Bookings.AppHost.csproj

Bookings.csprojReference the Azure Blob Storage projection package +1/-0

Reference the Azure Blob Storage projection package

• Adds the local Eventuous Azure Blob Storage project required by the new booking projection and query path.

samples/kurrentdb/Bookings/Bookings.csproj

appsettings.jsonConfigure standalone Azurite connectivity +3/-0

Configure standalone Azurite connectivity

• Adds the development blob storage connection string used when the Bookings service runs outside Aspire.

samples/kurrentdb/Bookings/appsettings.json

Directory.Build.propsPin the KurrentDB sample to net10 +8/-0

Pin the KurrentDB sample to net10

• Overrides inherited framework settings so all sample projects use the single target framework required by the Aspire AppHost.

samples/kurrentdb/Directory.Build.props

aspire.config.jsonConfigure the sample's default Aspire AppHost +5/-0

Configure the sample's default Aspire AppHost

• Points the Aspire CLI at the new Bookings.AppHost project.

samples/kurrentdb/aspire.config.json

docker-compose.ymlAdd KurrentDB and Azurite to standalone infrastructure +14/-16

Add KurrentDB and Azurite to standalone infrastructure

• Replaces the obsolete commented EventStoreDB definition with runnable KurrentDB and Azurite services. It documents the alternative KurrentDB image required on Apple Silicon.

samples/kurrentdb/docker-compose.yml

Eventuous.Tests.Spyglass.csprojRestrict Spyglass fixture tests to net10 +2/-0

Restrict Spyglass fixture tests to net10

• Pins Spyglass tests to net10 because their referenced Bookings sample fixtures must share the Aspire AppHost's single target framework.

src/Experimental/test/Eventuous.Tests.Spyglass/Eventuous.Tests.Spyglass.csproj

@qodo-free-for-open-source-projects

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (1) 📜 Skill insights (0)

Grey Divider


Action required

1. CreateIfNotExists blocks startup 📘 Rule violation ➹ Performance
Description
The application synchronously creates the blob container during startup even though Azure Storage
provides an asynchronous alternative. This blocks a thread on network I/O and violates the
requirement that all I/O be asynchronous.
Code

samples/kurrentdb/Bookings/Program.cs[R71-73]

+app.Services.GetRequiredService<BlobServiceClient>()
+    .GetBlobContainerClient(BookingStateBlobProjection.ContainerName)
+    .CreateIfNotExists();
Evidence
PR Compliance ID 2 requires asynchronous APIs for all I/O and .NoContext() where applicable. The
added startup code calls the synchronous Azure Blob Storage CreateIfNotExists() operation.

CLAUDE.md: All I/O Must Be Asynchronous and Use NoContext() for ConfigureAwait(false)
samples/kurrentdb/Bookings/Program.cs[70-73]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Blob-container creation performs synchronous network I/O during application startup.

## Issue Context
Replace `CreateIfNotExists()` with `CreateIfNotExistsAsync()` and await it using the repository's `.NoContext()` convention.

## Fix Focus Areas
- samples/kurrentdb/Bookings/Program.cs[70-73]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can show, collapse, or hide each part of a finding: code, evidence, and all

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +71 to +73
app.Services.GetRequiredService<BlobServiceClient>()
.GetBlobContainerClient(BookingStateBlobProjection.ContainerName)
.CreateIfNotExists();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Action required

1. createifnotexists blocks startup 📘 Rule violation ➹ Performance

The application synchronously creates the blob container during startup even though Azure Storage
provides an asynchronous alternative. This blocks a thread on network I/O and violates the
requirement that all I/O be asynchronous.
Agent Prompt
## Issue description
Blob-container creation performs synchronous network I/O during application startup.

## Issue Context
Replace `CreateIfNotExists()` with `CreateIfNotExistsAsync()` and await it using the repository's `.NoContext()` convention.

## Fix Focus Areas
- samples/kurrentdb/Bookings/Program.cs[70-73]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 960ac17803

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

.UseCheckpointStore<MongoCheckpointStore>()
.AddEventHandler<BookingStateProjection>()
.AddEventHandler<MyBookingsProjection>()
.AddEventHandler<BookingStateBlobProjection>()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Give the new blob projector its own checkpoint

When this version starts against MongoDB containing the pre-existing BookingsProjections checkpoint, the all-stream subscription resumes from that saved position, so the newly added handler never receives historical booking events. Existing bookings therefore return 404 from the new blob endpoint, and a later payment event can create an incomplete BookingView lacking the original room and guest data. Run this projector under an independent checkpoint that can replay from the beginning, or provide an explicit backfill/reset migration.

Useful? React with 👍 / 👎.

if (otelEnabled)
builder.AddOtlpExporter();
else
builder.AddZipkinExporter();
@github-actions

Copy link
Copy Markdown

Test Results

 44 files  + 21   44 suites  +21   11m 57s ⏱️ - 3m 27s
427 tests  -  17  427 ✅  -  17  0 💤 ±0  0 ❌ ±0 
760 runs  +305  760 ✅ +305  0 💤 ±0  0 ❌ ±0 

Results for commit 960ac17. ± Comparison against base commit 0f19628.

This pull request removes 26 and adds 9 tests. Note that renamed tests count towards both.
Eventuous.Tests.Azure.ServiceBus.IsSerialisableByServiceBus ‑ Passes(08/19/2026 17:03:32 +00:00)
Eventuous.Tests.Azure.ServiceBus.IsSerialisableByServiceBus ‑ Passes(08/19/2026 17:03:32)
Eventuous.Tests.Azure.ServiceBus.IsSerialisableByServiceBus ‑ Passes(4fd27478-7b64-4df0-9bd8-90fb61c89f24)
Eventuous.Tests.Azure.Storage.Blobs.BlobStorageProjectorTests ‑ AsyncContextAwareHandler_ExistingBlob_ShouldUpdateStateAndContext
Eventuous.Tests.Azure.Storage.Blobs.BlobStorageProjectorTests ‑ AsyncContextAwareHandler_NewBlob_ShouldUseContextAndStoreState
Eventuous.Tests.Azure.Storage.Blobs.BlobStorageProjectorTests ‑ AsyncStateHandler_ExistingBlob_ShouldUpdateState
Eventuous.Tests.Azure.Storage.Blobs.BlobStorageProjectorTests ‑ AsyncStateHandler_NewBlob_ShouldCreateAndStoreState
Eventuous.Tests.Azure.Storage.Blobs.BlobStorageProjectorTests ‑ ConcurrentAdditionOfNewBlob_ShouldReturnFailure
Eventuous.Tests.Azure.Storage.Blobs.BlobStorageProjectorTests ‑ ConcurrentModificationOfExistingBlob_ShouldReturnFailure
Eventuous.Tests.Azure.Storage.Blobs.BlobStorageProjectorTests ‑ CustomBlobId_ExistingBlob_ShouldUpdateWithEventId
…
Eventuous.Tests.Azure.ServiceBus.IsSerialisableByServiceBus ‑ Passes(08/19/2026 20:39:48 +00:00)
Eventuous.Tests.Azure.ServiceBus.IsSerialisableByServiceBus ‑ Passes(08/19/2026 20:39:48)
Eventuous.Tests.Azure.ServiceBus.IsSerialisableByServiceBus ‑ Passes(eaf44cab-8f35-40a1-95fc-d52a81c86969)
Eventuous.Tests.Subscriptions.SequenceTests ‑ ShouldReturnFirstBefore(CommitPosition { Position: 0, Sequence: 1, Timestamp: 2026-08-19T20:35:01.7843422+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-08-19T20:35:01.7843422+00:00 }, CommitPosition { Position: 0, Sequence: 4, Timestamp: 2026-08-19T20:35:01.7843422+00:00 }, CommitPosition { Position: 0, Sequence: 6, Timestamp: 2026-08-19T20:35:01.7843422+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-08-19T20:35:01.7843422+00:00 })
Eventuous.Tests.Subscriptions.SequenceTests ‑ ShouldReturnFirstBefore(CommitPosition { Position: 0, Sequence: 1, Timestamp: 2026-08-19T20:35:01.7843422+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-08-19T20:35:01.7843422+00:00 }, CommitPosition { Position: 0, Sequence: 6, Timestamp: 2026-08-19T20:35:01.7843422+00:00 }, CommitPosition { Position: 0, Sequence: 8, Timestamp: 2026-08-19T20:35:01.7843422+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-08-19T20:35:01.7843422+00:00 })
Eventuous.Tests.Subscriptions.SequenceTests ‑ ShouldReturnFirstBefore(CommitPosition { Position: 0, Sequence: 1, Timestamp: 2026-08-19T20:35:07.1277336+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-08-19T20:35:07.1277336+00:00 }, CommitPosition { Position: 0, Sequence: 4, Timestamp: 2026-08-19T20:35:07.1277336+00:00 }, CommitPosition { Position: 0, Sequence: 6, Timestamp: 2026-08-19T20:35:07.1277336+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-08-19T20:35:07.1277336+00:00 })
Eventuous.Tests.Subscriptions.SequenceTests ‑ ShouldReturnFirstBefore(CommitPosition { Position: 0, Sequence: 1, Timestamp: 2026-08-19T20:35:07.1277336+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-08-19T20:35:07.1277336+00:00 }, CommitPosition { Position: 0, Sequence: 6, Timestamp: 2026-08-19T20:35:07.1277336+00:00 }, CommitPosition { Position: 0, Sequence: 8, Timestamp: 2026-08-19T20:35:07.1277336+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-08-19T20:35:07.1277336+00:00 })
Eventuous.Tests.Subscriptions.SequenceTests ‑ ShouldReturnFirstBefore(CommitPosition { Position: 0, Sequence: 1, Timestamp: 2026-08-19T20:35:08.6669151+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-08-19T20:35:08.6669151+00:00 }, CommitPosition { Position: 0, Sequence: 4, Timestamp: 2026-08-19T20:35:08.6669151+00:00 }, CommitPosition { Position: 0, Sequence: 6, Timestamp: 2026-08-19T20:35:08.6669151+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-08-19T20:35:08.6669151+00:00 })
Eventuous.Tests.Subscriptions.SequenceTests ‑ ShouldReturnFirstBefore(CommitPosition { Position: 0, Sequence: 1, Timestamp: 2026-08-19T20:35:08.6669151+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-08-19T20:35:08.6669151+00:00 }, CommitPosition { Position: 0, Sequence: 6, Timestamp: 2026-08-19T20:35:08.6669151+00:00 }, CommitPosition { Position: 0, Sequence: 8, Timestamp: 2026-08-19T20:35:08.6669151+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-08-19T20:35:08.6669151+00:00 })

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