feat: throw ObjectDisposedException on use-after-dispose; test the guard (#205) - #318
Merged
Merged
Conversation
…ard (#205) Adds a real dispose contract to the three base classes: once disposed, every public entry point (ExtractAsync / LoadAsync / TransformAsync, all overloads) throws ObjectDisposedException via a new private ThrowIfDisposed(). This gives the Dispose(bool) idempotency flag a real reader, so its guard is no longer "equivalent because _disposed is never read": DisposedGuardTests now kills the guard's negation, the _disposed assignment, and the whole-method-body removal by asserting use-after-dispose throws (every overload, after both Dispose and DisposeAsync) and that Dispose stays idempotent. The 12 ThrowIfDisposed call sites are each covered. Only the guard's `{ return; }` block/return removal stays annotated — dropping it just re-runs the idempotent `_disposed = true`, which is genuinely unobservable. Score ~97% (97.06%), 370 tests. Remaining survivors: SystemProgressTimer/timer.Start timing only. Remaining // Stryker disable: the EnsureStarted early-return, this guard's redundant block/return, the sink's redundant cancellation check, and Report's unreachable overflow boundary — all provable equivalents. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
BenchmarkDotNet
Details
| Benchmark suite | Current: 993cf8c | Previous: 732efdd | Ratio |
|---|---|---|---|
Wolfgang.Etl.Abstractions.Benchmarks.ExtractorBenchmarks.Extract_NoProgress(RecordCount: 1000) |
32732.0576171875 ns (± 80.74249824063652) |
32688.512502034504 ns (± 213.01288290577438) |
1.00 |
Wolfgang.Etl.Abstractions.Benchmarks.ExtractorBenchmarks.Extract_WithProgress(RecordCount: 1000) |
34869.42077636719 ns (± 107.25465551142001) |
34913.1728108724 ns (± 92.90601829761779) |
1.00 |
Wolfgang.Etl.Abstractions.Benchmarks.ExtractorBenchmarks.Extract_NoProgress(RecordCount: 100000) |
3428437.5520833335 ns (± 46239.06222200528) |
3227835.359375 ns (± 5331.793855491133) |
1.06 |
Wolfgang.Etl.Abstractions.Benchmarks.ExtractorBenchmarks.Extract_WithProgress(RecordCount: 100000) |
3446404.3619791665 ns (± 18173.18798666761) |
3403880.4401041665 ns (± 29793.796946240178) |
1.01 |
Wolfgang.Etl.Abstractions.Benchmarks.PipelineBenchmarks.FluentPipeline(RecordCount: 1000) |
32691.82314046224 ns (± 165.5095721903417) |
31655.328531901043 ns (± 117.35331683022335) |
1.03 |
Wolfgang.Etl.Abstractions.Benchmarks.PipelineBenchmarks.ManualComposition(RecordCount: 1000) |
32335.262837727863 ns (± 149.18971403849528) |
32588.058044433594 ns (± 149.83187729728172) |
0.99 |
Wolfgang.Etl.Abstractions.Benchmarks.PipelineBenchmarks.BaseClassComposition(RecordCount: 1000) |
71301.486328125 ns (± 334.2925739232508) |
70625.31201171875 ns (± 1269.4931495812575) |
1.01 |
Wolfgang.Etl.Abstractions.Benchmarks.PipelineBenchmarks.FluentPipeline(RecordCount: 100000) |
3201795.609375 ns (± 1710.5236012409246) |
3210122.2981770835 ns (± 297.86708111041185) |
1.00 |
Wolfgang.Etl.Abstractions.Benchmarks.PipelineBenchmarks.ManualComposition(RecordCount: 100000) |
3209299.3411458335 ns (± 8392.07769805364) |
3240046.2122395835 ns (± 4670.818271378057) |
0.99 |
Wolfgang.Etl.Abstractions.Benchmarks.PipelineBenchmarks.BaseClassComposition(RecordCount: 100000) |
6853578.205729167 ns (± 790.9315334288442) |
6896450.057291667 ns (± 10313.052399783468) |
0.99 |
This comment was automatically generated by workflow using github-action-benchmark.
This was referenced Jul 24, 2026
This was referenced Aug 5, 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.
Summary
Follow-up to #317 (merged). Adds a real use-after-dispose contract and converts the last testable
_disposed-guard mutants from annotated-equivalents into ones killed by real tests.Refs #205.
What changed
ThrowIfDisposed()onLoaderBase/ExtractorBase/TransformerBase, called at the top of every public entry point (ExtractAsync/LoadAsync/TransformAsync, all 4 overloads each). Once disposed, they throwObjectDisposedExceptioninstead of silently running.DisposedGuardTestscovers every overload after bothDispose()andDisposeAsync(), plus idempotency and the live path.Why (mutation testing)
Previously the
Dispose(bool)idempotency guard was annotated equivalent because_disposedhad no reader. Now it has one, so the guard's negation, the_disposed = trueassignment, and the whole-method-body removal are all killed by real tests. Only the guard's{ return; }block/return stays annotated — dropping it merely re-runs the idempotent_disposed = true, which is genuinely unobservable.State
SystemProgressTimerstart/stop/dispose + basetimer.Start— timing-only, deliberately unmasked.// Stryker disable: theEnsureStartedearly-return, this guard's redundant block/return, the sink's redundant cancellation check, andReport's unreachable overflow boundary — all provable equivalents.Note
ObjectDisposedExceptionon use-after-dispose is a behavior addition (previously such calls ran). No public signature changes.