Skip to content

Add SER308: flag the library's own Wait/WaitAll/TryWait helpers - #3180

Merged
mgravell merged 2 commits into
mainfrom
marc/obsolete-sync-wait
Aug 18, 2026
Merged

Add SER308: flag the library's own Wait/WaitAll/TryWait helpers#3180
mgravell merged 2 commits into
mainfrom
marc/obsolete-sync-wait

Conversation

@mgravell

@mgravell mgravell commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

Follow-up to #3179, reworked after review feedback — the original approach used [Obsolete]; this one does not. The old commits are gone from the branch; the discussion above is the record of why.

Wait, WaitAll and TryWait are what SER307 discourages everywhere else, reached through the API the library itself offers for it: the calling thread is held for the round-trip while the reply needs a thread of its own to be processed. Shipping SER307 while providing a blessed way to do the same thing was only ever half a position.

Why an analyzer rule and not [Obsolete]

The feedback was right: [Obsolete] reports CS0618, which every obsoletion from every source shares. A consumer who wants to silence this has to silence all of them, including deprecations in their own code and in unrelated packages. That is far too blunt for an opinion we are choosing to have.

Options considered and rejected:

  • ObsoleteAttribute.DiagnosticId — exists for precisely this, and would be the natural answer. It is net5+, and this library still targets netstandard2.0, so it would give a granular ID on some target frameworks and CS0618 on others.
  • A hybrid (attribute where available, analyzer elsewhere) — the attribute would need #if on the interface and on ConnectionMultiplexer's own members, since marking only the interface does not warn through the class; and on modern targets both mechanisms would report at the same location.
  • [Experimental] — granular, but these APIs are long-standing rather than preview, so it would be saying something untrue.

An ID of our own behaves identically on every target framework, sits with the rest of the SER3xx family, and has a docs page. Recorded in the descriptor as worth revisiting rather than settled: if netstandard2.0 and net4x are ever dropped, the attribute becomes strictly better — metadata, no analyzer needed, works in every tool — and SER308 can retire in its favour.

Two interfaces, not one

IRedisAsync declares these for database/server/subscriber calls; IConnectionMultiplexer declares its own, unrelated set. Both are gathered — testing only one would have left half the surface unguarded, which is exactly how the earlier attempt missed the multiplexer entirely.

Matching goes through FindImplementationForInterfaceMember as well as directly, because ConnectionMultiplexer is what Connect returns, making conn.Wait(task) the common shape with the class as the containing type.

Suppressions in this repo

Split into its own commit so the rule is reviewable on its own. Two kinds, distinguished in the comments rather than left to the reader:

  • ForwardingKeyPrefixed, RedisBase, the Retry*/MultiGroup* wrappers, the test fixture. They must implement the interfaces in full; forwarding a flagged member is not using it, and the implementation cannot be dropped while the interface declares it.
  • One real oneCursorEnumerable's synchronous scan path, reached only from a caller who asked for the sync API, where TryWait is what applies the configured timeout.

Test call sites are suppressed rather than rewritten to await, deliberately: the Wait helpers apply the multiplexer's timeout and a bare await does not, so rewriting would quietly change what those tests exercise. Happy to convert them if preferred.

Verification

  • 154 analyzer tests green, including negative cases: an unrelated Wait(Task) on someone else's type, Task.Wait(), and ManualResetEventSlim.Wait() are all left alone — the rule matches the interface members, not the name.
  • Whole repo builds clean on all TFMs.
  • Library test results unchanged vs main on the touched files.

@mgravell mgravell changed the title Deprecate the IRedisAsync Wait family Deprecate the Wait/WaitAll family Aug 18, 2026
@mgravell
mgravell force-pushed the marc/obsolete-sync-wait branch from 1e227e7 to e4c3ac8 Compare August 18, 2026 12:35
@mgravell

Copy link
Copy Markdown
Collaborator Author

For @NickCraver and @philon-msft's consideration: I'm "done" with sync-over-async; thoughts on just saying no, but gently? See also https://seredis.dev/SyncOverAsync

Wait, WaitAll and TryWait are what SER307 discourages everywhere else, reached
through the API the library itself offers for it: the calling thread is held for
the round-trip while the reply needs a thread of its own. Shipping SER307 while
providing a blessed way to do the same thing was only ever half a position.

An analyzer rule rather than [Obsolete], and the reason is entirely about how it
is turned off. [Obsolete] reports CS0618, which every obsoletion from every
source shares, so a consumer wanting to silence *this* has to silence *all* of
them - including deprecations in their own code and in unrelated packages. That
was the review feedback, and it is right.

ObsoleteAttribute.DiagnosticId exists for exactly this and would be the natural
answer, but it is net5+ and this library still targets netstandard2.0: it would
give a granular ID on some target frameworks and CS0618 on others. A hybrid was
considered and rejected - the attribute would need #if on the interface *and* on
ConnectionMultiplexer's own members, since marking only the interface does not
warn through the class, and on modern targets both mechanisms would report at
the same location. [Experimental] is granular but would be saying something
untrue; these APIs are long-standing, not preview.

Recorded in the descriptor as worth revisiting rather than settled: if
netstandard2.0 and net4x are ever dropped, the attribute becomes strictly better
and this rule can retire in its favour.

Two unrelated interfaces declare these - IRedisAsync for database, server and
subscriber calls, and IConnectionMultiplexer for its own - so both are gathered.
Matching goes through FindImplementationForInterfaceMember as well as directly,
because ConnectionMultiplexer is what Connect returns, making `conn.Wait(task)`
the common shape with the class as its containing type. Testing one interface
would have left half the surface unguarded, which is how the first attempt at
this missed the multiplexer entirely.
Two kinds, and the distinction is in the comments rather than left to the
reader. Most are decorator layers - KeyPrefixed, RedisBase, the Retry* and
MultiGroup* wrappers, the test fixture - which must implement the interfaces in
full and simply forward; forwarding a flagged member is not using it, and the
implementation cannot be dropped while the interface declares it.

The exception is CursorEnumerable's synchronous scan path, which is the real
thing: it is reached only from a caller who asked for the sync API, and TryWait
is what applies the configured timeout to it.

Test call sites are suppressed rather than rewritten to await, deliberately: the
Wait helpers apply the multiplexer's timeout, which a bare await does not, so
rewriting would quietly change what those tests exercise.
@mgravell mgravell changed the title Deprecate the Wait/WaitAll family Add SER308: flag the library's own Wait/WaitAll/TryWait helpers Aug 18, 2026
@mgravell
mgravell force-pushed the marc/obsolete-sync-wait branch from e4c3ac8 to 288c56a Compare August 18, 2026 15:09
@mgravell
mgravell merged commit 9201860 into main Aug 18, 2026
6 checks passed
@mgravell
mgravell deleted the marc/obsolete-sync-wait branch August 18, 2026 15:22
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