C#中委托异步行为的模式

9

我正在尝试设计一个类,它暴露添加异步处理的能力。在同步编程中,可能会看起来像这样:

   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();

在异步的世界中,每个关注点可能需要返回一个任务,这并不简单。我见过有很多种方法实现这个功能,但我想知道是否有人发现了最佳实践。其中一个简单的可能性是

 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
        }
   }

有没有一些人们已经采用的“标准”? 我观察到流行的API之间似乎没有一致的方法。

我不确定你试图做什么以及为什么要这样做。 - Nkosi
这些问题是否有关联,它们将按顺序还是并行处理? - Nkosi
他们似乎共享对“ProcessingArgs”的访问权限,所以我感到困惑。 - Nkosi
连续的 - 它们肯定是相关的。一个具体的例子是添加多个步骤/任务以完成“交易”(LOB功能)。 - Jeff
1
这正是问题的关键。事件无法返回任务。即使我使用返回T任务的委托,结果也会丢失。 - Jeff
显示剩余4条评论
2个回答

2
以下委托将用于处理异步实现方面的问题。
public delegate Task PipelineStep<TContext>(TContext context);

从评论中可以看出

一个具体的例子是在完成“事务”(LOB功能)时添加多个步骤/任务

以下类允许以类似于 .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;
    }
}

以下扩展程序允许使用包装器更简单地进行内联设置。
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);
        });
    }
}

如果需要,可以进一步扩展以满足额外的包装需求。

代理在实际应用中的一个示例是在以下测试中演示的。

[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; }
    }
}

你是否希望等待下一个然后等待步骤?我猜这取决于Add是否意味着您添加要在添加的任何其他代码之前执行的代码。现在这种方式更像是“插入”。 - Jeff
1
@Jeff 步骤默认按照它们添加到管道中的顺序执行。默认的内联设置允许您在需要在上游执行后续操作的情况下手动更改该顺序。 - Nkosi
如果我想使用Task of T作为结果,而不仅仅是设置context.Result,你会如何设计/修改它?您是否只需更新签名并添加Insert方法(而不仅仅是Add),以便中间件可以将其结果传达给另一个中间件? - Jeff

1
如果您想保留它作为委托,您可以:
public class Processor
{
    public event Func<ProcessingArgs, Task> Processing;

    public async Task<int?> ProcessAsync()
    {
        if (Processing?.GetInvocationList() is Delegate[] processors)
        {
            var args = new ProcessingArgs();
            foreach (Func<ProcessingArgs, Task> processor in processors)
            {
                await processor(args);
            }
            return args.Result;
        }
        else return null;
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接