Muster zum Delegieren von asynchronem Verhalten in C #

9

Ich versuche, eine Klasse zu entwerfen, die die Möglichkeit bietet, Bedenken hinsichtlich der asynchronen Verarbeitung hinzuzufügen. Bei der synchronen Programmierung könnte dies so aussehen

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

In einer asynchronen Welt, in der jedes Unternehmen möglicherweise eine Aufgabe zurückgeben muss, ist dies nicht so einfach. Ich habe dies auf viele Arten gesehen, aber ich bin gespannt, ob es Best Practices gibt, die die Leute gefunden haben. Eine einfache Möglichkeit ist

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

Gibt es einen "Standard", den die Leute dafür übernommen haben? Es scheint keinen konsistenten Ansatz zu geben, den ich bei gängigen APIs beobachtet habe.

Jeff
quelle
Ich bin mir nicht sicher, was Sie versuchen und warum.
Nkosi
Ich versuche, Implementierungsbedenken an einen externen Beobachter zu delegieren (ähnlich wie Polymorphismus und der Wunsch nach Komposition über Vererbung). Hauptsächlich, um eine problematische Vererbungskette zu vermeiden (und tatsächlich unmöglich, da dies eine Mehrfachvererbung erfordern würde).
Jeff
Sind die Bedenken in irgendeiner Weise miteinander verbunden und werden sie nacheinander oder parallel bearbeitet?
Nkosi
Sie scheinen den Zugang zu dem zu teilen, ProcessingArgsalso war ich darüber verwirrt.
Nkosi
1
Genau darum geht es. Ereignisse können keine Aufgabe zurückgeben. Und selbst wenn ich einen Delegaten benutze, der eine Aufgabe von T zurückgibt, geht das Ergebnis verloren
Jeff

Antworten:

2

Der folgende Delegat wird verwendet, um Probleme mit der asynchronen Implementierung zu behandeln

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

Aus den Kommentaren ging hervor

Ein spezielles Beispiel ist das Hinzufügen mehrerer Schritte / Aufgaben, die zum Abschließen einer "Transaktion" erforderlich sind (LOB-Funktionalität).

Die folgende Klasse ermöglicht den Aufbau eines Delegaten, der solche Schritte fließend wie die .net Core Middleware abwickelt

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

Die folgende Erweiterung ermöglicht eine einfachere Inline-Einrichtung mithilfe von Wrappern

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

Es kann bei Bedarf für zusätzliche Wrapper weiter erweitert werden.

Ein Beispiel für einen Anwendungsfall des in Aktion befindlichen Delegaten wird im folgenden Test demonstriert

[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
quelle
Schöner Code.
Jeff
Möchten Sie nicht als nächstes auf den nächsten Schritt warten? Ich denke, es hängt davon ab, ob Hinzufügen impliziert, dass Sie Code hinzufügen, der vor jedem anderen hinzugefügten Code ausgeführt werden soll. So wie es ist, ist es eher wie ein "Insert"
Jeff
1
@ Jeff-Schritte werden standardmäßig in der Reihenfolge ausgeführt, in der sie der Pipeline hinzugefügt wurden. Das Standard-Inline-Setup ermöglicht es Ihnen, dies manuell zu ändern, wenn Sie möchten, dass auf dem Weg zum Upstream Up-Aktionen ausgeführt werden müssen
Nkosi
Wie würden Sie dies entwerfen / ändern, wenn ich Task of T als Ergebnis verwenden möchte, anstatt nur den Kontext festzulegen. Ergebnis? Würden Sie einfach die Signaturen aktualisieren und eine Einfügemethode hinzufügen (anstatt nur Hinzufügen), damit eine Middleware das Ergebnis einer anderen Middleware mitteilen kann?
Jeff
1

Wenn Sie es als Delegierte behalten möchten, können Sie:

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
quelle