fix: dispose CodeQL-flagged locals, correct the blob projector README - #578
Conversation
- 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 <noreply@anthropic.com>
PR Summary by QodoFix CodeQL cs/local-not-disposed by disposing streams and avoiding BinaryReader copy
AI Description
Diagram
High-Level Assessment
Files changed (2)
|
Code Review by Qodo
1. UploadAsync awaits missing NoContext()
|
| using var stream = new MemoryStream(json); | ||
| await blobClient.UploadAsync(stream, overwrite: true); |
There was a problem hiding this comment.
1. uploadasync awaits missing nocontext() 📘 Rule violation ☼ Reliability
The newly modified await blobClient.UploadAsync(...) calls don't use .NoContext(), which breaks the repo convention for avoiding captured synchronization context on I/O awaits. This can increase deadlock risk and reduce scalability in async flows.
Agent Prompt
## Issue description
The modified `UploadAsync` awaits do not use `.NoContext()`.
## Issue Context
Compliance requires async I/O awaits to use `.NoContext()` (`ConfigureAwait(false)`) per repository convention.
## Fix Focus Areas
- src/Azure/test/Eventuous.Tests.Azure.Storage.Blobs/BlobStorageProjectorTests.cs[37-42]
- src/Azure/test/Eventuous.Tests.Azure.Storage.Blobs/BlobStorageProjectorTests.cs[71-77]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| // 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); |
There was a problem hiding this comment.
2. elasticserializer does sync stream i/o 📘 Rule violation ➹ Performance
ElasticSerializer.Deserialize and Serialize perform synchronous read/write operations against Stream using JsonSerializer.Deserialize(...) and Utf8JsonWriter, despite async alternatives existing. This violates the async-I/O compliance requirement and can cause thread blocking under load.
Agent Prompt
## Issue description
The serializer performs synchronous stream I/O (`JsonSerializer.Deserialize(stream, ...)` and `Utf8JsonWriter`-based serialization), which can block threads.
## Issue Context
Compliance requires I/O to be asynchronous where async alternatives exist.
## Fix Focus Areas
- src/Experimental/src/Eventuous.ElasticSearch/Store/ElasticSerializer.cs[13-26]
- src/Experimental/src/Eventuous.ElasticSearch/Store/ElasticSerializer.cs[28-38]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
Test Results 44 files 44 suites 12m 27s ⏱️ Results for commit bd6b98e. |
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<T>` - 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 <noreply@anthropic.com>
Clears the four
cs/local-not-disposedalerts on the Code Quality rule page, and fixes two code samples in the blob projector README that don't compile. Two alerts came from #550 (blob storage projection); the two Elastic ones are pre-existing and just share the rule page.What the rule flags
Sources are any
ObjectCreationof a libraryIDisposabletype (user-defined types are excluded — "user types often have spurious IDisposable declarations"), with onlyTaskandWebControlwhitelisted. Passing the object to a library method is not a sink, because CodeQL can't see the callee body to know whether it disposes the argument. That's whyUploadAsync(new MemoryStream(...))trips it.Every object creation added by 0f19628 and 1c2fa15 was checked against that definition; the two MemoryStreams are the only hits from the PR.
Disposal changes
BlobStorageProjectorTests.cs— hoisted both upload streams intousing var, matching whatBlobStorageProjectoritself already does.Not real leaks: a
MemoryStreamover abyte[]owns no unmanaged resources. The fix is still correct — the Azure SDK does not dispose caller-supplied streams, so ownership genuinely was ours.ElasticSerializer.Serialize(Utf8JsonWriter) — a real, if soft, leak.Utf8JsonWriterover aStreamrents buffers fromArrayPool<byte>.Sharedand onlyDisposereturns them. Disposal flushes and does not close the underlying stream, so it's safe on a serializer contract.ElasticSerializer.Deserialize(BinaryReader) — this one can't be fixed by addingusing.BinaryReader.Dispose()closes the underlying stream unless constructed withleaveOpen: true, and closing the Elasticsearch client's stream from inside a serializer would be a genuine bug. The reader also existed only to copy the whole payload into abyte[], so it's removed in favour of deserializing straight from the stream.Side benefit: the old code called
(int)stream.Length, which requires a seekable stream. The new path doesn't. Behaviour is otherwise identical for a stream positioned at 0.README fix
The "Custom blob naming" section in
Eventuous.Azure.Storage.Blobs/README.mdhad two problems:typeof(T)doesn't compile in the example's context —Tis the base class's type parameter and isn't in scope inside a concrete projector deriving fromBlobStorageProjector<T>.GetBlobNameoverloads is misleading: 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.The overrides are now shown as the alternatives they are, with a note on which wins when both are present. The same two bugs were mirrored on the docs site and are fixed in Eventuous/eventuous-docs#10.
Verification
Eventuous.Tests.Azure.Storage.Blobs— 21/21 passed against Azurite (net10.0).Eventuous.ElasticSearch— builds clean, 0 warnings.Reviewer note
There are no tests for
ElasticSerializer(src/Experimental/test/holds only Spyglass projects), so that change is verified by compilation and reading only. TheDeserializerewrite is the riskier of the two and deserves a close look — happy to add aPersistedEventround-trip test if you'd rather not merge it uncovered.🤖 Generated with Claude Code