Wzorzec delegowania zachowania asynchronicznego w C #

9

Próbuję zaprojektować klasę, która ujawnia możliwość dodawania problemów związanych z przetwarzaniem asynchronicznym. W programowaniu synchronicznym może to wyglądać

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

w świecie asynchronicznym, gdzie każdy koncern może potrzebować zwrócić zadanie, nie jest to takie proste. Widziałem to na wiele sposobów, ale jestem ciekawy, czy istnieją jakieś najlepsze praktyki, które ludzie znaleźli. Jedną z prostych możliwości jest

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

Czy istnieje jakiś „standard”, który ludzie przyjęli w tym celu? Wydaje się, że nie ma spójnego podejścia, które zaobserwowałem w popularnych interfejsach API.

Jeff
źródło
Nie jestem pewien, co próbujesz zrobić i dlaczego.
Nkosi
Próbuję przekazać obawy dotyczące implementacji zewnętrznemu obserwatorowi (podobnie jak polimorfizm i pragnienie ułożenia nad dziedziczeniem). Głównie, aby uniknąć problematycznego łańcucha dziedziczenia (i faktycznie niemożliwe, ponieważ wymagałoby to wielokrotnego dziedziczenia).
Jeff
Czy obawy są powiązane w jakikolwiek sposób i czy będą rozpatrywane sekwencyjnie czy równolegle?
Nkosi
Wydaje się, że dzielą dostęp do, ProcessingArgswięc byłem zdezorientowany.
Nkosi
1
Właśnie o to chodzi w pytaniu. Wydarzenia nie mogą zwrócić zadania. I nawet jeśli użyję delegata, który zwraca Zadanie T, wynik zostanie utracony
Jeff

Odpowiedzi:

2

Następujący delegat będzie używany do obsługi problemów związanych z implementacją asynchroniczną

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

Z komentarzy zostało wskazane

Jednym konkretnym przykładem jest dodanie wielu kroków / zadań wymaganych do wykonania „transakcji” (funkcjonalność LOB)

Następująca klasa umożliwia zbudowanie delegata do płynnego wykonywania takich kroków, podobnie jak w przypadku oprogramowania pośredniego z rdzeniem .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;
    }
}

Poniższe rozszerzenie pozwala na prostszą instalację w linii za pomocą owijarek

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

W razie potrzeby można go rozszerzyć o dodatkowe owijarki.

Przykładowy przypadek użycia delegata w akcji pokazano w poniższym teście

[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
źródło
Piękny kod.
Jeff
Czy nie chciałbyś poczekać na następny krok? Myślę, że to zależy, czy Add oznacza dodanie kodu do wykonania przed jakimkolwiek innym kodem, który został dodany. Wygląda to bardziej jak „wkładka”
Jeff
1
Kroki @Jeff są domyślnie wykonywane w kolejności, w jakiej zostały dodane do potoku. Domyślna konfiguracja wbudowana pozwala to zmienić ręcznie, jeśli chcesz, na wypadek, gdyby po utworzeniu kopii zapasowej wykonano akcje postu
Nkosi,
Jak byś to zaprojektował / zmienił, gdybym chciał użyć Zadania T w wyniku zamiast ustawiania kontekstu. Rezultat? Czy po prostu zaktualizujesz podpisy i dodasz metodę Insert (zamiast tylko Dodaj), aby oprogramowanie pośrednie mogło komunikować swój wynik do innego oprogramowania pośredniego?
Jeff
1

Jeśli chcesz zachować go jako delegatów, możesz:

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
źródło