I'm trying to design a class that exposes the ability to add asynchronous processing concerns. In synchronous programming, this might look like
public class ProcessingArgs : EventArgs
{
public int Result { get; set; }
}
public class Processor
{
public event EventHandler<ProcessingArgs> Processing { get; }
public int Process()
{
var args = new ProcessingArgs();
Processing?.Invoke(args);
return args.Result;
}
}
var processor = new Processor();
processor.Processing += args => args.Result = 10;
processor.Processing += args => args.Result+=1;
var result = processor.Process();
in an asynchronous world, where each concern may need to return a task, this isn't so simple. I've seen this done lots of ways, but I'm curious if there are any best practices that people have found. One simple possibility is
public class Processor
{
public IList<Func<ProcessingArgs, Task>> Processing { get; } =new List<Func<ProcessingArgs, Task>>();
public async Task<int> ProcessAsync()
{
var args = new ProcessingArgs();
foreach(var func in Processing)
{
await func(args);
}
return args.Result
}
}
Is there some "standard" that people have adopted for this? There doesn't seem to be a consistent approach I've observed across popular APIs.
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; }
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With