Skip to content

feat: generic EtlPipeline core (#147) - #276

Merged
Chris-Wolfgang merged 7 commits into
vNextfrom
feature/147-etl-pipeline-framework
Jul 20, 2026
Merged

feat: generic EtlPipeline core (#147)#276
Chris-Wolfgang merged 7 commits into
vNextfrom
feature/147-etl-pipeline-framework

Conversation

@Chris-Wolfgang

@Chris-Wolfgang Chris-Wolfgang commented Jul 18, 2026

Copy link
Copy Markdown
Owner

Implements the Abstractions-side scope of #147 — the core of a generic, format-agnostic ETL pipeline. Operators live downstream (see the placement note).

Placement decision (option B)

ETL-Transformers already ships Where/Select/SelectMany/Distinct/Take/Skip/Buffer/… as ITransformAsync transformers. Reimplementing them here duplicated that logic, and Abstractions can't depend on Transformers to reuse it (the dependency runs Transformers → Abstractions). So the core stays minimal and the operators become extension methods in Wolfgang.Etl.Transformers (layered on Through), reusing the transformers that already exist there.

What's here (core only)

  • EtlPipeline — static entry: the Source sentinel (extension-dispatch target for format-package source factories) + built-in From(IAsyncEnumerable<T>) / From(ExtractorBase<T,TProgress>) factories. Named EtlPipeline, not Pipeline, since Pipeline is the existing fluent builder.
  • IEtlPipeline<T> — the plumbing: Through(ITransformAsync<T,TOut>) / Through(ITransformWithCancellationAsync<T,TOut>) to append transformer stages, To<TProgress>(LoaderBase<T,TProgress>) to terminate, AsAsyncEnumerable() to escape to raw IAsyncEnumerable.
  • IEtlPipelineSink.RunAsync(progress, ct) — end-to-end cancellation.
  • EtlPipelineProgressRecordsExtracted / RecordsLoaded / Elapsed (the two ends the core observes).

Verification

  • 272 tests pass on net10.0 (15 new: From both sources, Through single/chained/cancellation-aware, AsAsyncEnumerable, transformer exception, mid-stream cancellation, progress counters, all argument guards).
  • src builds clean across all TFMs (net462 → net10.0), warnings-as-errors.

Follow-ups (gated on Abstractions 0.16.0 shipping)

  • ETL-Transformers: add Where/Select/Distinct/… extension methods on IEtlPipeline<T> that wrap the existing transformers via Through.
  • Format packages (ETL-Csv/Json/…): EtlPipeline.Source.CsvExtractor<T>(...) source factories + sink terminators.

Notes

  • Targets vNext; Closes #147 fires when vNext merges to main.
  • New public surface added to PublicAPI.Unshipped.txt (RS0017 enforced; RS0016 off — reconcile at release).
  • New public surface ⇒ ship this vNext cycle as MINOR (0.15.0 → 0.16.0); version left unchanged for release-time management.

Add a generic, format-agnostic ETL pipeline to Wolfgang.Etl.Abstractions that
format packages (Csv/Json/Xml/SqlBulkCopy) will extend with class-named source
factories and sink terminators.

- EtlPipeline: static entry with the Source sentinel (for format-package source
  extensions) and From(IAsyncEnumerable) / From(ExtractorBase) built-in factories.
  Named EtlPipeline (not Pipeline) to avoid clashing with the existing fluent
  Pipeline builder and System.IO.Pipelines.
- IEtlPipeline<T>: lazy, strongly-typed operator surface — Where/Select/SelectMany
  (sync + async), Distinct (custom comparer), Take/Skip, Tap (sync + async),
  Buffer, To(LoaderBase) terminator, and AsAsyncEnumerable escape hatch.
- IEtlPipelineSink.RunAsync(progress, ct) with end-to-end cancellation.
- EtlPipelineProgress record (extracted/loaded/filtered/errored/elapsed).

Nothing runs until the terminal sink enumerates the stream. Cancellation is
observed per record. 30 unit tests cover every operator (sync + async), Buffer,
Distinct-with-comparer, an operator exception, mid-stream cancellation, progress
counters, AsAsyncEnumerable, and all argument guards. All 287 tests pass; src
builds clean across all TFMs (warnings-as-errors).

Closes #147

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

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.

ETL-Transformers already implements Where/Select/SelectMany/Distinct/Take/
Skip/Buffer/etc. as ITransformAsync transformers. Reimplementing them inside
Abstractions duplicated that logic (a third copy after System.Linq.Async and
ETL-Transformers), and Abstractions can't depend on Transformers to reuse it
(Transformers -> Abstractions).

Strip the operators from the core. IEtlPipeline<T> now exposes only the
plumbing: Through(ITransformAsync) / Through(ITransformWithCancellationAsync)
to append transformer stages, To(LoaderBase) to terminate, and
AsAsyncEnumerable() to escape. The LINQ-flavored operators will ship as
extension methods in Wolfgang.Etl.Transformers (layered on Through), reusing
the transformers that already exist there — gated on the Abstractions 0.16.0
release, same as the format-package factories.

EtlPipelineProgress drops RecordsFiltered/RecordsErrored (the core no longer
sees per-record operator decisions) and reports RecordsExtracted /
RecordsLoaded / Elapsed. 272 tests pass; src builds clean across all TFMs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Chris-Wolfgang and others added 2 commits July 18, 2026 17:08
Runnable Net8.0 + Net4.8 example that builds a generic EtlPipeline from an
IAsyncEnumerable source, chains three Through stages (string -> int -> int ->
string, showing the element type flowing across the chain and same-type stages
being fine), terminates with a LoaderBase sink, and reports EtlPipelineProgress
(RecordsExtracted at the source, RecordsLoaded at the sink). Wired into the
solution under the existing Net80/Net48 folders. Both build clean; the Net8.0
build runs end-to-end.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Document the format-agnostic EtlPipeline core (From/Through/To/AsAsyncEnumerable)
alongside the fluent Pipeline: chained Through with type flow, EtlPipelineProgress,
cancellation, the AsAsyncEnumerable escape hatch, the operator (Wolfgang.Etl.Transformers)
and format-package extension layers, a Pipeline-vs-EtlPipeline comparison table, and a
link to Example8. Add a matching key-feature bullet in the introduction.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread examples/Net4.8/Example8-EtlPipeline/Program.cs Outdated
Comment thread examples/Net8.0/Example8-EtlPipeline/Program.cs Outdated
Comment thread src/Wolfgang.Etl.Abstractions/EtlPipeline/IEtlPipeline.cs
Chris-Wolfgang and others added 3 commits July 20, 2026 08:10
The DoubleTransformer read as 'produces a double' but was typed int -> int.
Make it genuinely int -> double (item * 2.0) and FormatTransformer double ->
string, so the chain now flows string -> int -> double -> string — three
distinct types, and the transformer's name matches its output type. Format with
:F1 so the double is visible in the output.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add two Through overloads that take a stream-to-stream delegate instead of an
ITransformAsync class:

  Through<TOut>(Func<IAsyncEnumerable<T>, IAsyncEnumerable<TOut>>)
  Through<TOut>(Func<IAsyncEnumerable<T>, CancellationToken, IAsyncEnumerable<TOut>>)

These are the delegate form of the transformer primitive (same contract as
ITransformAsync.TransformAsync), so they stay at the core's 'append a stage'
level and let callers supply a one-off stage inline without declaring a class.
Distinct from a per-element Func<T,TOut> (that's Select, an operator that lives
in Wolfgang.Etl.Transformers). Unlike AsAsyncEnumerable()+From(), Through keeps
the pipeline's extracted/loaded counting intact. 4 new tests; 276 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a "Supplying a stage" subsection to the generic EtlPipeline docs: Through
accepts either an ITransformAsync class or a stream-to-stream delegate
(Func<IAsyncEnumerable<T>, IAsyncEnumerable<TOut>>, plus a cancellation-aware
overload) for one-off inline stages, distinct from a per-element Select operator.
Also note the minimal From(...).To(...) form when the source output already
matches the loader input.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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