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

それぞれの関心事がタスクを返す必要がある非同期の世界では、これはそれほど単純ではありません。私はこれがさまざまな方法で行われるのを見てきましたが、人々が見つけたベストプラクティスがあるかどうか知りたいです。簡単な可能性の1つは

 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

1
それがまさに問題のポイントです。イベントはタスクを返すことができません。Tのタスクを返すデリゲートを使用しても、結果は失われます
Jeff

回答:


2

次のデリゲートは、非同期の実装の問題を処理するために使用されます

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

コメントから指摘されました

1つの具体的な例は、「トランザクション」を完了するために必要な複数のステップ/タスクの追加です(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

次に待ってから、ステップを待ってみませんか?追加が、追加された他のコードの前に実行するコードを追加することを意味するかどうかに依存すると思います。
Jeff

1
@Jeffステップは、デフォルトでは、パイプラインに追加された順序で実行されます。デフォルトのインラインセットアップでは、ストリームに戻る途中で実行するポストアクションがある場合に備えて、手動で変更できます
Nkosi

結果として単にcontext.Resultを設定する代わりにTask of Tを使用したい場合、これをどのように設計/変更しますか?ミドルウェアがその結果を別のミドルウェアに伝達できるように、シグネチャを更新し、(Addの代わりに)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;
    }
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.