Skip to content

Release v0.20.0 — retry seam, middleware, error aggregation - #342

Merged
Chris-Wolfgang merged 13 commits into
mainfrom
vNext-plus-one
Jul 30, 2026
Merged

Release v0.20.0 — retry seam, middleware, error aggregation#342
Chris-Wolfgang merged 13 commits into
mainfrom
vNext-plus-one

Conversation

@Chris-Wolfgang

Copy link
Copy Markdown
Owner

Release: Wolfgang.Etl.Abstractions 0.20.0

Minor release. Bundles the 0.20.0 line from vNext-plus-onemain. Purely additive — no breaking change (validates against the published 0.19.0 baseline; CompatibilitySuppressions.xml removed).

What's in it

Release-prep done

  • Baseline 0.19.0; CompatibilitySuppressions.xml removed (additive vs 0.19.0). Pack + Package Validation green.
  • CHANGELOG [Unreleased][0.20.0] - 2026-07-30.
  • PublicAPI Unshipped → Shipped (216 entries, ordinal). Version 0.20.0.
  • 452 tests, 100% mutation, all 11 TFMs; code-review findings addressed.

⚠️ Timing — do NOT tag until tomorrow night

0.19.0's NuGet date is 2026-07-29, so v0.20.0 must be tagged at/after 2026-07-30T00:00Z to land on a distinct NuGet date. Merging this PR is safe anytime (release.yaml fires only on release:published); create the v0.20.0 tag tomorrow night.

No protected files in the diff → no admin bypass needed.

Chris-Wolfgang and others added 11 commits July 27, 2026 21:23
…ses (#94)

Add a protected virtual WrapWorkerExecution hook to ExtractorBase, LoaderBase, and
TransformerBase, wrapped around every worker invocation (both the no-progress and
with-progress paths). The default implementation is a no-op, so behaviour is unchanged;
an override receives a re-invocable worker factory (call it again to retry a transient
failure) and stream-level semantics are documented on the method.

Kept dependency-free per the 0.20.0 decision (Option 1): Abstractions stays zero-dep and
roots 8 downstream packages, so no Polly dependency here. A ready-made Polly integration
will ship as a separate opt-in Wolfgang.Etl.Polly package (#332).

- 13 new RetrySeamTests (default no-op, seam-invoked-once on both paths, factory
  re-invocability, genuine transient-failure recovery, null guard) across all 3 bases.
  426 unit tests pass; Stryker 100.00% (0 survivors) on the full project.
- PublicAPI.Unshipped updated with the 3 protected virtual entries (RS0017-validated).
- CHANGELOG [Unreleased] Added entry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add IItemMiddleware<T> + MiddlewareResult<T> (Continue/Drop) and WithMiddleware(...)
extensions that attach cross-cutting per-item behaviour (logging, validation, metrics,
throttling, dedup) to any IAsyncEnumerable<T> stream — extractor/transformer output or
loader input — without changing the component, and compose inside an EtlPipeline via
Through(s => s.WithMiddleware(...)). Single and ordered-chain overloads; a dropped item
short-circuits the rest of the chain. Dependency-free.

- 15 MiddlewareTests (transform, drop, chain order, stop-at-drop, empty chain, token
  flow, pipeline composition, null guards) + MiddlewareResult value semantics.
  441 unit tests pass; Stryker 100.00% (0 survivors).
- PublicAPI.Unshipped updated (RS0016 completeness + RS0017 correctness validated).
- CHANGELOG [Unreleased] Added entry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fix the base-class asymmetry: CurrentErrorItemCount lived only on the concrete base
classes, so EtlPipelineProgress.ErrorItemCount could only surface the extractor's errors
(Through takes ITransformAsync, an interface that didn't expose it).

- Add IReportsItemErrors { int CurrentErrorItemCount { get; } }, implemented by
  ExtractorBase, LoaderBase, TransformerBase (property already existed — additive).
- EtlRunState now holds a list of error-count readers and sums them in Snapshot().
- Register a reader from every stage that reports errors: the extractor (From), each
  transformer (Through, via 'is IReportsItemErrors'), and the loader (sink). ErrorItemCount
  now reflects items ANY stage's error policy discarded. Pre-1.0 behaviour change.

- 5 AggregateErrorsTests (sum across all three stages, delegate stage contributes 0, zero
  when nothing reports, plain-ITransformAsync overload, interface implemented by all bases).
  446 unit tests pass; Stryker 100.00% (0 survivors).
- PublicAPI.Unshipped updated (RS0016 completeness + RS0017 correctness validated).
- CHANGELOG [Unreleased] Added + Changed entries.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts:
#	CHANGELOG.md
#	src/Wolfgang.Etl.Abstractions/EtlPipeline/EtlRunState.cs
#	src/Wolfgang.Etl.Abstractions/PublicAPI.Unshipped.txt
Reconcile complete: the 0.20 feature stack (#94 retry seam / #93 middleware / #335 error
aggregation) merged in, plus vNext's 0.19.0 prep + #338 clock seam merged down. CHANGELOG
[Unreleased] holds the 0.20 features; [0.19.0] section carried in. PublicAPI merged
(features in Unshipped, Report ctor promoted to Shipped). 452 tests pass; all 11 TFMs
clean; pack + Package Validation green vs 0.18.1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- MiddlewareExtensions: validate arguments EAGERLY (public wrapper + private iterator local
  function) so a null source/middleware/chain-member fails fast at the call site, matching the
  base classes' TransformAsync pattern instead of throwing only on first enumeration. Tests for
  the null cases become synchronous (Assert.Throws).
- Reinstate ConfigureAwait(false) on the WithMiddleware 'await foreach' loops for
  net462/netstandard2.0 sync-context safety (public extension is more exposed than the internal
  worker loops); the equivalent bool mutants are marked Stryker-disable.
- README: add Per-item Error Handling / Middleware / Retry Seam feature rows (the #84 error
  row was also missing).
- CHANGELOG: note the internal #338 time-source seam under [Unreleased].

452 tests pass; 100% mutation (0 survivors); all 11 TFMs clean. No public API change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- PackageValidationBaselineVersion 0.18.1 -> 0.19.0; the int->long break is gone vs 0.19.0
  (0.19.0 already ships long counters), so 0.20 is purely additive — CompatibilitySuppressions.xml
  removed and pack validates clean vs 0.19.0.
- Promote CHANGELOG [Unreleased] -> [0.20.0] (2026-07-30).
- Promote the 0.20 public surface (retry seam / middleware / IReportsItemErrors) PublicAPI.Unshipped
  -> Shipped (216 entries, ordinal-sorted); reset Unshipped.

Version already 0.20.0. 452 tests; all 11 TFMs clean; PublicAPI RS0016+RS0017 validated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 29, 2026 01:23

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

…; avoid reverting them on release)

# Conflicts:
#	CHANGELOG.md

@github-actions github-actions Bot left a comment

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.

BenchmarkDotNet

Details
Benchmark suite Current: 4850525 Previous: 5e5eff8 Ratio
Wolfgang.Etl.Abstractions.Benchmarks.ExtractorBenchmarks.Extract_NoProgress(RecordCount: 1000) 26180.478098551434 ns (± 163.09071239051553) 34346.27861531576 ns (± 767.7271166066424) 0.76
Wolfgang.Etl.Abstractions.Benchmarks.ExtractorBenchmarks.Extract_WithProgress(RecordCount: 1000) 27760.10508219401 ns (± 330.4004072958602) 35690.69081624349 ns (± 121.99168762232769) 0.78
Wolfgang.Etl.Abstractions.Benchmarks.ExtractorBenchmarks.Extract_NoProgress(RecordCount: 100000) 2585459.24609375 ns (± 2664.7329475021124) 3607541.7135416665 ns (± 4820.553954320889) 0.72
Wolfgang.Etl.Abstractions.Benchmarks.ExtractorBenchmarks.Extract_WithProgress(RecordCount: 100000) 2706979.3723958335 ns (± 7911.503269506926) 3485849.25 ns (± 1446.7723543303268) 0.78
Wolfgang.Etl.Abstractions.Benchmarks.PipelineBenchmarks.FluentPipeline(RecordCount: 1000) 25344.441060384113 ns (± 89.81456462245298) 31901.971710205078 ns (± 107.64253574413279) 0.79
Wolfgang.Etl.Abstractions.Benchmarks.PipelineBenchmarks.ManualComposition(RecordCount: 1000) 24542.37175496419 ns (± 77.88746524664231) 32595.139221191406 ns (± 132.60242047288244) 0.75
Wolfgang.Etl.Abstractions.Benchmarks.PipelineBenchmarks.BaseClassComposition(RecordCount: 1000) 58122.23839314779 ns (± 98.74054413246576) 74138.58180745442 ns (± 339.1681844140048) 0.78
Wolfgang.Etl.Abstractions.Benchmarks.PipelineBenchmarks.FluentPipeline(RecordCount: 100000) 2494574.7083333335 ns (± 1691.8449564856976) 3219619.8020833335 ns (± 3141.7580180829386) 0.77
Wolfgang.Etl.Abstractions.Benchmarks.PipelineBenchmarks.ManualComposition(RecordCount: 100000) 2487880.421875 ns (± 3388.7047087723986) 3208296.451171875 ns (± 1993.4468905203664) 0.78
Wolfgang.Etl.Abstractions.Benchmarks.PipelineBenchmarks.BaseClassComposition(RecordCount: 100000) 5410841.114583333 ns (± 2483.5733481380953) 8852946.604166666 ns (± 19787.514036475975) 0.61

This comment was automatically generated by workflow using github-action-benchmark.

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.

2 participants