refactor!: freeze a clean v4 public API surface#361
Merged
Conversation
Now-or-never cleanup before the 4.0.0 contract freezes: - Internalize the ActionTaskWrapper/ItemTaskWrapper structs and demote the abstract processor bases' protected plumbing (TaskWrappers, EnumerableTaskCompletionSources, CancellationToken, constructors, DisposeAsyncCore) to private protected - none of it was usable outside the assembly because Process() is internal abstract. - Seal every leaf processor and builder class. - Move IAsyncEnumerableProcessor to EnumerableAsyncProcessor.Interfaces and make both variants IAsyncDisposable/IDisposable. The six async-enumerable processors now dispose their linked CancellationTokenSource when ExecuteAsync completes (previously leaked a registration on the caller's token) and support explicit disposal. - Honor scheduleOnThreadPool on the bounded parallel paths (it was silently ignored whenever maxConcurrency was set). - Remove the parameterized no-selector IAsyncEnumerable ProcessInParallel overloads whose maxConcurrency/scheduleOnThreadPool did nothing. - Keep binary compatibility with assemblies compiled against v3, notably TUnit.Engine, which this repo's own test runner depends on: restore the parameterless ProcessInParallel(items, ct) collect overload and add ProcessInParallel(int) forwarders on the four enumerable builders. V3BinaryCompatibilityTests pins the exact signatures; this also fixes --treenode-filter discovery, broken since the v4 API consolidation. - Packaging: generate XML docs (IntelliSense was missing from the package), mark AOT-compatible, merge duplicate metadata groups. - Docs: correct stale CLAUDE.md/README claims (RateLimitedParallel strategy, minimumIterationTime model, disposal contract) and expand the migration guide.
Mark the no-selector ProcessInParallel collect overload [Obsolete] so source consumers migrate off it, and hide the builder ProcessInParallel(int) forwarders with [EditorBrowsable(Never)]. The forwarders cannot be [Obsolete]: exact-int overload resolution prefers them for the idiomatic ProcessInParallel(5) call, which would warn on every normal bounded use.
This was referenced Jul 21, 2026
Greptile SummaryThis PR freezes the public API for version 4 and updates processor lifecycle behavior. The main changes are:
Confidence Score: 5/5This looks safe to merge.
Important Files Changed
Reviews (3): Last reviewed commit: "fix: DisposeAsync awaits the in-flight r..." | Re-trigger Greptile |
This was referenced Jul 21, 2026
Closed
…osed Explicit Dispose/DisposeAsync on the six IAsyncEnumerableProcessor implementations previously disposed the linked CancellationTokenSource without cancelling it, so an active ExecuteAsync kept running and later caller-token cancellation no longer reached the pipeline (disposal had severed the link). Disposal now cancels first; the completion-path self-dispose skips the cancel since nothing is left in flight. Both paths race-safely share an Interlocked guard. Builders capture the processor token up front so late selector invocations never touch a disposed source. Regression test: disposing mid-run terminates the pipeline with OperationCanceledException.
Async disposal previously cancelled the linked source and returned immediately; a selector already running could still produce side effects after 'await processor.DisposeAsync()'. Mirror ProcessorLifecycle: void processors track their execution task, streaming processors complete a signal when enumeration finishes, and DisposeAsync cancels then waits up to the shared 30-second disposal window before releasing the source. Synchronous Dispose still cancels without blocking.
This was referenced Jul 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Now-or-never API cleanup before 4.0.0 freezes the contract, from the pre-release audit.
API surface (breaking)
ActionTaskWrapper,ItemTaskWrapper× arities). Both base hierarchies declareinternal abstract Task Process(), so no external subclass was ever possible — everyprotectedmember on the abstract bases was unreachable noise about to be frozen. Those members (TaskWrappers,EnumerableTaskCompletionSources,CancellationToken, constructors,DisposeAsyncCore) are nowprivate protected. Tests keep access viaInternalsVisibleTo.sealed— zero-risk today (internal ctors/members already prevented subclassing), binary-breaking to do later.IAsyncEnumerableProcessormoved toEnumerableAsyncProcessor.Interfaces(was declared in theExtensionsnamespace despite living inInterfaces/) and both variants now extendIAsyncDisposable, IDisposable.IAsyncEnumerableProcessor<TOutput>is also covariant now.IAsyncEnumerable<T>.ProcessInParallel(maxConcurrency, …)overloads removed — they buffered the stream into a list and silently ignoredmaxConcurrency/scheduleOnThreadPool.Behavior fixes
CancellationTokenSourceleak fixed: the async-enumerable builder path created a linked CTS nobody ever disposed, pinning a registration on the caller's token for its lifetime. The six async-enumerable processors now dispose it whenExecuteAsynccompletes and support explicitDispose/DisposeAsync(idempotent;await usingwithout execution is safe).scheduleOnThreadPoolis honored on bounded paths — previously ignored whenevermaxConcurrencywas set (both processors and the extension overload).TUnit binary compatibility (important)
TUnit.Engineis compiled against EnumerableAsyncProcessor 3.8.4, and any locally-referenced v4 build shadows it (same assembly identity). Two members TUnit binds to had been removed:ItemActionAsyncProcessorBuilder<,>.ProcessInParallel(int)(removed in the v4 consolidation — this had already silently broken--treenode-filterdiscovery on main) and the parameterlessProcessInParallel<T>(IAsyncEnumerable<T>, CancellationToken). Without them, TUnit test discovery dies withMissingMethodException— in this repo and for any TUnit user who adds EAP 4.0 to a test project.This PR keeps two functional, documented, transitional compat members:
ProcessInParallel(int)forwarders on the four enumerable builders, and the parameterless collect overload (docs state plainly that it parallelizes nothing).V3BinaryCompatibilityTestspins the exact signatures with an explanation. They can be dropped once TUnit ships a build compiled against v4 — or better, internalizes EAP intoTUnit.Engine(ILRepack), which ends the diamond dependency permanently. Side effect:--treenode-filterworks again for the first time since the consolidation.Packaging
GenerateDocumentationFile— the package previously shipped no IntelliSense XML despite extensive///docs.IsAotCompatible(library uses zero reflection).PropertyGroups; removed dead privateToAsyncEnumerablehelper.PublicAPI.Shipped.txtregenerated (176→181 entries reflecting all of the above).Docs
levelOfParallelismrename claim (InParallelAsynckeeps that parameter name).RateLimitedParallelstrategy andminimumIterationTimeslot-dwell description (now token bucket); documented the TUnit dogfooding constraint.Tests
1659 passing across net8.0/net9.0/net10.0 (removed 2 tests for deleted overloads; added 2 async-enumerable disposal regression tests + the TUnit compat guard). Filtered discovery verified working.