Pattern for delegating async behavior in C#
The following delegate will be used to handle asynchronous implementation concerns
public delegate Task PipelineStep<TContext>(TContext context);
From the comments it was indicated
One specific example is adding multiple steps/tasks required to complete a "transaction" (LOB functionality)
The following class allows for the building up of a delegate to handle such steps in a fluent manner similar to .net core middleware
public class PipelineBuilder<TContext> {
private readonly Stack<Func<PipelineStep<TContext>, PipelineStep<TContext>>> steps =
new Stack<Func<PipelineStep<TContext>, PipelineStep<TContext>>>();
public PipelineBuilder<TContext> AddStep(Func<PipelineStep<TContext>, PipelineStep<TContext>> step) {
steps.Push(step);
return this;
}
public PipelineStep<TContext> Build() {
var next = new PipelineStep<TContext>(context => Task.CompletedTask);
while (steps.Any()) {
var step = steps.Pop();
next = step(next);
}
return next;
}
}
The following extension allow for simpler in-line setup using wrappers
public static class PipelineBuilderAddStepExtensions {
public static PipelineBuilder<TContext> AddStep<TContext>
(this PipelineBuilder<TContext> builder,
Func<TContext, PipelineStep<TContext>, Task> middleware) {
return builder.AddStep(next => {
return context => {
return middleware(context, next);
};
});
}
public static PipelineBuilder<TContext> AddStep<TContext>
(this PipelineBuilder<TContext> builder, Func<TContext, Task> step) {
return builder.AddStep(async (context, next) => {
await step(context);
await next(context);
});
}
public static PipelineBuilder<TContext> AddStep<TContext>
(this PipelineBuilder<TContext> builder, Action<TContext> step) {
return builder.AddStep((context, next) => {
step(context);
return next(context);
});
}
}
It can be extended further as needed for additional wrappers.
An example use-case of the delegate in action is demonstrated in the following test
[TestClass]
public class ProcessBuilderTests {
[TestMethod]
public async Task Should_Process_Steps_In_Sequence() {
//Arrange
var expected = 11;
var builder = new ProcessBuilder()
.AddStep(context => context.Result = 10)
.AddStep(async (context, next) => {
//do something before
//pass context down stream
await next(context);
//do something after;
})
.AddStep(context => { context.Result += 1; return Task.CompletedTask; });
var process = builder.Build();
var args = new ProcessingArgs();
//Act
await process.Invoke(args);
//Assert
args.Result.Should().Be(expected);
}
public class ProcessBuilder : PipelineBuilder<ProcessingArgs> {
}
public class ProcessingArgs : EventArgs {
public int Result { get; set; }
}
}