Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions ETL-Abstractions.sln
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wolfgang.Etl.Abstractions.Benchmarks", "benchmarks\Wolfgang.Etl.Abstractions.Benchmarks\Wolfgang.Etl.Abstractions.Benchmarks.csproj", "{7855D034-57F4-4CF0-8EB4-5677EF80EC57}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example7-FluentPipeline", "examples\Net8.0\Example7-FluentPipeline\Example7-FluentPipeline.csproj", "{A39682A7-CD1A-40A2-9F5B-F1E045C11F8D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example7-FluentPipeline", "examples\Net4.8\Example7-FluentPipeline\Example7-FluentPipeline.csproj", "{F26156B5-9FDC-46CA-9152-F888932238E1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -321,6 +325,30 @@ Global
{7855D034-57F4-4CF0-8EB4-5677EF80EC57}.Release|x64.Build.0 = Release|Any CPU
{7855D034-57F4-4CF0-8EB4-5677EF80EC57}.Release|x86.ActiveCfg = Release|Any CPU
{7855D034-57F4-4CF0-8EB4-5677EF80EC57}.Release|x86.Build.0 = Release|Any CPU
{A39682A7-CD1A-40A2-9F5B-F1E045C11F8D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A39682A7-CD1A-40A2-9F5B-F1E045C11F8D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A39682A7-CD1A-40A2-9F5B-F1E045C11F8D}.Debug|x64.ActiveCfg = Debug|Any CPU
{A39682A7-CD1A-40A2-9F5B-F1E045C11F8D}.Debug|x64.Build.0 = Debug|Any CPU
{A39682A7-CD1A-40A2-9F5B-F1E045C11F8D}.Debug|x86.ActiveCfg = Debug|Any CPU
{A39682A7-CD1A-40A2-9F5B-F1E045C11F8D}.Debug|x86.Build.0 = Debug|Any CPU
{A39682A7-CD1A-40A2-9F5B-F1E045C11F8D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A39682A7-CD1A-40A2-9F5B-F1E045C11F8D}.Release|Any CPU.Build.0 = Release|Any CPU
{A39682A7-CD1A-40A2-9F5B-F1E045C11F8D}.Release|x64.ActiveCfg = Release|Any CPU
{A39682A7-CD1A-40A2-9F5B-F1E045C11F8D}.Release|x64.Build.0 = Release|Any CPU
{A39682A7-CD1A-40A2-9F5B-F1E045C11F8D}.Release|x86.ActiveCfg = Release|Any CPU
{A39682A7-CD1A-40A2-9F5B-F1E045C11F8D}.Release|x86.Build.0 = Release|Any CPU
{F26156B5-9FDC-46CA-9152-F888932238E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F26156B5-9FDC-46CA-9152-F888932238E1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F26156B5-9FDC-46CA-9152-F888932238E1}.Debug|x64.ActiveCfg = Debug|Any CPU
{F26156B5-9FDC-46CA-9152-F888932238E1}.Debug|x64.Build.0 = Debug|Any CPU
{F26156B5-9FDC-46CA-9152-F888932238E1}.Debug|x86.ActiveCfg = Debug|Any CPU
{F26156B5-9FDC-46CA-9152-F888932238E1}.Debug|x86.Build.0 = Debug|Any CPU
{F26156B5-9FDC-46CA-9152-F888932238E1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F26156B5-9FDC-46CA-9152-F888932238E1}.Release|Any CPU.Build.0 = Release|Any CPU
{F26156B5-9FDC-46CA-9152-F888932238E1}.Release|x64.ActiveCfg = Release|Any CPU
{F26156B5-9FDC-46CA-9152-F888932238E1}.Release|x64.Build.0 = Release|Any CPU
{F26156B5-9FDC-46CA-9152-F888932238E1}.Release|x86.ActiveCfg = Release|Any CPU
{F26156B5-9FDC-46CA-9152-F888932238E1}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -349,6 +377,8 @@ Global
{AF971B90-A335-49AF-8AB6-F387CAED12E4} = {9B9A162C-C5B8-495C-A6D0-8C3135E283B9}
{2D19706F-4199-46BD-B047-C4ED3AEDD90A} = {9B9A162C-C5B8-495C-A6D0-8C3135E283B9}
{7855D034-57F4-4CF0-8EB4-5677EF80EC57} = {AE206253-B766-4B6A-8C08-9E70605A2B27}
{A39682A7-CD1A-40A2-9F5B-F1E045C11F8D} = {3C48157B-5E90-489E-9444-E01F51D59F86}
{F26156B5-9FDC-46CA-9152-F888932238E1} = {336D72A1-8E5E-49DE-83D9-DF6BE458BA24}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {F673635D-58CE-48A5-9AE4-31F4484BED9E}
Expand Down
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,36 @@ await Pipeline
`InvalidOperationException`. Construct a new pipeline per run.
- **Raw exception propagation** — stage exceptions bubble up unchanged; stage instances
retain their `CurrentItemCount` and other state for post-mortem inspection.
- **Caller-owned lifetimes** — the pipeline never disposes the stages you hand it.
- **Caller-owned lifetimes by default** — the pipeline does not dispose the stages you
hand it unless you opt in with `.DisposeStagesOnCompletion()` (see below).

The pipeline is syntactic sugar over the existing `IAsyncEnumerable` composition — there
is no new runtime behavior, no buffering, and no additional allocations per item.

### Disposing stages

By default the caller owns stage lifetimes. When the stages are owned by the call site and
should not outlive the run — the common short-lived case — opt into automatic disposal with
`.DisposeStagesOnCompletion()` instead of wrapping every stage in its own `using`:

```csharp
await Pipeline
.Extract(csvExtractor)
.Transform(parseRecord)
.Load(sqlLoader)
.DisposeStagesOnCompletion()
.RunAsync(cancellationToken);
```

- Each stage that implements `IAsyncDisposable` is disposed via `DisposeAsync`; otherwise, if
it implements `IDisposable`, via `Dispose`. Stages that implement neither are skipped.
- Stages are disposed in **reverse construction order** (loader → transformers → extractor),
matching the LIFO convention of nested `using`/`await using` blocks and DI-scope disposal.
- Disposal runs whether the run succeeded or threw, and every stage is disposed even if an
earlier disposal throws. If the run **succeeded**, any disposal failures surface together as
an `AggregateException`. If the run **threw**, that exception propagates unchanged (disposal
still runs, but its own failures are suppressed so the run's failure stays the primary signal).

---

## ✨ Features
Expand Down
93 changes: 84 additions & 9 deletions docfx_project/docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@ This guide will help you quickly get up and running with Wolfgang.Etl.Abstractio

## Prerequisites

<!-- List any prerequisites needed. For example:
- .NET 8.0 or later
- Visual Studio 2022 or Visual Studio Code
-->
- A project targeting any framework the package supports — .NET Framework 4.6.2+,
.NET Standard 2.0+, or .NET 5.0 through .NET 10.0.
- Any editor: Visual Studio 2022, Visual Studio Code, or JetBrains Rider.

## Installation

Expand All @@ -25,17 +24,93 @@ Install-Package Wolfgang.Etl.Abstractions

## Quick Start

<!-- Add a quick start example. For example: -->
An ETL flow has three kinds of stage — an **extractor** (produces items), zero or
more **transformers** (reshape items), and a **loader** (consumes items). Implement
the matching async interface for each:

```csharp
// Add your quick start code example here
// This should show the simplest way to use your library

using Wolfgang.Etl.Abstractions;

// Example usage
// Extract: produce a stream of items.
internal sealed class NumberExtractor : IExtractAsync<int>
{
public async IAsyncEnumerable<int> ExtractAsync()
{
for (var i = 1; i <= 5; i++)
{
yield return i;
await Task.Yield();
}
}
}

// Transform: map each item to a new shape or type.
internal sealed class ToStringTransformer : ITransformAsync<int, string>
{
public async IAsyncEnumerable<string> TransformAsync(IAsyncEnumerable<int> items)
{
await foreach (var item in items)
{
yield return $"Item {item}";
}
}
}

// Load: consume the final stream.
internal sealed class ConsoleLoader : ILoadAsync<string>
{
public async Task LoadAsync(IAsyncEnumerable<string> items)
{
await foreach (var item in items)
{
Console.WriteLine(item);
}
}
}
```

Then compose them into a single, strongly-typed pipeline with the fluent
`Pipeline` API and run it:

```csharp
await Pipeline
.Extract(new NumberExtractor())
.Transform(new ToStringTransformer())
.Load(new ConsoleLoader())
.RunAsync();
```

The compiler enforces that each stage's output type matches the next stage's
input, so a mismatch is a build error rather than a runtime surprise.

### Optional capabilities

The chain grows to fit the run without changing the stages:

- **Cancellation** — pass a token to `RunAsync(token)`; it is forwarded to every
stage that supports cancellation.
- **Progress** — for stages that implement a progress-capable interface, call
`.WithProgress(progress)` to receive `IProgress<T>` reports. Calling it on a
stage that doesn't support progress is a compile error.
- **Naming** — `.WithName("nightly-import")` labels the pipeline for diagnostics.
- **Stage disposal** — `.DisposeStagesOnCompletion()` disposes each stage (in
reverse construction order) once the run finishes, so short-lived stages the
call site owns don't each need their own `using`.

```csharp
await Pipeline
.Extract(extractor).WithProgress(extractProgress)
.Transform(transformer)
.Load(loader).WithProgress(loadProgress)
.WithName("nightly-import")
.DisposeStagesOnCompletion()
.RunAsync(cancellationToken);
```

A complete, runnable version of the basic walkthrough lives in
[`examples/Net8.0/Example7-FluentPipeline`](https://github.com/Chris-Wolfgang/ETL-Abstractions/tree/main/examples/Net8.0/Example7-FluentPipeline)
(the `Net4.8` folder has the same example for .NET Framework).

## Next Steps

- Explore the [API Reference](../api/index.md) for detailed documentation
Expand Down
27 changes: 19 additions & 8 deletions docfx_project/docs/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,28 @@ Welcome to Wolfgang.Etl.Abstractions!

## Overview

Interface and base classes for building ETLs

<!-- Add your project overview here -->
Wolfgang.Etl.Abstractions provides the interfaces and base classes for building
Extract–Transform–Load flows on top of `IAsyncEnumerable<T>`. You implement an
extractor, zero or more transformers, and a loader, then compose them into a
single strongly-typed, streaming pipeline.

## Key Features

<!-- List the main features of your project. For example:
- Feature 1: Description
- Feature 2: Description
- Feature 3: Description
-->
- **Fluent, type-safe pipeline** — `Pipeline.Extract(...).Transform(...).Load(...).RunAsync()`
composes stages into one runnable flow; the compiler enforces that each stage's
output type matches the next stage's input.
- **Async streaming** — built on `IAsyncEnumerable<T>`, so items flow through the
pipeline without buffering the whole set in memory.
- **Opt-in progress reporting** — progress-capable stages surface `IProgress<T>`
reports with throughput and ETA, on a configurable interval.
- **Cooperative cancellation** — a `CancellationToken` passed to `RunAsync` is
forwarded to every stage that supports it.
- **Optional stage disposal** — `.DisposeStagesOnCompletion()` tears stages down
in reverse order after the run, for call sites that own short-lived stages.
- **Broad target coverage** — .NET Framework 4.6.2+, .NET Standard 2.0+, and
.NET 5.0 through .NET 10.0.

See the [Getting Started](getting-started.md) guide for a full walkthrough.

## Getting Help

Expand Down
9 changes: 9 additions & 0 deletions examples/Net4.8/Example7-FluentPipeline/ConsoleColors.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Example7_FluentPipeline
{
internal class ConsoleColors
{
public const string Green = "\u001b[32m";
public const string Yellow = "\u001b[33m";
public const string Reset = "\u001b[0m";
}
}
23 changes: 23 additions & 0 deletions examples/Net4.8/Example7-FluentPipeline/ETL/ConsoleLoader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Wolfgang.Etl.Abstractions;

namespace Example7_FluentPipeline.ETL
{
internal class ConsoleLoader : ILoadAsync<string>
{
public async Task LoadAsync(IAsyncEnumerable<string> items)
{
Console.WriteLine($"{ConsoleColors.Green}Loading{ConsoleColors.Reset} data to console asynchronously...\n");

await foreach (var item in items)
{
Console.WriteLine($"Loading item: {item}\n");
await Task.Delay(50); // Simulate some delay for loading
}

Console.WriteLine($"{ConsoleColors.Green}Loading{ConsoleColors.Reset} completed.\n");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Wolfgang.Etl.Abstractions;

namespace Example7_FluentPipeline.ETL
{
internal class FibonacciExtractor : IExtractAsync<int>
{
public async IAsyncEnumerable<int> ExtractAsync()
{
Console.WriteLine($"{ConsoleColors.Green}Extracting{ConsoleColors.Reset} Fibonacci numbers asynchronously...\n");

var current = 1;
var previous = 0;
for (var x = 0; x < 10; ++x)
{
Console.WriteLine($"Extracting Fibonacci number {x + 1}: {current}");
yield return current;
var temp = current;
current += previous;
previous = temp;
await Task.Yield(); // Simulate asynchronous operation
}

Console.WriteLine($"{ConsoleColors.Green}Extraction{ConsoleColors.Reset} completed.\n");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Wolfgang.Etl.Abstractions;

namespace Example7_FluentPipeline.ETL
{
internal class IntToStringTransformer : ITransformAsync<int, string>
{
public async IAsyncEnumerable<string> TransformAsync(IAsyncEnumerable<int> items)
{
Console.WriteLine($"{ConsoleColors.Green}Transforming{ConsoleColors.Reset} integers to strings asynchronously...\n");

await foreach (var item in items)
{
Console.WriteLine($"Transforming integer {item} to string.");
await Task.Delay(50); // Simulate some delay for transformation
yield return item.ToString();
}

Console.WriteLine($"{ConsoleColors.Green}Transformation{ConsoleColors.Reset} completed.\n");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net48</TargetFramework>
<RootNamespace>Example7_FluentPipeline</RootNamespace>
<AssemblyName>Example7-FluentPipeline</AssemblyName>
<LangVersion>8</LangVersion>
<Deterministic>true</Deterministic>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<NoWarn>$(NoWarn);CA2007</NoWarn>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.5" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\src\Wolfgang.Etl.Abstractions\Wolfgang.Etl.Abstractions.csproj" />
</ItemGroup>

<!-- Analyzer PackageReferences are centralized in Directory.Build.props -->

</Project>
37 changes: 37 additions & 0 deletions examples/Net4.8/Example7-FluentPipeline/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System;
using System.Threading.Tasks;
using Example7_FluentPipeline.ETL;
using Wolfgang.Etl.Abstractions;

namespace Example7_FluentPipeline
{
internal class Program
{
private static async Task Main()
{
Console.WriteLine($"{ConsoleColors.Green}.NET Version: {Environment.Version}{ConsoleColors.Reset}\n");

Console.WriteLine($"{ConsoleColors.Yellow}Starting ETL process via the fluent Pipeline API...{ConsoleColors.Reset}\n\n");

// Example1 wires the extractor, transformer, and loader together by hand:
//
// var source = extractor.ExtractAsync();
// var transformed = transformer.TransformAsync(source);
// await loader.LoadAsync(transformed);
//
// The fluent Pipeline API composes the same three stages into a single
// strongly-typed chain. The compiler enforces that each stage's output
// type matches the next stage's input — a mismatch is a build error, not
// a runtime surprise. Each stage is passed via its most-derived interface
// so the right overload is selected (see the Pipeline XML docs).
await Pipeline
.Extract(new FibonacciExtractor()) // IExtractAsync<int>
.Transform(new IntToStringTransformer()) // ITransformAsync<int, string>
.Load(new ConsoleLoader()) // ILoadAsync<string>
.WithName("fibonacci-demo") // optional, purely informational
.RunAsync();

Console.WriteLine($"\n\n{ConsoleColors.Yellow}ETL process completed.{ConsoleColors.Reset}");
}
}
}
Loading
Loading