Ages ago I wrote some posts about
an approach to pipeline-style code patterns
for C# code. Recently I got a question on a gist I'd written to go with that article, because someone was having issues adding async code into pipeline components. Async patterns are much more common now, but this wasn't something I'd actually tried. Cue some interesting experiments...
Async gets everywhere...url copied!
In my original code I had a basic pipeline step pattern that could map an input to an output:
public interface IPipelineStep<INPUT, OUTPUT>
{
OUTPUT Process(INPUT input);
}
If you want to make the
Process()
method be awaitable, then you have to declare that method slightly differently:
public interface IAsyncPipelineStep<INPUT, OUTPUT>
{
Task<OUTPUT> Process(INPUT input);
}
It has to return a
Task<T>
to keep the compiler happy. In the background the compiler does some trickery to allow your C# to look like a simple synchronous program, but allow some things to happen asynchronously. And that
Task<T>
return data is part of this magic. But this causes a bit of a problem: Now the input of our next step has to take in a
Task<INPUT>. What does the output become? The code above would suggest it turns into
Task<Task<OUTPUT>>
and that doesn't sound right. What would happen in a ten step pipeline? The nested
Tasks would grow out of control...
We want to define our pipeline steps based on their data inputs and outputs. We don't want to have to make the definition based on which position in the overall pipeline it will occupy. That would make our pipeline components really hard to work with – if you have to insert a new step at the start of the pipeline, all subsequent definitions would need to change. And that is Not Good...
So what can we do instead?
A first try at fixing this...url copied!
If the output of a pipeline step that awaits things needs to be a `Task