Pola untuk mendelegasikan perilaku async di C #

9

Saya mencoba merancang kelas yang memperlihatkan kemampuan untuk menambahkan masalah pemrosesan asinkron. Dalam pemrograman sinkron, ini mungkin terlihat seperti

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

di dunia asinkron, di mana setiap masalah mungkin perlu mengembalikan tugas, ini tidak begitu sederhana. Saya telah melihat ini melakukan banyak cara, tetapi saya ingin tahu apakah ada praktik terbaik yang ditemukan orang. Satu kemungkinan sederhana adalah

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

Adakah "standar" yang digunakan orang untuk hal ini? Tampaknya tidak ada pendekatan konsisten yang saya amati di seluruh API populer.

Jeff
sumber
Saya tidak yakin tentang apa yang Anda coba lakukan dan mengapa.
Nkosi
Saya mencoba untuk mendelegasikan keprihatinan implementasi ke pengamat eksternal (mirip dengan polimorfisme dan keinginan untuk komposisi atas warisan). Terutama untuk menghindari rantai pewarisan yang bermasalah (dan sebenarnya tidak mungkin karena akan membutuhkan banyak warisan).
Jeff
Apakah kekhawatiran terkait dengan cara apa pun dan apakah akan diproses secara berurutan atau paralel?
Nkosi
Mereka sepertinya berbagi akses ke ProcessingArgsjadi saya bingung tentang itu.
Nkosi
1
Itulah tepatnya pertanyaannya. Acara tidak dapat mengembalikan tugas. Dan bahkan jika saya menggunakan delegasi yang mengembalikan Tugas T, hasilnya akan hilang
Jeff

Jawaban:

2

Delegasi berikut akan digunakan untuk menangani masalah implementasi asinkron

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

Dari komentar itu ditunjukkan

Salah satu contoh spesifik adalah menambahkan beberapa langkah / tugas yang diperlukan untuk menyelesaikan "transaksi" (fungsionalitas LOB)

Kelas berikut memungkinkan untuk membangun delegasi untuk menangani langkah-langkah tersebut dengan cara yang mirip dengan .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;
    }
}

Ekstensi berikut memungkinkan pengaturan in-line yang lebih sederhana menggunakan pembungkus

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

Ini dapat diperpanjang lebih lanjut sesuai kebutuhan untuk pembungkus tambahan.

Contoh kasus penggunaan delegasi dalam aksi ditunjukkan dalam tes berikut

[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; }
    }
}
Nkosi
sumber
Kode yang indah
Jeff
Tidakkah Anda ingin menunggu selanjutnya kemudian menunggu langkah? Saya kira itu tergantung jika Add menyiratkan bahwa Anda menambahkan kode untuk dieksekusi sebelum kode lain yang telah ditambahkan. Cara itu lebih seperti "memasukkan"
Jeff
1
@ Jeff langkah secara default dieksekusi dalam urutan mereka ditambahkan ke dalam pipa. Pengaturan inline default memungkinkan Anda untuk mengubahnya secara manual jika Anda ingin seandainya ada tindakan posting yang harus dilakukan dalam perjalanan cadangan
Nkosi
Bagaimana Anda mendesain / mengubah ini jika saya ingin menggunakan Task of T sebagai hasilnya alih-alih hanya mengatur konteks. Hasilnya? Apakah Anda hanya memperbarui tanda tangan dan menambahkan metode Sisipkan (bukan hanya Tambahkan) sehingga middleware dapat mengkomunikasikan hasilnya ke middleware lain?
Jeff
1

Jika Anda ingin menjadikannya sebagai delegasi, Anda dapat:

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;
    }
}
Paulo Morgado
sumber