From 1d250ecb0d73f7add1014a704701d2afaaf6c541 Mon Sep 17 00:00:00 2001 From: Alexey Zimarev Date: Fri, 21 Aug 2026 12:59:32 +0200 Subject: [PATCH 1/2] fix: dispose locals flagged by CodeQL cs/local-not-disposed - Blob projector tests: hoist the upload MemoryStreams into `using var`, matching what BlobStorageProjector itself already does. The Azure SDK does not dispose caller-supplied streams, so ownership was ours. - ElasticSerializer: dispose the Utf8JsonWriter. It rents buffers from ArrayPool and only Dispose returns them. Disposal flushes and does not close the caller's stream, so it is safe on a serializer contract. - ElasticSerializer: drop the BinaryReader and deserialize straight from the stream. Adding `using` there would have closed the caller's stream, and the reader only added a full copy of the payload. This also removes the stream.Length call, so non-seekable streams now work. Co-Authored-By: Claude Opus 5 --- .../BlobStorageProjectorTests.cs | 6 ++++-- .../Eventuous.ElasticSearch/Store/ElasticSerializer.cs | 8 +++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Azure/test/Eventuous.Tests.Azure.Storage.Blobs/BlobStorageProjectorTests.cs b/src/Azure/test/Eventuous.Tests.Azure.Storage.Blobs/BlobStorageProjectorTests.cs index 0dad57381..54269184b 100644 --- a/src/Azure/test/Eventuous.Tests.Azure.Storage.Blobs/BlobStorageProjectorTests.cs +++ b/src/Azure/test/Eventuous.Tests.Azure.Storage.Blobs/BlobStorageProjectorTests.cs @@ -37,7 +37,8 @@ BlobContainerClient GetContainer(string containerName) => async Task SetupExistingBlob(string containerName, string blobName, TState initialState) { var blobClient = GetContainer(containerName).GetBlobClient(blobName); var json = JsonSerializer.SerializeToUtf8Bytes(initialState); - await blobClient.UploadAsync(new MemoryStream(json), overwrite: true); + using var stream = new MemoryStream(json); + await blobClient.UploadAsync(stream, overwrite: true); } /// @@ -71,7 +72,8 @@ Func OverwriteBlob(string containerName, string blobName) => async () => { var modifiedState = new ConcurrentState { Value = 999 }; var modifiedJson = JsonSerializer.SerializeToUtf8Bytes(modifiedState); var blobClient = GetContainer(containerName).GetBlobClient(blobName); - await blobClient.UploadAsync(new MemoryStream(modifiedJson), overwrite: true); + using var stream = new MemoryStream(modifiedJson); + await blobClient.UploadAsync(stream, overwrite: true); }; // ========== SYNC STATE HANDLER TESTS ========== diff --git a/src/Experimental/src/Eventuous.ElasticSearch/Store/ElasticSerializer.cs b/src/Experimental/src/Eventuous.ElasticSearch/Store/ElasticSerializer.cs index 10a96d475..9bbbde03c 100644 --- a/src/Experimental/src/Eventuous.ElasticSearch/Store/ElasticSerializer.cs +++ b/src/Experimental/src/Eventuous.ElasticSearch/Store/ElasticSerializer.cs @@ -11,8 +11,9 @@ public class ElasticSerializer(IElasticsearchSerializer builtIn, JsonSerializerO readonly ITypeMapper _typeMapper = typeMapper ?? TypeMap.Instance; public object Deserialize(Type type, Stream stream) { - var reader = new BinaryReader(stream); - var obj = JsonSerializer.Deserialize(reader.ReadBytes((int)stream.Length), type, _options); + // Read the stream directly: a BinaryReader here would either close the caller's stream when + // disposed, or leak its buffers when not, and it only added a full copy of the payload + var obj = JsonSerializer.Deserialize(stream, type, _options); if (type != typeof(PersistedEvent)) return obj!; @@ -31,7 +32,8 @@ public void Serialize(T data, Stream stream, SerializationFormatting formatti return; } - var writer = new Utf8JsonWriter(stream); + // Disposing the writer returns its pooled buffers and flushes; it doesn't close the caller's stream + using var writer = new Utf8JsonWriter(stream); JsonSerializer.Serialize(writer, data, _options); } From 16e834367ce0316d1477a25ea27452539c73c8d3 Mon Sep 17 00:00:00 2001 From: Alexey Zimarev Date: Fri, 21 Aug 2026 13:17:35 +0200 Subject: [PATCH 2/2] docs: fix the blob projector README custom naming example Two problems in the "Custom blob naming" section, both mirrored in the docs site page (Eventuous/eventuous-docs#10): - `typeof(T)` doesn't compile in the example's context: `T` is the base class's type parameter and isn't in scope inside a concrete projector deriving from `BlobStorageProjector` - overriding both `GetBlobName` overloads is misleading, because the default two-argument implementation delegates to the one-argument one, so a one-argument override is never called once the two-argument one is replaced Show the two overrides as the alternatives they are, and say which one wins when both are present. Co-Authored-By: Claude Opus 5 --- .../Eventuous.Azure.Storage.Blobs/README.md | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/Azure/src/Eventuous.Azure.Storage.Blobs/README.md b/src/Azure/src/Eventuous.Azure.Storage.Blobs/README.md index 00a77abd1..014bf72a7 100644 --- a/src/Azure/src/Eventuous.Azure.Storage.Blobs/README.md +++ b/src/Azure/src/Eventuous.Azure.Storage.Blobs/README.md @@ -70,24 +70,29 @@ Note, this means the idempotency is weaker as only the last message ID is checke ### Custom blob naming -By default, blob names are generated using `GetBlobName(string id)` which creates names in the format `{id}/{T}.json`, where `id` defaults to the stream ID from `context.Stream.GetId()`. +By default, blob names are generated using `GetBlobName(string id)` which creates names in the format `{id}/{StateType}.json`, where `id` defaults to the stream ID from `context.Stream.GetId()`. You can customize blob naming in two ways: **1. Override the virtual methods globally for all events:** ```csharp -protected override string GetBlobName(string id, IMessageConsumeContext context) { - // Use stream name and type in the path - var streamName = context.Stream.ToString(); - return $"projections/{streamName}/{id}.json"; -} +public class BookingProjection : BlobStorageProjector { + // ... -protected override string GetBlobName(string id) { - return $"{id}/{typeof(T).Name}.json"; + protected override string GetBlobName(string id) => $"bookings/{id}.json"; } ``` +When the blob name depends on the event, override the overload that takes the consume context instead: + +```csharp +protected override string GetBlobName(string id, IMessageConsumeContext context) + => $"projections/{context.Stream}/{id}.json"; +``` + +The default implementation of the two-argument overload calls the one-argument overload, so overriding the two-argument version replaces the naming completely — a one-argument override is then never called. + **2. Override blob ID per event handler using `getBlobId`:** ```csharp