Skip to content

chore: replace remaining ConfigureAwait(false) with NoContext in library code - #584

Merged
alexeyzimarev merged 2 commits into
devfrom
chore/nocontext-stragglers
Aug 21, 2026
Merged

chore: replace remaining ConfigureAwait(false) with NoContext in library code#584
alexeyzimarev merged 2 commits into
devfrom
chore/nocontext-stragglers

Conversation

@alexeyzimarev

Copy link
Copy Markdown
Contributor

Summary

Closes out the last unresolved review comment from #561 (the .NoContext() convention flag on StoreFunctions): library code uses the NoContext() extension for async continuations, but eight await sites still called ConfigureAwait(false) directly.

  • Converts the stragglers in StoreFunctions, BaseTracer, ChannelExtensions, and the Sqlite/SqlServer projectors — the projectors' GetConnection awaits previously had no configuration at all.
  • Adds an IAsyncDisposable overload to TaskExtensions so await using (enumerator.NoContext()) can follow the same convention in BaseTracer.

No behavior change — every edit resolves to the same ConfigureAwait(false) under the hood.

Verification

  • Clean full-solution rebuild: 0 errors, no warnings in any touched file.
  • Core, Subscriptions, and Sqlite test suites: 204 tests, all passing.

The other high-severity bot findings on #561 were checked and need no code change: the ExchangeCache race was already fixed in the PR itself, and the RabbitMQ concurrent-publish flag is a false positive — verified against decompiled RabbitMQ.Client 7.2.1, which serializes the wire write internally (_confirmSemaphore + atomic frame buffer per message).

🤖 Generated with Claude Code

…ary code

Closes out the last unresolved review comment from #561: library code
uses the NoContext() extension for async continuations, but eight await
sites still called ConfigureAwait(false) directly. Converts them in
StoreFunctions, BaseTracer, ChannelExtensions, and the Sqlite/SqlServer
projectors (whose GetConnection awaits had no configuration at all), and
adds an IAsyncDisposable overload to TaskExtensions so `await using` can
follow the same convention.

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

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Replace remaining ConfigureAwait(false) usages with NoContext convention

✨ Enhancement 🕐 10-20 Minutes

Grey Divider

AI Description

• Standardize async continuations on .NoContext() across remaining library call sites.
• Add IAsyncDisposable.NoContext() to keep await using consistent with the convention.
• Align SQL projectors to avoid implicit context capture on connection creation and commands.
Diagram

graph TD
  SF["StoreFunctions"] --> NC(["TaskExtensions.NoContext"])
  BT["BaseTracer"] --> NC
  CE["ChannelExtensions"] --> NC
  SSP["SqlServerProjector"] --> NC
  SLSP["SqliteProjector"] --> NC
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Add a Roslyn analyzer to forbid ConfigureAwait(false) in library code
  • ➕ Prevents regressions automatically during CI/build
  • ➕ Removes need for recurring mechanical cleanup PRs
  • ➕ Can provide precise guidance/fixers to use .NoContext()
  • ➖ Added maintenance and packaging complexity
  • ➖ May require exceptions/allowlists for edge cases
  • ➖ Initial setup effort exceeds the scope of this small cleanup
2. Use a repository-wide codestyle rule + pre-commit/CI grep gate
  • ➕ Lightweight compared to a full analyzer
  • ➕ Fast to implement and understand
  • ➕ Keeps convention enforcement close to the repo
  • ➖ Cruder signal (false positives/negatives)
  • ➖ Less ergonomic than an analyzer (no code fix, weaker context)

Recommendation: This PR’s approach is the right immediate fix: it standardizes all remaining await sites on the established .NoContext() convention without changing behavior. Consider a follow-up analyzer or CI check only if regressions keep reappearing; otherwise the maintenance cost may not be worth it.

Files changed (6) +13 / -10

Enhancement (1) +3 / -0
TaskExtensions.csAdd NoContext overload for IAsyncDisposable +3/-0

Add NoContext overload for IAsyncDisposable

• Introduces 'IAsyncDisposable.NoContext()' returning 'ConfiguredAsyncDisposable' so 'await using (disposable.NoContext())' follows the same convention as task and async-enumerable usage.

src/Core/src/Eventuous.Shared/Tools/TaskExtensions.cs

Refactor (5) +10 / -10
BaseTracer.csUse NoContext for async enumerator disposal and MoveNextAsync +2/-2

Use NoContext for async enumerator disposal and MoveNextAsync

• Replaces 'ConfigureAwait(false)' calls with '.NoContext()' for both 'await using' on the enumerator and 'MoveNextAsync()' awaits, keeping tracing wrappers aligned with the library convention.

src/Core/src/Eventuous.Persistence/Diagnostics/Tracing/BaseTracer.cs

StoreFunctions.csUse NoContext for backward event enumeration +1/-1

Use NoContext for backward event enumeration

• Switches 'await foreach' from 'ConfigureAwait(false)' to '.NoContext(cancellationToken)' when reading events backwards to match the project’s async continuation convention.

src/Core/src/Eventuous.Persistence/EventStore/StoreFunctions.cs

ChannelExtensions.csAwait channel completion via NoContext +1/-1

Await channel completion via NoContext

• Replaces 'await source.Completion.ConfigureAwait(false)' with 'await source.Completion.NoContext()' to standardize continuation behavior when propagating channel failures.

src/Core/src/Eventuous.Subscriptions/Channels/ChannelExtensions.cs

SqlServerProjector.csStandardize projector awaits on NoContext (connection, handler, execution) +3/-3

Standardize projector awaits on NoContext (connection, handler, execution)

• Ensures connection acquisition, handler execution, and command execution all use '.NoContext()', including the previously unconfigured 'GetConnection' await used by 'await using'.

src/SqlServer/src/Eventuous.SqlServer/Projections/SqlServerProjector.cs

SqliteProjector.csStandardize projector awaits on NoContext (connection, handler, execution) +3/-3

Standardize projector awaits on NoContext (connection, handler, execution)

• Mirrors the SQL Server projector changes by applying '.NoContext()' to connection acquisition, handler execution, and command execution, removing implicit continuation behavior.

src/Sqlite/src/Eventuous.Sqlite/Projections/SqliteProjector.cs

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

qodo-free-for-open-source-projects Bot commented Aug 21, 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. SqlServerProjector disposal lacks NoContext ✓ Resolved 📘 Rule violation ☼ Reliability
Description
await using var connection = ... configures the GetConnection await with .NoContext(), but the
implicit DisposeAsync() from await using still runs without .NoContext(), potentially
capturing a synchronization context in library code. This violates the requirement that library
async continuations opt out of context capture consistently.
Code

src/SqlServer/src/Eventuous.SqlServer/Projections/SqlServerProjector.cs[37]

+        await using var connection = await ConnectionFactory.GetConnection(_connectionString, context.CancellationToken).NoContext();
Evidence
PR Compliance ID 2 requires library awaits to use .NoContext() (ConfigureAwait(false)). In the
cited code, .NoContext() is applied only to the GetConnection await inside the await using
initialization, but the compiler-generated async disposal path for await using (i.e., the implicit
DisposeAsync() continuation) is not explicitly configured with .NoContext() at those locations,
leaving a potential synchronization-context capture in library code.

CLAUDE.md: All I/O Must Be Async and Use NoContext (ConfigureAwait(false)): CLAUDE.md: All I/O Must Be Async and Use NoContext (ConfigureAwait(false)): CLAUDE.md: All I/O Must Be Async and Use NoContext (ConfigureAwait(false)): CLAUDE.md: All I/O Must Be Async and Use NoContext (ConfigureAwait(false))
src/SqlServer/src/Eventuous.SqlServer/Projections/SqlServerProjector.cs[37-40]
src/Sqlite/src/Eventuous.Sqlite/Projections/SqliteProjector.cs[37-40]

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

## Issue description
`await using var connection = await ... .NoContext();` does not apply `.NoContext()` to the implicit async disposal, so `DisposeAsync()` may capture a synchronization context.
## Issue Context
The repository convention (and this PR’s intent) is to use `.NoContext()` for library awaits/continuations, including async disposal when using `await using`.
## Fix Focus Areas
- src/SqlServer/src/Eventuous.SqlServer/Projections/SqlServerProjector.cs[36-41]
- src/Sqlite/src/Eventuous.Sqlite/Projections/SqliteProjector.cs[36-41]

ⓘ 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

@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown

Test Results

   45 files  + 22     45 suites  +22   13m 11s ⏱️ - 3m 59s
  583 tests +  4    583 ✅ +  9  0 💤 ±0  0 ❌  - 5 
1 152 runs  +562  1 152 ✅ +567  0 💤 ±0  0 ❌  - 5 

Results for commit be92051. ± Comparison against base commit 989e12d.

This pull request removes 5 and adds 9 tests. Note that renamed tests count towards both.
Eventuous.Tests.Azure.ServiceBus.IsSerialisableByServiceBus ‑ Passes(08/21/2026 11:58:16 +00:00)
Eventuous.Tests.Azure.ServiceBus.IsSerialisableByServiceBus ‑ Passes(08/21/2026 11:58:16)
Eventuous.Tests.Azure.ServiceBus.IsSerialisableByServiceBus ‑ Passes(f9d95c9d-b08a-4a92-8184-8af0b559b1b4)
Eventuous.Tests.Subscriptions.SequenceTests ‑ ShouldReturnFirstBefore(CommitPosition { Position: 0, Sequence: 1, Timestamp: 2026-08-21T11:59:35.7192750+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-08-21T11:59:35.7192750+00:00 }, CommitPosition { Position: 0, Sequence: 4, Timestamp: 2026-08-21T11:59:35.7192750+00:00 }, CommitPosition { Position: 0, Sequence: 6, Timestamp: 2026-08-21T11:59:35.7192750+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-08-21T11:59:35.7192750+00:00 })
Eventuous.Tests.Subscriptions.SequenceTests ‑ ShouldReturnFirstBefore(CommitPosition { Position: 0, Sequence: 1, Timestamp: 2026-08-21T11:59:35.7192750+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-08-21T11:59:35.7192750+00:00 }, CommitPosition { Position: 0, Sequence: 6, Timestamp: 2026-08-21T11:59:35.7192750+00:00 }, CommitPosition { Position: 0, Sequence: 8, Timestamp: 2026-08-21T11:59:35.7192750+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-08-21T11:59:35.7192750+00:00 })
Eventuous.Tests.Azure.ServiceBus.IsSerialisableByServiceBus ‑ Passes(08/21/2026 13:17:02 +00:00)
Eventuous.Tests.Azure.ServiceBus.IsSerialisableByServiceBus ‑ Passes(08/21/2026 13:17:02)
Eventuous.Tests.Azure.ServiceBus.IsSerialisableByServiceBus ‑ Passes(92e13a44-19e7-4c3b-b1d3-f0d35cae2ea0)
Eventuous.Tests.Subscriptions.SequenceTests ‑ ShouldReturnFirstBefore(CommitPosition { Position: 0, Sequence: 1, Timestamp: 2026-08-21T13:12:40.6806737+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-08-21T13:12:40.6806737+00:00 }, CommitPosition { Position: 0, Sequence: 4, Timestamp: 2026-08-21T13:12:40.6806737+00:00 }, CommitPosition { Position: 0, Sequence: 6, Timestamp: 2026-08-21T13:12:40.6806737+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-08-21T13:12:40.6806737+00:00 })
Eventuous.Tests.Subscriptions.SequenceTests ‑ ShouldReturnFirstBefore(CommitPosition { Position: 0, Sequence: 1, Timestamp: 2026-08-21T13:12:40.6806737+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-08-21T13:12:40.6806737+00:00 }, CommitPosition { Position: 0, Sequence: 6, Timestamp: 2026-08-21T13:12:40.6806737+00:00 }, CommitPosition { Position: 0, Sequence: 8, Timestamp: 2026-08-21T13:12:40.6806737+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-08-21T13:12:40.6806737+00:00 })
Eventuous.Tests.Subscriptions.SequenceTests ‑ ShouldReturnFirstBefore(CommitPosition { Position: 0, Sequence: 1, Timestamp: 2026-08-21T13:12:41.6026064+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-08-21T13:12:41.6026064+00:00 }, CommitPosition { Position: 0, Sequence: 4, Timestamp: 2026-08-21T13:12:41.6026064+00:00 }, CommitPosition { Position: 0, Sequence: 6, Timestamp: 2026-08-21T13:12:41.6026064+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-08-21T13:12:41.6026064+00:00 })
Eventuous.Tests.Subscriptions.SequenceTests ‑ ShouldReturnFirstBefore(CommitPosition { Position: 0, Sequence: 1, Timestamp: 2026-08-21T13:12:41.6026064+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-08-21T13:12:41.6026064+00:00 }, CommitPosition { Position: 0, Sequence: 6, Timestamp: 2026-08-21T13:12:41.6026064+00:00 }, CommitPosition { Position: 0, Sequence: 8, Timestamp: 2026-08-21T13:12:41.6026064+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-08-21T13:12:41.6026064+00:00 })
Eventuous.Tests.Subscriptions.SequenceTests ‑ ShouldReturnFirstBefore(CommitPosition { Position: 0, Sequence: 1, Timestamp: 2026-08-21T13:12:49.3073757+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-08-21T13:12:49.3073757+00:00 }, CommitPosition { Position: 0, Sequence: 4, Timestamp: 2026-08-21T13:12:49.3073757+00:00 }, CommitPosition { Position: 0, Sequence: 6, Timestamp: 2026-08-21T13:12:49.3073757+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-08-21T13:12:49.3073757+00:00 })
Eventuous.Tests.Subscriptions.SequenceTests ‑ ShouldReturnFirstBefore(CommitPosition { Position: 0, Sequence: 1, Timestamp: 2026-08-21T13:12:49.3073757+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-08-21T13:12:49.3073757+00:00 }, CommitPosition { Position: 0, Sequence: 6, Timestamp: 2026-08-21T13:12:49.3073757+00:00 }, CommitPosition { Position: 0, Sequence: 8, Timestamp: 2026-08-21T13:12:49.3073757+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-08-21T13:12:49.3073757+00:00 })

♻️ This comment has been updated with latest results.

`await using var` always awaits the implicit DisposeAsync() unconfigured —
the declaration form cannot be combined with NoContext(), only the block
form can. Splits acquisition from disposal in the projectors and the
paged-read enumerator so the disposal await also opts out of context
capture, matching BaseTracer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@alexeyzimarev
alexeyzimarev merged commit 805d1ea into dev Aug 21, 2026
17 checks passed
@alexeyzimarev
alexeyzimarev deleted the chore/nocontext-stragglers branch August 21, 2026 13:39
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