# Fluent Pipeline DSL in .NET – Turning Complex Workflows into Readable, Type-Safe Code

**Introduction**

Data-heavy back-end services—imports, ETL routines, validation workflows—often begin life as one long method full of if/else blocks. Soon the logic becomes brittle, difficult to test and almost impossible to extend. The **Fluent Pipeline DSL** pattern offers a clean escape hatch: break the work into single-purpose steps (*handlers*), chain them with a fluent mini-language, and let the compiler guarantee that every piece fits.

## Below we explore the pattern using the **Why → How → What** structure.

# WHY

| Pain point | Consequence |
| --- | --- |
| **Coupled logic** – Each step knows too much about the others. | A change in one area ripples throughout the code base. |
| **Low testability** – Flow control and business logic are intertwined. | Unit tests become huge, flaky, or are skipped altogether. |
| **Poor extensibility** – Adding, removing, or reordering steps is risky. | Teams clone code, and the system degrades into a “big ball of mud.” |

A Fluent Pipeline solves these issues by:

* **Isolating** each action into a self-contained handler.
    
* **Composing** handlers in a clear, linear pipeline.
    
* **Enforcing type safety** so that incompatible steps fail at compile-time.
    
* **Exposing a DSL** that reads like a specification of the business process.
    

---

## **HOW**

**Contract per step**

```csharp
 public interface IHandler<in TIn, TOut>
{
    Task<TOut> HandleAsync(
        TIn input,
        ProcessingContext ctx,
        CancellationToken ct = default);
}
```

**Minimal engine**

```csharp
public sealed class PipelineEngine
{
    private readonly IServiceProvider _sp;
    public PipelineEngine(IServiceProvider sp) => _sp = sp;

    public Task<TOut> RunAsync<TH, TIn, TOut>(

        TIn input, ProcessingContext ctx, CancellationToken ct = default)

        where TH : class, IHandler<TIn, TOut>

        => _sp.GetRequiredService<TH>().HandleAsync(input, ctx, ct);
}
```

**Fluent builder + DSL**

```csharp
public sealed class PipelineBuilder<TCur>
{
    /* … holds engine, current Task, context, token … */

    public PipelineBuilder<TNext> Then<TH, TNext>()

        where TH : class, IHandler<TCur, TNext>     { /* chain logic */ }

    public Task<TCur> FinishAsync() => _currentTask;

}

public static class PipelineDsl
{
    public static PipelineBuilder<TOut> Execute<TH, TIn, TOut>(…);
    public static PipelineBuilder<TNext> Then<TH, TCur, TNext>(…);
}
```

**Handlers are plain services**

```csharp
public sealed class XmlValidation : IHandler<string, string> { … }
public sealed class XmlParsing   : IHandler<string, ParsedDto> { … }
public sealed class SaveToDb     : IHandler<ParsedDto, ImportSummary> { … }
```

**Orchestrate with MediatR (or any caller)**

```csharp
 var summary = await _engine
 .Execute<XmlValidation, string, string>(xml, ctx)
 .Then<XmlParsing,   string, ParsedDto>()
 .Then<SaveToDb,     ParsedDto, ImportSummary>()
 .FinishAsync();
```

The pipeline is **lazy**—nothing runs until await. Cross-cutting concerns (logging, metrics, retries) can wrap each Task centrally without touching individual handlers.

## WHAT – Benefits & Typical Use-Cases

| Benefit | Why it matters in .NET back-ends |
| --- | --- |
| **Extreme modularity** | Swap, insert, or remove steps by changing one line. |
| **Compile-time safety** | Generic signatures catch incompatible chains early. |
| **First-class testing** | Handlers test in isolation; entire pipelines test with mocks. |
| **Clear observability** | Builder can decorate each step for timing and tracing. |
| **Seamless with CQRS / Background jobs** | Command handlers become simple orchestrators; a job scheduler (TickerQ, Hangfire, Azure Queue) triggers the command but stays unaware of the inner flow. |

### External analogy – Image processing in Python

A similar pattern is common in data science pipelines, e.g., `Pillow` handlers: `validate → resize → convert → upload`. Each step receives an image object, transforms it, and the fluent chain expresses the workflow as readable code—exactly the same idea we bring to .NET.

> Use the Fluent Pipeline DSL whenever you have a **sequence of business transformations** that should be **easy to extend, test, and reason about**. Your future self—and your teammates—will thank you.
