Skip to content

Aspire and azure blob storage#550

Open
quezlatch wants to merge 24 commits into
Eventuous:devfrom
quezlatch:aspire-and-blob-storage
Open

Aspire and azure blob storage#550
quezlatch wants to merge 24 commits into
Eventuous:devfrom
quezlatch:aspire-and-blob-storage

Conversation

@quezlatch

Copy link
Copy Markdown
Contributor

This PR includes two new features instead of one I'm afraid, but they do dovetail together nicely. It builds on my Azure ServiceBus piece.

Azure blob storage

Adds a new projection target for persisting state to Azure Blob Storage. The implementation provides a flexible way to project events into blob-stored state objects.

  • Added new projection support for Azure Blob Storage
  • Enables event sourcing projections to store state as blobs
  • Includes configurable blob naming and serialisation options
  • Retry option in case of race conditions

Aspire Sample

A new sample demonstrates how to use Eventuous in a distributed .NET Aspire application with Azure services.

  • Added new Aspire AppHost project (Bookings.AppHost) with distributed application configuration
  • Configured Azure Storage emulator with blob container support
  • Integrated Azure SQL Server, Service Bus, and Storage resources in the sample
  • Added Scalar.Aspire for API reference management between services
  • Updated both Bookings and Payments sample projects to reference Azure Storage Blobs package
  • Still uses Swashbuckle rather than MS OpenApi, as that clashes with JetBrains Annotations
  • Is pretty cool to see the debugger just work across distributed services
  • Requires Aspire CLI, and really an IDE extension

PR prepared with the help of Mistral Vibe.

quezlatch and others added 21 commits June 19, 2026 08:30
…StorageBlobsProjector

- Create new test project following Eventuous.Tests.Azure.ServiceBus structure
- Add Testcontainers.Azurite package to Directory.Packages.props
- Add IntegrationFixture with Azurite and KurrentDB containers
- Test all On method variants (sync/async, state/context) for new and existing blobs
- Test concurrent modification scenario (412 Precondition Failed)
- Test no handler scenario (returns Ignored)

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
…face intent

- Add helper methods: SetupContainer, SetupExistingBlob, GetBlobState, AssertSuccess, AssertIgnored
- Rename projector classes to surface handler patterns (SyncStateProjector, etc.)
- Group tests by handler variant with clear section comments
- Test names now follow [Variant]_[Scenario]_[ExpectedBehavior] pattern
- Reduce LOC from ~450 to ~330 (-27%)
- Remove fixture parameter from CreateContext (unused)

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
…obServiceClient and container name

- Add StorageBlobsProjector(BlobServiceClient, string containerName) constructor
- Update test helper methods to work with container names instead of BlobContainerClient
- Add GetContainer() helper to get BlobContainerClient from fixture
- Update all test projector classes with new constructor overload
- Update all tests to use fixture.BlobServiceClient with container names

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
@qodo-free-for-open-source-projects

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Add Azure Blob Storage projector and .NET Aspire Azure sample
✨ Enhancement 🧪 Tests ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

Description

• Add Azure Blob Storage projector for persisting projection state with optimistic concurrency.
• Add Azurite-based integration tests covering handler variants and race-condition retries.
• Add .NET Aspire Azure sample wiring SQL, Service Bus, and Blob storage emulators.
Diagram

graph TD
  AppHost["Aspire AppHost"] --> Bookings["Bookings API"] --> Sql[("Azure SQL")]
  Bookings --> Blobs[("Blob Storage")]
  Sql --> Projector["StorageBlobsProjector"] --> Blobs
  Payments["Payments API"] --> Gateway["Eventuous Gateway"] --> Bus[("Service Bus")]
  Bus --> Bookings
  AppHost --> Payments --> Sql
  AppHost --> Bus
  AppHost --> Blobs
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Use a database-backed read model (SQL/Cosmos) instead of blobs
  • ➕ Richer querying/secondary indexes without custom blob reads
  • ➕ More mature concurrency/transaction semantics for multi-document updates
  • ➖ Higher operational and cost footprint for simple “document per key” projections
  • ➖ More schema/migration work than blob-stored JSON
2. Use Azure Table Storage for key/value projection state
  • ➕ Natural fit for per-aggregate or per-user keyed state
  • ➕ Built-in ETag concurrency similar to blobs
  • ➖ Less flexible for larger nested documents vs a single JSON blob
  • ➖ Different SDK/semantics; might require more mapping code
3. Use blob leasing instead of ETag retries for write contention
  • ➕ Explicit single-writer semantics can reduce retry storms under heavy contention
  • ➕ Clearer failure mode when multiple consumers compete on the same key
  • ➖ More complexity (lease acquisition/renewal/release) and potential lease orphaning
  • ➖ Slower for low-contention workloads where optimistic writes work well

Recommendation: The chosen approach (ETag-based optimistic concurrency with optional retries) is a good default for projections that write one blob per key and can tolerate occasional retry. Consider adding jitter/backoff to RaceRetries (or documenting recommended values) if high write contention is expected; otherwise the current design is appropriately simple and integrates cleanly with existing Eventuous handler registration patterns.

Files changed (39) +1611 / -1

Enhancement (23) +902 / -0
AppHost.csCompose Aspire distributed app with SQL, Service Bus emulator, and Blob storage +39/-0

Compose Aspire distributed app with SQL, Service Bus emulator, and Blob storage

• Defines the distributed application topology: Azure SQL Server container, Service Bus emulator with queue, and Azure Storage emulator with a blob container. Wires Bookings and Payments projects to these resources and adds Scalar API reference aggregation.

samples/azure/Bookings.AppHost/AppHost.cs

Bookings.Payments.csprojCreate Payments web project with Azure client dependencies and telemetry +44/-0

Create Payments web project with Azure client dependencies and telemetry

• Adds a new net web project for the Azure Payments sample, including Azure.Storage.Blobs + Microsoft.Extensions.Azure, OpenTelemetry exporters/instrumentation, Serilog, and Swashbuckle. References Eventuous components needed for SQL Server store, gateways, and Azure Service Bus integration.

samples/azure/Bookings.Payments/Bookings.Payments.csproj

Payments.csAdd gateway transform from payment events to integration messages +31/-0

Add gateway transform from payment events to integration messages

• Implements a gateway transform that maps PaymentRecorded events into BookingPaymentRecorded integration events and targets the PaymentsIntegration stream for Service Bus production.

samples/azure/Bookings.Payments/Integration/Payments.cs

Program.csBootstrap Payments API with Swagger, telemetry, and Eventuous services +28/-0

Bootstrap Payments API with Swagger, telemetry, and Eventuous services

• Sets up Serilog logging, Swagger (Swashbuckle), OpenTelemetry, and registers Eventuous services for command discovery. Exposes OpenAPI JSON under a custom route and enables Prometheus scraping.

samples/azure/Bookings.Payments/Program.cs

Registrations.csWire Payments Eventuous stack (SQL store, gateway, Service Bus producer, Blob client) +37/-0

Wire Payments Eventuous stack (SQL store, gateway, Service Bus producer, Blob client)

• Registers Azure clients for Service Bus and BlobServiceClient via Microsoft.Extensions.Azure. Configures SQL Server event store + checkpoint store, command service, ServiceBusProducer, and a gateway subscription that publishes integration events.

samples/azure/Bookings.Payments/Registrations.cs

BookingsCommandService.csAdd booking command handlers for the Azure sample +32/-0

Add booking command handlers for the Azure sample

• Implements a CommandService for Booking aggregate with handlers for booking a room and recording a payment. Uses NodaTime to build stay periods and injects a room availability service.

samples/azure/Bookings/Application/BookingsCommandService.cs

BookingsQueryService.csAdd query service wrapper for blob-backed MyBookings projection +7/-0

Add query service wrapper for blob-backed MyBookings projection

• Provides a small service that loads per-user bookings by delegating to a keyed MyBookingsProjection instance.

samples/azure/Bookings/Application/BookingsQueryService.cs

Commands.csDefine booking command DTOs used by HTTP API and command service +17/-0

Define booking command DTOs used by HTTP API and command service

• Adds BookRoom and RecordPayment command records for the Azure sample APIs.

samples/azure/Bookings/Application/Commands.cs

BookingDocument.csIntroduce BookingDocument read model stored in blob projections +16/-0

Introduce BookingDocument read model stored in blob projections

• Defines the document shape used by blob projections to represent booking details and payment state.

samples/azure/Bookings/Application/Queries/BookingDocument.cs

BookingStateProjection.csAdd blob projector for per-booking BookingDocument state +27/-0

Add blob projector for per-booking BookingDocument state

• Implements a StorageBlobsProjector-based projection that folds booking events into a BookingDocument stored in the "bookings-container" blob container.

samples/azure/Bookings/Application/Queries/BookingStateProjection.cs

MyBookings.csDefine per-user MyBookings document model +10/-0

Define per-user MyBookings document model

• Adds a user-centric bookings list document (immutable list) suitable for blob storage projection and query endpoints.

samples/azure/Bookings/Application/Queries/MyBookings.cs

MyBookingsProjection.csAdd blob projector for per-user booking lists with fallback reads +45/-0

Add blob projector for per-user booking lists with fallback reads

• Projects RoomBooked/BookingCancelled into a per-user MyBookings blob, with a custom blob id derived from event context or existing stream state. Adds a helper method to load the document from Blob Storage with a 404 -> null behavior.

samples/azure/Bookings/Application/Queries/MyBookingsProjection.cs

Bookings.csprojCreate Bookings web project with blob projection and Azure dependencies +35/-0

Create Bookings web project with blob projection and Azure dependencies

• Adds a new net web project for the Azure Bookings sample, including Azure blob client packages, OpenTelemetry/Serilog/Swagger, and a reference to the new Eventuous.Azure.Storage.Blobs project.

samples/azure/Bookings/Bookings.csproj

CommandApi.csAdd booking command HTTP endpoints +28/-0

Add booking command HTTP endpoints

• Implements a CommandHttpApiBase controller exposing endpoints to book a room and (for demo) record a payment directly.

samples/azure/Bookings/HttpApi/Bookings/CommandApi.cs

QueryApi.csAdd booking aggregate query endpoint via IEventReader +18/-0

Add booking aggregate query endpoint via IEventReader

• Adds a controller endpoint that loads BookingState from the event store by stream id.

samples/azure/Bookings/HttpApi/Bookings/QueryApi.cs

Logging.csAdd shared Serilog console logging configuration +22/-0

Add shared Serilog console logging configuration

• Provides a reusable Serilog configuration used by the Azure sample services, including overrides for noisy namespaces.

samples/azure/Bookings/Infrastructure/Logging.cs

Telemetry.csAdd OpenTelemetry metrics/tracing configuration with optional OTLP +41/-0

Add OpenTelemetry metrics/tracing configuration with optional OTLP

• Configures OpenTelemetry metrics (Prometheus exporter, optional OTLP) and tracing (Zipkin fallback when OTLP is absent), including Eventuous instrumentation.

samples/azure/Bookings/Infrastructure/Telemetry.cs

Payments.csAdd Service Bus-driven integration handler to record payments +35/-0

Add Service Bus-driven integration handler to record payments

• Defines an Eventuous subscription handler that consumes BookingPaymentRecorded integration events and issues a RecordPayment command against the Booking aggregate.

samples/azure/Bookings/Integration/Payments.cs

Program.csBootstrap Bookings API with controllers, query route, telemetry, and Spyglass +44/-0

Bootstrap Bookings API with controllers, query route, telemetry, and Spyglass

• Sets up JSON options for NodaTime, Swagger, OpenTelemetry, Eventuous, and Spyglass. Adds a minimal API query endpoint for per-user bookings loaded from blob projection state.

samples/azure/Bookings/Program.cs

Registrations.csWire Bookings Eventuous stack (SQL store, blob projections, Service Bus subscription) +59/-0

Wire Bookings Eventuous stack (SQL store, blob projections, Service Bus subscription)

• Registers default event serializer with NodaTime support, Azure clients (Service Bus + BlobServiceClient), SQL Server event store/checkpoints, and two subscriptions: SQL-based projections using blob projectors, and Service Bus subscription for payment integration.

samples/azure/Bookings/Registrations.cs

Eventuous.Azure.Storage.Blobs.csprojAdd new Eventuous.Azure.Storage.Blobs library project +39/-0

Add new Eventuous.Azure.Storage.Blobs library project

• Introduces a new Azure Blobs integration library with Azure.Storage.Blobs dependency and a packaged README. Includes InternalsVisibleTo for its new test project and links shared tooling sources.

src/Azure/src/Eventuous.Azure.Storage.Blobs/Eventuous.Azure.Storage.Blobs.csproj

StorageBlobProjectorOptions.csAdd options object for blob projector serialization and retry behavior +33/-0

Add options object for blob projector serialization and retry behavior

• Defines configurable JSON serializer options, custom serialize/deserialize delegates, and a RaceRetries setting for handling write races.

src/Azure/src/Eventuous.Azure.Storage.Blobs/StorageBlobProjectorOptions.cs

StorageBlobsProjector.csImplement StorageBlobsProjector with ETag concurrency and optional retries +215/-0

Implement StorageBlobsProjector with ETag concurrency and optional retries

• Adds a generic event handler that materializes per-key state into JSON blobs, supports multiple handler registration patterns (sync/async, with/without context), and allows custom blob id selection. Implements optimistic concurrency via ETags for updates and If-None-Match for inserts, with configurable retries on 409/412 responses.

src/Azure/src/Eventuous.Azure.Storage.Blobs/StorageBlobsProjector.cs

Bug fix (1) +1 / -1
ServiceBusSubscription.csMake eventSerializer parameter optional in ServiceBusSubscription ctor +1/-1

Make eventSerializer parameter optional in ServiceBusSubscription ctor

• Updates the constructor signature to default eventSerializer to null, improving DI ergonomics and aligning with other optional dependencies.

src/Azure/src/Eventuous.Azure.ServiceBus/Subscriptions/ServiceBusSubscription.cs

Tests (4) +599 / -0
Eventuous.Tests.Azure.Storage.Blobs.csprojAdd Azure Storage.Blobs integration test project +19/-0

Add Azure Storage.Blobs integration test project

• Creates a dedicated test project for StorageBlobsProjector with dependencies on Azure.Storage.Blobs and Testcontainers.Azurite/KurrentDb, plus shared subscription test infrastructure.

src/Azure/test/Eventuous.Tests.Azure.Storage.Blobs/Eventuous.Tests.Azure.Storage.Blobs.csproj

IntegrationFixture.csAdd Azurite-backed fixture providing BlobServiceClient for integration tests +49/-0

Add Azurite-backed fixture providing BlobServiceClient for integration tests

• Starts an Azurite container for the test suite and exposes a BlobServiceClient. Configures the default event serializer used by tests.

src/Azure/test/Eventuous.Tests.Azure.Storage.Blobs/Fixtures/IntegrationFixture.cs

StorageBlobsProjectorTests.csAdd comprehensive tests for StorageBlobsProjector handler variants and races +517/-0

Add comprehensive tests for StorageBlobsProjector handler variants and races

• Validates new-blob creation and existing-blob updates across sync/async and context-aware handlers, including custom blob id generation. Adds concurrency tests for ETag mismatch failures and a retry-success scenario when RaceRetries is enabled.

src/Azure/test/Eventuous.Tests.Azure.Storage.Blobs/StorageBlobsProjectorTests.cs

TestEvent.csAdd typed test event with EventType registration for blob projector tests +14/-0

Add typed test event with EventType registration for blob projector tests

• Defines a TestEvent record annotated with EventType and registers known event types, supporting projector handler registration and type mapping in tests.

src/Azure/test/Eventuous.Tests.Azure.Storage.Blobs/TestEvent.cs

Documentation (1) +1 / -0
README.mdAdd placeholder README for Azure Storage.Blobs integration +1/-0

Add placeholder README for Azure Storage.Blobs integration

• Adds an initial (WIP) README file to be included in the NuGet package.

src/Azure/src/Eventuous.Azure.Storage.Blobs/README.md

Other (10) +108 / -0
Directory.Packages.propsAdd Aspire + Azure Blobs + Azurite and OTEL SqlClient package versions +8/-0

Add Aspire + Azure Blobs + Azurite and OTEL SqlClient package versions

• Introduces pinned package versions for Aspire Azure hosting components, Azure.Storage.Blobs, Microsoft.Extensions.Azure, Scalar.Aspire, Testcontainers.Azurite, and OpenTelemetry SqlClient instrumentation. This enables both the new blob projector feature and the Aspire sample to compile with consistent dependency versions.

Directory.Packages.props

Eventuous.slnxRegister Azure Storage.Blobs projects and new Azure Aspire sample projects +8/-0

Register Azure Storage.Blobs projects and new Azure Aspire sample projects

• Adds the new Eventuous.Azure.Storage.Blobs library and its test project to the solution structure. Also adds a new Samples/Azure folder containing the Aspire AppHost plus Bookings/Payments/domain projects.

Eventuous.slnx

Bookings.AppHost.csprojAdd Aspire AppHost project with Azure hosting packages +25/-0

Add Aspire AppHost project with Azure hosting packages

• Creates a net10.0 Aspire AppHost executable referencing the Bookings and Payments projects. Adds package references for Aspire Azure ServiceBus/Sql/Storage and Scalar.Aspire.

samples/azure/Bookings.AppHost/Bookings.AppHost.csproj

appsettings.Development.jsonDevelopment logging defaults for AppHost +8/-0

Development logging defaults for AppHost

• Adds basic logging level configuration for local development runs of the Aspire AppHost.

samples/azure/Bookings.AppHost/appsettings.Development.json

appsettings.jsonAppHost logging configuration (Aspire.Hosting.Dcp warnings) +9/-0

AppHost logging configuration (Aspire.Hosting.Dcp warnings)

• Sets default log levels and reduces Aspire DCP noise by elevating it to Warning.

samples/azure/Bookings.AppHost/appsettings.json

Bookings.Domain.csprojAdd Azure sample domain project linking existing KurrentDB sample sources +16/-0

Add Azure sample domain project linking existing KurrentDB sample sources

• Creates a domain project for the Azure sample by referencing core Eventuous domain/shared projects and linking domain source files from the existing kurrentdb sample directory. Adds NodaTime dependency.

samples/azure/Bookings.Domain/Bookings.Domain.csproj

appsettings.jsonPayments sample configuration placeholders for Aspire-provided connection strings +14/-0

Payments sample configuration placeholders for Aspire-provided connection strings

• Adds ConnectionStrings placeholders for the Aspire-provisioned SQL, Service Bus emulator, and blobs endpoints, plus basic logging/host settings.

samples/azure/Bookings.Payments/appsettings.json

appsettings.jsonBookings sample configuration placeholders for Aspire connection strings +8/-0

Bookings sample configuration placeholders for Aspire connection strings

• Adds Aspire-provided ConnectionStrings placeholders for SQL, Service Bus emulator, and blob storage, plus host configuration.

samples/azure/Bookings/appsettings.json

Directory.Build.propsForce net10.0 target framework for Azure sample tree +7/-0

Force net10.0 target framework for Azure sample tree

• Adds a sample-scoped build props file that imports the repo defaults and sets target framework(s) to net10.0 for the Azure sample projects.

samples/azure/Directory.Build.props

aspire.config.jsonDeclare Bookings.AppHost as the Aspire appHost entrypoint +5/-0

Declare Bookings.AppHost as the Aspire appHost entrypoint

• Adds Aspire configuration pointing tooling at the AppHost project path.

samples/azure/aspire.config.json

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

qodo-free-for-open-source-projects Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Missing .NoContext() in StorageBlobsProjector ✓ Resolved 📘 Rule violation ☼ Reliability
Description
New library code awaits multiple async I/O operations without applying .NoContext(), which breaks
the repository convention for ConfigureAwait(false) and can increase deadlock risk in library
consumers. This affects blob download/upload and internal async flows in the projector.
Code

src/Azure/src/Eventuous.Azure.Storage.Blobs/StorageBlobsProjector.cs[R163-213]

+        public async ValueTask<EventHandlingStatus> Handle(IMessageConsumeContext context) {
+            typedContext = context as MessageConsumeContext<TEvent> ?? new MessageConsumeContext<TEvent>(context);
+            var blobId = GetBlobId == null
+                ? context.Stream.GetId()
+                : await GetBlobId(typedContext);
+            var blobName = projector.GetBlobName(blobId, typedContext);
+
+            blobClient = projector.GetBlobContainerClient(blobName);
+
+            return await ModifyBlobWithRetries(projector._raceRetries);
+        }
+
+        private async Task<EventHandlingStatus> ModifyBlobWithRetries(int retries) {
+            try {
+                await ModifyBlob();
+                return EventHandlingStatus.Success;
+            } catch (RequestFailedException ex) when (ex.Status == 412 || ex.Status == 409) {
+                return retries > 0 ? await ModifyBlobWithRetries(retries - 1) : EventHandlingStatus.Failure;
+            }
+        }
+
+        private async Task ModifyBlob() {
+            try {
+                BlobDownloadResult blobContent = await blobClient.DownloadContentAsync();
+
+                var content = blobContent.Content;
+                var current = projector.Deserialize(content);
+
+                var uploadOptions = new BlobUploadOptions {
+                    Conditions = new BlobRequestConditions { IfMatch = blobContent!.Details.ETag }
+                };
+                await UploadUpdated(current, uploadOptions);
+            } catch (RequestFailedException ex) when (ex.Status == 404) {
+                // Blob doesn't exist, start with a new instance
+                var insertOptions = new BlobUploadOptions {
+                    Conditions = new BlobRequestConditions { IfNoneMatch = ETag.All }
+                };
+                await UploadUpdated(new T(), insertOptions);
+            }
+        }
+
+        private async Task UploadUpdated(T current, BlobUploadOptions uploadOptions) {
+            var task = EventHandler(typedContext, current);
+            var updated = task.IsCompletedSuccessfully
+                ? task.Result
+                : await task;
+            var json = projector.Serialize(updated);
+
+            using var stream = new MemoryStream(json);
+            var response = await blobClient.UploadAsync(stream, uploadOptions, typedContext.CancellationToken);
+        }
Evidence
PR Compliance ID 1 requires async I/O awaits to apply .NoContext(). In StorageBlobsProjector.cs,
several blob SDK calls and internal async operations are awaited without .NoContext() (e.g.,
download/upload and retry paths).

CLAUDE.md: Use Asynchronous APIs for All I/O Operations and Apply NoContext(): CLAUDE.md: Use Asynchronous APIs for All I/O Operations and Apply NoContext(): CLAUDE.md: Use Asynchronous APIs for All I/O Operations and Apply NoContext(): CLAUDE.md: Use Asynchronous APIs for All I/O Operations and Apply NoContext(): CLAUDE.md: Use Asynchronous APIs for All I/O Operations and Apply NoContext(): CLAUDE.md: Use Asynchronous APIs for All I/O Operations and Apply NoContext(): CLAUDE.md: Use Asynchronous APIs for All I/O Operations and Apply NoContext(): CLAUDE.md: Use Asynchronous APIs for All I/O Operations and Apply NoContext()
src/Azure/src/Eventuous.Azure.Storage.Blobs/StorageBlobsProjector.cs[163-213]
src/Core/src/Eventuous.Shared/Tools/TaskExtensions.cs[11-26]

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

## Issue description
`StorageBlobsProjector<T>` performs async I/O and awaits tasks without the repo-standard `.NoContext()` helper (ConfigureAwait(false)). This violates the async I/O guideline for library code.
## Issue Context
The repo provides `Eventuous.Tools.TaskExtensions.NoContext()` and other integration code (e.g., ServiceBus) consistently applies it.
## Fix Focus Areas
- src/Azure/src/Eventuous.Azure.Storage.Blobs/StorageBlobsProjector.cs[163-213]

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


2. Sync blobClient.Upload() in tests ✓ Resolved 📘 Rule violation ➹ Performance
Description
New tests perform synchronous blob uploads (blobClient.Upload(...)) instead of using async APIs,
introducing synchronous I/O. This can cause thread pool starvation and violates the async I/O
requirement.
Code

src/Azure/test/Eventuous.Tests.Azure.Storage.Blobs/StorageBlobsProjectorTests.cs[R278-286]

+        var projector = new RaceRetryProjector(fixture.BlobServiceClient, containerName,
+            messWithState: () => {
+                // Simulate concurrent modification: modify the blob directly with a different value
+                var modifiedState = new ConcurrentState { Value = 999 };
+                var modifiedJson = JsonSerializer.SerializeToUtf8Bytes(modifiedState);
+                var blobClient = GetContainer(containerName).GetBlobClient(blobName);
+                blobClient.Upload(new MemoryStream(modifiedJson), overwrite: true);
+            });
+        var context = CreateContext(new TestEvent { Value = 10 });
Evidence
PR Compliance ID 1 forbids introducing synchronous I/O. The new test code uses the synchronous Azure
Blob call blobClient.Upload(...) to modify blobs, rather than UploadAsync(...).

CLAUDE.md: Use Asynchronous APIs for All I/O Operations and Apply NoContext(): CLAUDE.md: Use Asynchronous APIs for All I/O Operations and Apply NoContext(): CLAUDE.md: Use Asynchronous APIs for All I/O Operations and Apply NoContext(): CLAUDE.md: Use Asynchronous APIs for All I/O Operations and Apply NoContext(): CLAUDE.md: Use Asynchronous APIs for All I/O Operations and Apply NoContext(): CLAUDE.md: Use Asynchronous APIs for All I/O Operations and Apply NoContext(): CLAUDE.md: Use Asynchronous APIs for All I/O Operations and Apply NoContext(): CLAUDE.md: Use Asynchronous APIs for All I/O Operations and Apply NoContext()
src/Azure/test/Eventuous.Tests.Azure.Storage.Blobs/StorageBlobsProjectorTests.cs[278-286]

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

## Issue description
Synchronous Azure Blob SDK calls were introduced in tests (`blobClient.Upload(...)`). The repository compliance requires async I/O APIs.
## Issue Context
These uploads are used to simulate concurrent modifications; they can be implemented with `UploadAsync(...).NoContext()` and (if needed) awaited from an async callback.
## Fix Focus Areas
- src/Azure/test/Eventuous.Tests.Azure.Storage.Blobs/StorageBlobsProjectorTests.cs[278-286]
- src/Azure/test/Eventuous.Tests.Azure.Storage.Blobs/StorageBlobsProjectorTests.cs[306-313]
- src/Azure/test/Eventuous.Tests.Azure.Storage.Blobs/StorageBlobsProjectorTests.cs[329-336]

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


3. BookingPaymentRecorded not TypeMap-registered ✓ Resolved 📘 Rule violation ☼ Reliability
Description
The Azure Bookings sample registers TypeMap only for the Bookings.Domain assembly, but it also
introduces the BookingPaymentRecorded integration event (in the Bookings web project) that will be
deserialized by a Service Bus subscription. If that event type isn’t registered, the default
serializer can’t resolve the event type name and deserialization can fail (e.g., unknown type / null
payload), breaking the integration flow.
Code

samples/azure/Bookings/Program.cs[R11-12]

+TypeMap.RegisterKnownEventTypes(typeof(BookingEvents.V1.RoomBooked).Assembly);
+Logging.ConfigureLog();
Evidence
PR Compliance ID 2 requires new serialized event types to be registered in TypeMap;
BookingPaymentRecorded is newly introduced and annotated with
[EventType("BookingPaymentRecorded")], yet samples/azure/Bookings/Program.cs only calls
TypeMap.RegisterKnownEventTypes(typeof(BookingEvents.V1.RoomBooked).Assembly), which scans the
domain events assembly rather than the Bookings web project where BookingPaymentRecorded lives.
When TypeMap can’t resolve the event type name, DefaultEventSerializer yields an unknown type
result and EventSubscription deserialization returns a null payload on such failures, which breaks
the subscriber (PaymentsIntegrationHandler) that expects to handle BookingPaymentRecorded.

CLAUDE.md: Register Event Types in TypeMap for Serialization: CLAUDE.md: Register Event Types in TypeMap for Serialization: CLAUDE.md: Register Event Types in TypeMap for Serialization: CLAUDE.md: Register Event Types in TypeMap for Serialization: CLAUDE.md: Register Event Types in TypeMap for Serialization: CLAUDE.md: Register Event Types in TypeMap for Serialization: CLAUDE.md: Register Event Types in TypeMap for Serialization: CLAUDE.md: Register Event Types in TypeMap for Serialization
samples/azure/Bookings/Program.cs[11-12]
samples/azure/Bookings/Integration/Payments.cs[32-35]
samples/azure/Bookings/Program.cs[1-12]
samples/azure/Bookings.Domain/Bookings.Domain.csproj[12-15]
src/Core/src/Eventuous.Serialization/DefaultEventSerializer.cs[21-25]
src/Core/src/Eventuous.Subscriptions/EventSubscription.cs[154-174]

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

## Issue description
A new serialized integration event type (`BookingPaymentRecorded`) is used by the Bookings Azure sample’s Service Bus subscription, but TypeMap registration currently scans only the domain events assembly, so the event type isn’t discoverable during deserialization and the subscription can fail (unknown type / null payload).
## Issue Context
- The sample registers known event types with `TypeMap.RegisterKnownEventTypes(typeof(BookingEvents.V1.RoomBooked).Assembly)`, which only covers `Bookings.Domain`.
- `BookingPaymentRecorded` is declared in the Bookings web project assembly and is handled by `PaymentsIntegrationHandler`, so its type must be resolvable by TypeMap at runtime.
- A sample-friendly fix is to either register both relevant assemblies explicitly or call `TypeMap.RegisterKnownEventTypes()` (no args) to scan loaded assemblies in this sample.
## Fix Focus Areas
- samples/azure/Bookings/Program.cs[1-12]
- samples/azure/Bookings/Integration/Payments.cs[32-35]
- samples/azure/Bookings.Domain/Bookings.Domain.csproj[12-15]
- src/Core/src/Eventuous.Serialization/DefaultEventSerializer.cs[21-25]
- src/Core/src/Eventuous.Subscriptions/EventSubscription.cs[154-174]

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


View more (2)
4. Wrong DownloadContentAsync type ✓ Resolved 🐞 Bug ≡ Correctness
Description
StorageBlobsProjector.ModifyBlob and MyBookingsProjection.LoadDocument assign
BlobClient.DownloadContentAsync() directly to BlobDownloadResult and then read .Content/.Details,
but in this repo the API is used as returning a Response<BlobDownloadResult>, so the code won’t
compile and ETag access is incorrect.
Code

src/Azure/src/Eventuous.Azure.Storage.Blobs/StorageBlobsProjector.cs[R186-193]

+                BlobDownloadResult blobContent = await blobClient.DownloadContentAsync();
+
+                var content = blobContent.Content;
+                var current = projector.Deserialize(content);
+
+                var uploadOptions = new BlobUploadOptions {
+                    Conditions = new BlobRequestConditions { IfMatch = blobContent!.Details.ETag }
+                };
Evidence
The repository’s own StorageBlobsProjector tests demonstrate that DownloadContentAsync() returns a
response wrapper and that the BlobDownloadResult payload must be accessed via .Value; this
directly conflicts with the new projector and sample code patterns that assign the call result to
BlobDownloadResult and access .Content/.Details directly, which would not type-check under the
repo’s established SDK usage and leads to incorrect ETag access.

src/Azure/src/Eventuous.Azure.Storage.Blobs/StorageBlobsProjector.cs[184-193]
src/Azure/test/Eventuous.Tests.Azure.Storage.Blobs/StorageBlobsProjectorTests.cs[45-49]
samples/azure/Bookings/Application/Queries/MyBookingsProjection.cs[35-41]

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

## Issue description
`BlobClient.DownloadContentAsync()` is being treated as if it returns `BlobDownloadResult`, but in this repo it is consumed as `Response<BlobDownloadResult>`. This causes compilation failures in both the projector and the sample code, and it also results in incorrect access patterns for `Content` and `Details.ETag`.
## Issue Context
The test suite for the blob projector shows the correct usage pattern: store the `Response<BlobDownloadResult>` returned from `DownloadContentAsync()` and then access the actual `BlobDownloadResult` via `.Value`. Both `StorageBlobsProjector.ModifyBlob` and `MyBookingsProjection.LoadDocument` should be updated to match this established pattern (and pass through the appropriate cancellation token where applicable), then update subsequent reads to use the value object.
## Fix Focus Areas
- src/Azure/src/Eventuous.Azure.Storage.Blobs/StorageBlobsProjector.cs[184-200]
- samples/azure/Bookings/Application/Queries/MyBookingsProjection.cs[35-41]
- src/Azure/test/Eventuous.Tests.Azure.Storage.Blobs/StorageBlobsProjectorTests.cs[45-49]

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


5. Handler shared mutable state ✓ Resolved 🐞 Bug ☼ Reliability
Description
StorageBlobsProjector’s per-event Handler stores typedContext and blobClient in instance fields,
so overlapping Handle() calls can overwrite those fields and upload the wrong blob/state under
concurrent message processing.
Code

src/Azure/src/Eventuous.Azure.Storage.Blobs/StorageBlobsProjector.cs[R154-172]

+        private MessageConsumeContext<TEvent> typedContext = null!;
+        private BlobClient blobClient = null!;
+
+        public Handler(StorageBlobsProjector<T> storageBlobsProjector, Func<IMessageConsumeContext<TEvent>, T, ValueTask<T>> handler, GetBlobId<TEvent>? getBlobId) {
+            projector = storageBlobsProjector;
+            EventHandler = handler;
+            GetBlobId = getBlobId;
+        }
+
+        public async ValueTask<EventHandlingStatus> Handle(IMessageConsumeContext context) {
+            typedContext = context as MessageConsumeContext<TEvent> ?? new MessageConsumeContext<TEvent>(context);
+            var blobId = GetBlobId == null
+                ? context.Stream.GetId()
+                : await GetBlobId(typedContext);
+            var blobName = projector.GetBlobName(blobId, typedContext);
+
+            blobClient = projector.GetBlobContainerClient(blobName);
+
+            return await ModifyBlobWithRetries(projector._raceRetries);
Evidence
The new projector stores per-message state as fields, while the built-in Eventuous EventHandler
pattern uses a per-call local typedContext inside the handler function to avoid shared mutable
state.

src/Azure/src/Eventuous.Azure.Storage.Blobs/StorageBlobsProjector.cs[154-172]
src/Core/src/Eventuous.Subscriptions/Handlers/EventHandler.cs[41-48]

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

## Issue description
`StorageBlobsProjector<T>.Handler<TEvent>` keeps per-message state (`typedContext`, `blobClient`) as instance fields. Because `Handle()` is async and the handler instance is reused, concurrent/overlapping invocations can corrupt these fields.
### Issue Context
Refactor so per-call state is held in local variables and passed down the call chain (e.g., `ModifyBlobWithRetries(BlobClient blobClient, MessageConsumeContext<TEvent> ctx, int retries)`), removing mutable fields.
### Fix Focus Areas
- src/Azure/src/Eventuous.Azure.Storage.Blobs/StorageBlobsProjector.cs[149-214]
- src/Core/src/Eventuous.Subscriptions/Handlers/EventHandler.cs[41-48]
### Suggested change
- Remove `typedContext` and `blobClient` fields.
- Inside `Handle(...)`, create locals (`var typedContext = ...; var blobClient = ...;`) and pass them to helper methods.
- Keep behavior identical, just eliminate shared mutable state.

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


Grey Divider

Qodo Logo

Comment thread samples/azure/Bookings/Program.cs
Comment thread src/Azure/src/Eventuous.Azure.Storage.Blobs/StorageBlobsProjector.cs Outdated
Comment thread src/Azure/src/Eventuous.Azure.Storage.Blobs/StorageBlobsProjector.cs Outdated
@github-actions

github-actions Bot commented Jun 25, 2026

Copy link
Copy Markdown

Test Results

   69 files  + 47     69 suites  +47   1h 5m 21s ⏱️ + 52m 18s
  375 tests + 23    375 ✅ + 23  0 💤 ±0  0 ❌ ±0 
1 128 runs  +765  1 128 ✅ +765  0 💤 ±0  0 ❌ ±0 

Results for commit f21f2df. ± Comparison against base commit b5465c3.

♻️ This comment has been updated with latest results.

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