在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中似乎没有观察到一致的方法。


我不确定您要做什么以及为什么这么做。
恩科西

我试图将实现的关注点委托给外部观察者(类似于多态和对继承的渴望)。主要是为了避免有问题的继承链(并且实际上是不可能的,因为这将需要多重继承)。
杰夫

这些关注点是否以任何方式相关,并且将按顺序或并行处理?
恩科西

他们似乎共享访问权限,ProcessingArgs因此对此我感到困惑。
恩科西

1
这正是问题的重点。事件无法返回任务。即使我使用返回T任务的委托,结果也将丢失
Jeff

Answers:


2

以下委托将用于处理异步实现问题

public delegate Task PipelineStep<TContext>(TContext context);

从评论中可以看出

一个特定的示例是添加完成“事务”(LOB功能)所需的多个步骤/任务。

下列类允许建立代表以类似于.net核心中间件的流畅方式处理此类步骤的委托。

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

漂亮的代码。
杰夫,

您是否要等待下一个然后等待步骤?我想这取决于“添加”是否意味着您要在要添加的任何其他代码之前添加要执行的代码。它的方式更像是“插入”
Jeff

1
默认情况下,@ Jeff步骤按照添加到管道中的顺序执行。默认的内联设置允许您手动更改,以防万一在后退流中需要执行后期操作
Nkosi

如果我想使用T任务而不是仅仅设置context.Result,您将如何设计/更改它?您是否只需更新签名并添加一个Insert方法(而不是仅添加),以便中间件可以将其结果传达给另一个中间件?
杰夫,

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;
    }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.