Skip to content

Composable per-item middleware / interceptor (#93) - #334

Merged
Chris-Wolfgang merged 1 commit into
vNext-plus-onefrom
feat/93-middleware
Jul 28, 2026
Merged

Composable per-item middleware / interceptor (#93)#334
Chris-Wolfgang merged 1 commit into
vNext-plus-onefrom
feat/93-middleware

Conversation

@Chris-Wolfgang

Copy link
Copy Markdown
Owner

Closes #93. Stacked on #333 (#94) — base is feat/94-retry-seam.

Summary

Adds a dependency-free middleware / interceptor mechanism for cross-cutting per-item concerns:

  • IItemMiddleware<T>ValueTask<MiddlewareResult<T>> OnItemAsync(T item, CancellationToken).
  • MiddlewareResult<T> — value type with Item + Skip, built via MiddlewareResult.Continue(item) (keep/replace) or MiddlewareResult.Drop<T>() (remove).
  • WithMiddleware(...) extensions on IAsyncEnumerable<T> — a single middleware or an ordered chain (each sees the previous one's output; a drop short-circuits the rest of the chain).
var cleaned = extractor.ExtractAsync(ct)
    .WithMiddleware(new LoggingMiddleware<Order>())
    .WithMiddleware(new ValidationMiddleware<Order>());   // drops invalid orders

Because it decorates a stream, it composes anywhere — extractor/transformer output, loader input, and inside an EtlPipeline:

EtlPipeline.Create().From(src).Through(s => s.WithMiddleware(m)).To(loader);

Design notes

  • Dependency-free, per the lightweight-Abstractions principle — pure interfaces + decorator extensions, no new package reference.
  • MiddlewareResult<T> has full value equality (satisfies CA1815); the factory methods live on a non-generic MiddlewareResult companion (CA1000-clean).
  • Error interaction (Add OnItemError hook + RecordsSkipped (dead-letter / error capture) to base classes #84): middleware runs as ordinary stream stages, so a stage's OnItemError still wraps its own worker — they compose without overlap.

Verification

  • 441 unit tests pass (15 new MiddlewareTests + MiddlewareResult value-semantics: transform, drop, chain order, stop-at-drop, empty chain, token flow, EtlPipeline composition, all null guards).
  • Stryker 100.00 %, 0 survivors (full project; equivalents — ConfigureAwait(false), the GetHashCode formula, and the diagnostic message string — marked with justified // Stryker disable once comments per repo convention).
  • Builds clean across all 11 TFMs; PublicAPI.Unshipped updated (RS0016 completeness + RS0017 correctness both validated).

Notes for release-prep (not in this PR)

Targets the 0.20.0 line (vNext-plus-one). Version bump 0.19.0 → 0.20.0 and baseline → 0.19.0 are deferred to 0.20.0 release-prep, gated on 0.19.0 publishing.

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>
Copilot AI review requested due to automatic review settings July 28, 2026 01:52

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.

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