Pipeline
Processes data through an ordered sequence of steps (stages) that transform its input into output.
Context
A request, message, or data must go through several transformations: parse → validate → enrich → save → notify. You want each step to be independent and reorderable.
Problem
- A monolithic function with 8 responsibilities is difficult to test.
- Reordering steps implies rewriting the flow.
Solution
Define a common interface for each stage and compose the pipeline by connecting them. Each stage does one thing and passes the result to the next one.
#Structure
IPipelineStep<TIn, TOut>— generic contract.Pipeline<T>— chains compatible steps.- Each step is testable in isolation.
#C# Example — generic pipeline
public interface IPipelineStep<in TIn, out TOut>
{
TOut Process(TIn input);
}
public class Pipeline<TIn, TOut>
{
private readonly Func<TIn, TOut> _run;
private Pipeline(Func<TIn, TOut> run) => _run = run;
public static Pipeline<T, T> Start<T>() => new(x => x);
public Pipeline<TIn, TNext> Then<TNext>(IPipelineStep<TOut, TNext> next) =>
new(x => next.Process(_run(x)));
public TOut Run(TIn input) => _run(input);
}
// Stages
public class Trim : IPipelineStep<string, string> { public string Process(string s) => s.Trim(); }
public class ToUpper : IPipelineStep<string, string> { public string Process(string s) => s.ToUpperInvariant(); }
public class Hash : IPipelineStep<string, byte[]> { public byte[] Process(string s) => System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(s)); }
// Composition
var pipe = Pipeline<string, string>.Start<string>()
.Then(new Trim())
.Then(new ToUpper())
.Then(new Hash());
byte[] digest = pipe.Run(" hola ");#C# Example — asynchronous pipeline (middleware style)
public delegate Task PipelineDelegate<TContext>(TContext ctx, Func<Task> next);
public class AsyncPipeline<TContext>
{
private readonly List<PipelineDelegate<TContext>> _steps = new();
public AsyncPipeline<TContext> Use(PipelineDelegate<TContext> step) { _steps.Add(step); return this; }
public Task RunAsync(TContext ctx)
{
Func<Task> next = () => Task.CompletedTask;
for (int i = _steps.Count - 1; i >= 0; i--)
{
var step = _steps[i];
var current = next;
next = () => step(ctx, current);
}
return next();
}
}This is exactly the idea of ASP.NET Core middleware and MediatR pipeline behaviors.
When NOT to use it
- When the flow has a lot of branching: Pipeline shines when it is linear.
- When there is high coupling between steps (they need to share internal state).
Tradeoffs
| Pro | Con |
|---|---|
| Each step is SRP and testable | Jumps in the code follow the flow |
| Reorderable and reusable | Generic types can become complex |
| Fits with parallelism / streaming | Errors must be propagated explicitly |
#Variants
- Classic Pipes & Filters (stream-oriented).
- Middleware pipeline (with
next()— ASP.NET style). - Channel-based pipeline (with
System.Threading.Channels).
#extra #pipeline #data-flow