var pipeline = new PipelineBuilder<string>
.AddInitial(n => n*12)
.AddStep(n => n.ToString())
.Build();
Executing `pipeline(2)' should return "24".
Also, the type parameter in the builder <string> indicates the type of the result (return type of final step).
Is it even possible? I've fought with the type system of C# and I'm done.
The closer I've got to it is this code:
public class PipelineBuilder<TInitial, TResult>
{
private List<Func<dynamic, dynamic>> functions = new();
public PipelineBuilder<TInitial, TOutput, TResult> AddInitial<TOutput>(Func<TInitial, TOutput> step)
{
functions.Add(o => step(o));
return new PipelineBuilder<TInitial, TOutput, TResult>(functions);
}
public PipelineBuilder<TInput, TOutput, TResult> AddStep<TInput, TOutput>(Func<TOutput, TInput> step)
{
functions.Add(a => step(a));
return new PipelineBuilder<TInput, TOutput, TResult>(functions);
}
}
public class PipelineBuilder<TInput, TOutput, TResult>
{
private List<Func<dynamic, dynamic>> functions;
public PipelineBuilder(List<Func<dynamic, dynamic>> functions)
{
this.functions = functions;
}
public PipelineBuilder<TInput, TNewOutput, TResult> AddStep<TNewOutput>(Func<TOutput, TNewOutput> step)
{
functions.Add(a => step(a));
return new PipelineBuilder<TInput, TNewOutput, TResult>(functions);
}
public Func<TInput, TResult> Build()
{
return input =>
{
dynamic result = input;
foreach (var step in functions)
{
result = step(result);
}
return (TResult)(object)result;
};
}
}
The problem with this approach is that I've found no way to match the output type of the final step with the return type of the builder (TResult).
If the types don't match, all I get is a runtime exception. I can't even know if they match when the Build method is execute due to functions being Func<dynamic, dynamic>.
You can do something very close to what you want by chaining together lambdas as follows:
And then use it as follows:
Demo fiddle #1 here.
Notes:
In your design, there is no way for the compiler to infer the type of the initial argument
n:Adding a type
(int n)to the lambda argument resolves the ambiguity.If the current step result type is not equal to the required final step result type, the extension method
.Build()will not be found, and you will get a compiler error:The error isn't particularly descriptive, but it's still a proper compiler error and not a runtime exception.
Demo fiddle #2 here.
By chaining together lambdas as steps are added, you eliminate the need for the
List<Func<dynamic, dynamic>> functionsmember (and any other use ofdynamic).All that being said, I don't recommend this design. It feels awkward and inconsistent with the LINQ programming style to define the final result type at the beginning when the final type will also be defined by the final
AddStep()call. EliminatingTFinalfrom the initial declaration makes things much simpler:Demo fiddle #3 here.