Skip to content

fix: dispose CodeQL-flagged locals, correct the blob projector README - #578

Merged
alexeyzimarev merged 3 commits into
devfrom
fix/codeql-local-not-disposed
Aug 21, 2026
Merged

fix: dispose CodeQL-flagged locals, correct the blob projector README#578
alexeyzimarev merged 3 commits into
devfrom
fix/codeql-local-not-disposed

Conversation

@alexeyzimarev

@alexeyzimarev alexeyzimarev commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Clears the four cs/local-not-disposed alerts 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 ObjectCreation of a library IDisposable type (user-defined types are excluded — "user types often have spurious IDisposable declarations"), with only Task and WebControl whitelisted. 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 why UploadAsync(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 into using var, matching what BlobStorageProjector itself already does.

Not real leaks: a MemoryStream over a byte[] 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. Utf8JsonWriter over a Stream rents buffers from ArrayPool<byte>.Shared and only Dispose returns 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 adding using. BinaryReader.Dispose() closes the underlying stream unless constructed with leaveOpen: 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 a byte[], 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.md had two problems:

  • 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: 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. The Deserialize rewrite is the riskier of the two and deserves a close look — happy to add a PersistedEvent round-trip test if you'd rather not merge it uncovered.

🤖 Generated with Claude Code

- 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>
@qodo-free-for-open-source-projects

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Fix CodeQL cs/local-not-disposed by disposing streams and avoiding BinaryReader copy

🐞 Bug fix 🕐 20-40 Minutes

Grey Divider

AI Description

• Dispose MemoryStream instances used in Azure blob upload tests
• Serialize with a disposed Utf8JsonWriter to return pooled buffers
• Deserialize directly from Stream to avoid closing callers and requiring seekable streams
Diagram

graph TD
  T1["BlobStorageProjectorTests"] --> A1["Azure BlobClient"] --> S1["Upload Stream"]
  ES["ElasticSerializer"] --> STJ["System.Text.Json"] --> CS[("Caller Stream")]
  subgraph Legend
    direction LR
    _mod["Module/Component"] ~~~ _io[("Stream")]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Keep BinaryReader but use leaveOpen + dispose
  • ➕ Maintains explicit byte[] buffering behavior (useful if callers provide non-JSON streams with odd behavior)
  • ➕ Satisfies CodeQL by disposing BinaryReader safely
  • ➖ Still copies the entire payload into memory
  • ➖ Still encourages length-based reads (often implies seekability assumptions)
  • ➖ More code/complexity than direct JsonSerializer.Deserialize(stream, ...)
2. Copy to MemoryStream then deserialize
  • ➕ Works for non-seekable streams while preserving ability to retry/rewind within the copy
  • ➕ Keeps the original caller stream untouched/position-independent after copy
  • ➖ Always incurs full buffering cost
  • ➖ Adds extra allocation and copy without a clear need for this serializer

Recommendation: Prefer the PR's approach: deserializing directly from the provided Stream removes unnecessary buffering, avoids accidental stream closure, and eliminates the prior seekable-stream assumption. The Serialize change (disposing Utf8JsonWriter) is also the correct ownership model because it returns pooled buffers without closing the underlying stream.

Files changed (2) +9 / -5

Bug fix (2) +9 / -5
BlobStorageProjectorTests.csDispose upload MemoryStreams in blob projection tests +4/-2

Dispose upload MemoryStreams in blob projection tests

• Replaces inline new MemoryStream(...) arguments with using-var streams before UploadAsync. This aligns the tests with expected caller ownership of streams and clears CodeQL local-not-disposed findings.

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

ElasticSerializer.csDispose Utf8JsonWriter and deserialize directly from Stream +5/-3

Dispose Utf8JsonWriter and deserialize directly from Stream

• Updates Deserialize to call JsonSerializer.Deserialize(stream, ...) directly, removing the BinaryReader-based full-buffer copy and the stream.Length dependency (improves compatibility with non-seekable streams). Updates Serialize to use a disposed Utf8JsonWriter so pooled buffers are returned and flushing is guaranteed without closing the caller's stream.

src/Experimental/src/Eventuous.ElasticSearch/Store/ElasticSerializer.cs

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

Copy link
Copy Markdown
Contributor

Code Review by Qodo

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

Grey Divider


Remediation recommended

1. UploadAsync awaits missing NoContext() 📘 Rule violation ☼ Reliability
Description
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.
Code

src/Azure/test/Eventuous.Tests.Azure.Storage.Blobs/BlobStorageProjectorTests.cs[R40-41]

+        using var stream = new MemoryStream(json);
+        await blobClient.UploadAsync(stream, overwrite: true);
Evidence
PR Compliance ID 2 requires async I/O awaits to use .NoContext(). The PR modifies the
UploadAsync calls and the resulting awaits still omit .NoContext() at the cited lines.

CLAUDE.md: All I/O Must Be Asynchronous and Use NoContext() for ConfigureAwait(false)
src/Azure/test/Eventuous.Tests.Azure.Storage.Blobs/BlobStorageProjectorTests.cs[37-42]
src/Azure/test/Eventuous.Tests.Azure.Storage.Blobs/BlobStorageProjectorTests.cs[71-77]

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

## 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


2. ElasticSerializer does sync stream I/O 📘 Rule violation ➹ Performance
Description
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.
Code

src/Experimental/src/Eventuous.ElasticSearch/Store/ElasticSerializer.cs[R14-16]

+        // 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);
Evidence
PR Compliance ID 2 forbids synchronous I/O where async alternatives exist. The PR changes
ElasticSerializer to call JsonSerializer.Deserialize(stream, ...) and to use Utf8JsonWriter
for stream output, both of which are synchronous stream I/O at the cited locations.

CLAUDE.md: All I/O Must Be Asynchronous and Use NoContext() for ConfigureAwait(false)
src/Experimental/src/Eventuous.ElasticSearch/Store/ElasticSerializer.cs[13-17]
src/Experimental/src/Eventuous.ElasticSearch/Store/ElasticSerializer.cs[35-38]

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

## 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


Grey Divider

Tip of the day
💡 Did you know, you can tweak Display preferences with a live preview to see your comment before it ships

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +40 to +41
using var stream = new MemoryStream(json);
await blobClient.UploadAsync(stream, overwrite: true);

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.

Remediation recommended

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

Comment on lines +14 to +16
// 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);

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.

Remediation recommended

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

@github-actions

Copy link
Copy Markdown

Test Results

   44 files     44 suites   12m 27s ⏱️
  547 tests   547 ✅ 0 💤 0 ❌
1 098 runs  1 098 ✅ 0 💤 0 ❌

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>
@alexeyzimarev alexeyzimarev changed the title fix: dispose locals flagged by CodeQL cs/local-not-disposed fix: dispose CodeQL-flagged locals, correct the blob projector README Aug 21, 2026
@alexeyzimarev
alexeyzimarev merged commit a178a37 into dev Aug 21, 2026
15 checks passed
@alexeyzimarev
alexeyzimarev deleted the fix/codeql-local-not-disposed branch August 21, 2026 11:23
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